···3131 pub legacy_login_body: &'static str,
3232 pub migration_verification_subject: &'static str,
3333 pub migration_verification_body: &'static str,
3434+ pub channel_verified_subject: &'static str,
3535+ pub channel_verified_body: &'static str,
3436}
35373638pub fn get_strings(locale: &str) -> &'static NotificationStrings {
···6668 legacy_login_body: "Hello @{handle},\n\nA login to your account was detected using a legacy app (like Bluesky) that doesn't support TOTP verification.\n\nDetails:\n- Time: {timestamp}\n- IP Address: {ip}\n\nYour TOTP protection was bypassed for this login. The session has limited permissions for sensitive operations.\n\nIf this wasn't you, please:\n1. Change your password immediately\n2. Review your active sessions\n3. Consider disabling legacy app logins in your security settings\n\nStay safe,\n{hostname}",
6769 migration_verification_subject: "Verify your email - {hostname}",
6870 migration_verification_body: "Welcome to {hostname}!\n\nYour account has been migrated successfully. To complete the setup, please verify your email address.\n\nYour verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 48 hours.\n\nOr if you like to live dangerously:\n{verify_link}\n\nIf you did not migrate your account, please ignore this email.",
7171+ channel_verified_subject: "Channel verified - {hostname}",
7272+ channel_verified_body: "Hello {handle},\n\n{channel} has been verified as a notification channel for your account on {hostname}.",
6973};
70747175static STRINGS_ZH: NotificationStrings = NotificationStrings {
···9094 legacy_login_body: "您好 @{handle},\n\n检测到使用不支持 TOTP 验证的传统应用(如 Bluesky)登录您的账户。\n\n详细信息:\n- 时间:{timestamp}\n- IP 地址:{ip}\n\n此次登录绕过了 TOTP 保护。该会话对敏感操作的权限有限。\n\n如果这不是您的操作,请:\n1. 立即更改密码\n2. 检查您的活跃会话\n3. 考虑在安全设置中禁用传统应用登录\n\n请注意安全,\n{hostname}",
9195 migration_verification_subject: "验证您的邮箱 - {hostname}",
9296 migration_verification_body: "欢迎来到 {hostname}!\n\n您的账户已成功迁移。要完成设置,请验证您的邮箱地址。\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在 48 小时后过期。\n\n或者直接点击链接:\n{verify_link}\n\n如果您没有迁移账户,请忽略此邮件。",
9797+ channel_verified_subject: "通知渠道已验证 - {hostname}",
9898+ channel_verified_body: "您好 {handle},\n\n{channel} 已被验证为您在 {hostname} 上的通知渠道。",
9399};
9410095101static STRINGS_JA: NotificationStrings = NotificationStrings {
···114120 legacy_login_body: "@{handle} 様\n\nTOTP 認証に対応していないレガシーアプリ(Bluesky など)からのログインが検出されました。\n\n詳細:\n- 時刻:{timestamp}\n- IP アドレス:{ip}\n\nこのログインでは TOTP 保護がバイパスされました。このセッションは機密操作に対する権限が制限されています。\n\n心当たりがない場合は:\n1. 直ちにパスワードを変更してください\n2. アクティブなセッションを確認してください\n3. セキュリティ設定でレガシーアプリのログインを無効にすることを検討してください\n\nご注意ください。\n{hostname}",
115121 migration_verification_subject: "メールアドレスの認証 - {hostname}",
116122 migration_verification_body: "{hostname} へようこそ!\n\nアカウントの移行が完了しました。設定を完了するには、メールアドレスを認証してください。\n\n認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは48時間後に期限切れとなります。\n\n自己責任でワンクリック認証:\n{verify_link}\n\nアカウントを移行していない場合は、このメールを無視してください。",
123123+ channel_verified_subject: "通知チャンネル認証完了 - {hostname}",
124124+ channel_verified_body: "{handle} 様\n\n{channel} が {hostname} の通知チャンネルとして認証されました。",
117125};
118126119127static STRINGS_KO: NotificationStrings = NotificationStrings {
···138146 legacy_login_body: "안녕하세요 @{handle}님,\n\nTOTP 인증을 지원하지 않는 레거시 앱(예: Bluesky)을 사용한 로그인이 감지되었습니다.\n\n세부 정보:\n- 시간: {timestamp}\n- IP 주소: {ip}\n\n이 로그인에서 TOTP 보호가 우회되었습니다. 이 세션은 민감한 작업에 대한 권한이 제한됩니다.\n\n본인이 아닌 경우:\n1. 즉시 비밀번호를 변경하세요\n2. 활성 세션을 검토하세요\n3. 보안 설정에서 레거시 앱 로그인 비활성화를 고려하세요\n\n{hostname} 드림",
139147 migration_verification_subject: "이메일 인증 - {hostname}",
140148 migration_verification_body: "{hostname}에 오신 것을 환영합니다!\n\n계정 마이그레이션이 완료되었습니다. 설정을 완료하려면 이메일 주소를 인증하세요.\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 48시간 후에 만료됩니다.\n\n위험을 감수하고 원클릭 인증:\n{verify_link}\n\n계정을 마이그레이션하지 않았다면 이 이메일을 무시하세요.",
149149+ channel_verified_subject: "알림 채널 인증 완료 - {hostname}",
150150+ channel_verified_body: "안녕하세요 {handle}님,\n\n{channel}이(가) {hostname}의 알림 채널로 인증되었습니다.",
141151};
142152143153static STRINGS_SV: NotificationStrings = NotificationStrings {
···162172 legacy_login_body: "Hej @{handle},\n\nEn inloggning till ditt konto upptäcktes med en äldre app (som Bluesky) som inte stöder TOTP-verifiering.\n\nDetaljer:\n- Tid: {timestamp}\n- IP-adress: {ip}\n\nDitt TOTP-skydd kringgicks för denna inloggning. Sessionen har begränsade behörigheter för känsliga operationer.\n\nOm detta inte var du:\n1. Ändra ditt lösenord omedelbart\n2. Granska dina aktiva sessioner\n3. Överväg att inaktivera äldre appinloggningar i dina säkerhetsinställningar\n\nVar försiktig,\n{hostname}",
163173 migration_verification_subject: "Verifiera din e-post - {hostname}",
164174 migration_verification_body: "Välkommen till {hostname}!\n\nDitt konto har migrerats framgångsrikt. För att slutföra installationen, verifiera din e-postadress.\n\nDin verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 48 timmar.\n\nEller om du gillar att leva farligt:\n{verify_link}\n\nOm du inte migrerade ditt konto kan du ignorera detta meddelande.",
175175+ channel_verified_subject: "Aviseringskanal verifierad - {hostname}",
176176+ channel_verified_body: "Hej {handle},\n\n{channel} har verifierats som aviseringskanal för ditt konto på {hostname}.",
165177};
166178167179static STRINGS_FI: NotificationStrings = NotificationStrings {
···186198 legacy_login_body: "Hei @{handle},\n\nTilillesi havaittiin kirjautuminen vanhalla sovelluksella (kuten Bluesky), joka ei tue TOTP-vahvistusta.\n\nTiedot:\n- Aika: {timestamp}\n- IP-osoite: {ip}\n\nTOTP-suojauksesi ohitettiin tässä kirjautumisessa. Istunnolla on rajoitetut oikeudet arkaluontoisiin toimintoihin.\n\nJos tämä et ollut sinä:\n1. Vaihda salasanasi välittömästi\n2. Tarkista aktiiviset istuntosi\n3. Harkitse vanhojen sovellusten kirjautumisen poistamista käytöstä turvallisuusasetuksissa\n\nOle varovainen,\n{hostname}",
187199 migration_verification_subject: "Vahvista sähköpostisi - {hostname}",
188200 migration_verification_body: "Tervetuloa palveluun {hostname}!\n\nTilisi on siirretty onnistuneesti. Viimeistele asennus vahvistamalla sähköpostiosoitteesi.\n\nVahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 48 tunnissa.\n\nTai jos pidät vaarallisesta elämästä:\n{verify_link}\n\nJos et siirtänyt tiliäsi, voit jättää tämän viestin huomiotta.",
201201+ channel_verified_subject: "Ilmoituskanava vahvistettu - {hostname}",
202202+ channel_verified_body: "Hei {handle},\n\n{channel} on vahvistettu ilmoituskanavaksi tilillesi palvelussa {hostname}.",
189203};
190204191205pub fn format_message(template: &str, vars: &[(&str, &str)]) -> String {
+64-3
crates/tranquil-comms/src/sender.rs
···6767 }
6868}
69697070+pub fn escape_html(text: &str) -> String {
7171+ text.replace('&', "&")
7272+ .replace('<', "<")
7373+ .replace('>', ">")
7474+}
7575+7076pub fn is_valid_phone_number(number: &str) -> bool {
7177 if number.len() < 2 || number.len() > 20 {
7278 return false;
···244250 let bot_token = std::env::var("TELEGRAM_BOT_TOKEN").ok()?;
245251 Some(Self::new(bot_token))
246252 }
253253+254254+ pub async fn set_webhook(
255255+ &self,
256256+ webhook_url: &str,
257257+ secret_token: Option<&str>,
258258+ ) -> Result<(), SendError> {
259259+ let url = format!("https://api.telegram.org/bot{}/setWebhook", self.bot_token);
260260+ let mut payload = json!({ "url": webhook_url });
261261+ if let Some(secret) = secret_token {
262262+ payload["secret_token"] = json!(secret);
263263+ }
264264+ let response = self
265265+ .http_client
266266+ .post(&url)
267267+ .json(&payload)
268268+ .send()
269269+ .await
270270+ .map_err(|e| SendError::ExternalService(format!("setWebhook request failed: {}", e)))?;
271271+ if !response.status().is_success() {
272272+ let body = response.text().await.unwrap_or_default();
273273+ return Err(SendError::ExternalService(format!(
274274+ "setWebhook returned error: {}",
275275+ body
276276+ )));
277277+ }
278278+ Ok(())
279279+ }
280280+281281+ pub async fn resolve_bot_username(&self) -> Result<String, SendError> {
282282+ let url = format!("https://api.telegram.org/bot{}/getMe", self.bot_token);
283283+ let response = self.http_client.get(&url).send().await.map_err(|e| {
284284+ SendError::ExternalService(format!("Telegram getMe request failed: {}", e))
285285+ })?;
286286+287287+ if !response.status().is_success() {
288288+ let body = response.text().await.unwrap_or_default();
289289+ return Err(SendError::ExternalService(format!(
290290+ "Telegram getMe returned error: {}",
291291+ body
292292+ )));
293293+ }
294294+295295+ let data: serde_json::Value = response.json().await.map_err(|e| {
296296+ SendError::ExternalService(format!("Failed to parse getMe response: {}", e))
297297+ })?;
298298+299299+ data.get("result")
300300+ .and_then(|r| r.get("username"))
301301+ .and_then(|u| u.as_str())
302302+ .map(|s| s.to_string())
303303+ .ok_or_else(|| {
304304+ SendError::ExternalService("getMe response missing username".to_string())
305305+ })
306306+ }
247307}
248308249309#[async_trait]
···254314255315 async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
256316 let chat_id = ¬ification.recipient;
257257- let subject = notification.subject.as_deref().unwrap_or("Notification");
258258- let text = format!("*{}*\n\n{}", subject, notification.body);
317317+ let subject = escape_html(notification.subject.as_deref().unwrap_or("Notification"));
318318+ let body = escape_html(¬ification.body);
319319+ let text = format!("<b>{}</b>\n\n{}", subject, body);
259320 let url = format!("https://api.telegram.org/bot{}/sendMessage", self.bot_token);
260321 let payload = json!({
261322 "chat_id": chat_id,
262323 "text": text,
263263- "parse_mode": "Markdown"
324324+ "parse_mode": "HTML"
264325 });
265326 let mut last_error = None;
266327 for attempt in 0..MAX_RETRIES {
···311311312312 async fn get_comms_prefs(&self, user_id: Uuid) -> Result<Option<UserCommsPrefs>, DbError> {
313313 let row = sqlx::query!(
314314- r#"SELECT email, handle, preferred_comms_channel as "preferred_channel!: CommsChannel", preferred_locale
314314+ r#"SELECT email, handle, preferred_comms_channel as "preferred_channel!: CommsChannel", preferred_locale, telegram_chat_id, discord_id, signal_number
315315 FROM users WHERE id = $1"#,
316316 user_id
317317 )
···323323 handle: Handle::from(r.handle),
324324 preferred_channel: r.preferred_channel,
325325 preferred_locale: r.preferred_locale,
326326+ telegram_chat_id: r.telegram_chat_id,
327327+ discord_id: r.discord_id,
328328+ signal_number: r.signal_number,
326329 }))
327330 }
328331···561564 Ok(row)
562565 }
563566567567+ async fn check_channel_verified_by_did(
568568+ &self,
569569+ did: &Did,
570570+ channel: CommsChannel,
571571+ ) -> Result<Option<bool>, DbError> {
572572+ let row = sqlx::query!(
573573+ r#"SELECT
574574+ email_verified,
575575+ discord_verified,
576576+ telegram_verified,
577577+ signal_verified
578578+ FROM users
579579+ WHERE did = $1"#,
580580+ did.as_str()
581581+ )
582582+ .fetch_optional(&self.pool)
583583+ .await
584584+ .map_err(map_sqlx_error)?;
585585+586586+ Ok(row.map(|r| match channel {
587587+ CommsChannel::Email => r.email_verified,
588588+ CommsChannel::Discord => r.discord_verified,
589589+ CommsChannel::Telegram => r.telegram_verified,
590590+ CommsChannel::Signal => r.signal_verified,
591591+ }))
592592+ }
593593+564594 async fn admin_update_email(&self, did: &Did, email: &str) -> Result<u64, DbError> {
565595 let result = sqlx::query!(
566596 "UPDATE users SET email = $1 WHERE did = $2",
···609639 discord_verified,
610640 telegram_username,
611641 telegram_verified,
642642+ telegram_chat_id,
612643 signal_number,
613644 signal_verified
614645 FROM users WHERE did = $1"#,
···624655 discord_verified: r.discord_verified,
625656 telegram_username: r.telegram_username,
626657 telegram_verified: r.telegram_verified,
658658+ telegram_chat_id: r.telegram_chat_id,
627659 signal_number: r.signal_number,
628660 signal_verified: r.signal_verified,
629661 }))
···676708677709 async fn clear_telegram(&self, user_id: Uuid) -> Result<(), DbError> {
678710 sqlx::query!(
679679- "UPDATE users SET telegram_username = NULL, telegram_verified = FALSE, updated_at = NOW() WHERE id = $1",
711711+ "UPDATE users SET telegram_username = NULL, telegram_verified = FALSE, telegram_chat_id = NULL, updated_at = NOW() WHERE id = $1",
680712 user_id
681713 )
682714 .execute(&self.pool)
···31353167 Ok(tranquil_db_traits::RecoverPasskeyAccountResult {
31363168 passkeys_deleted: deleted.rows_affected(),
31373169 })
31703170+ }
31713171+31723172+ async fn set_unverified_telegram(
31733173+ &self,
31743174+ user_id: Uuid,
31753175+ telegram_username: &str,
31763176+ ) -> Result<(), DbError> {
31773177+ sqlx::query!(
31783178+ r#"UPDATE users SET
31793179+ telegram_username = $1,
31803180+ telegram_verified = CASE WHEN LOWER(telegram_username) = LOWER($1) THEN telegram_verified ELSE FALSE END,
31813181+ telegram_chat_id = CASE WHEN LOWER(telegram_username) = LOWER($1) THEN telegram_chat_id ELSE NULL END,
31823182+ updated_at = NOW()
31833183+ WHERE id = $2"#,
31843184+ telegram_username,
31853185+ user_id
31863186+ )
31873187+ .execute(&self.pool)
31883188+ .await
31893189+ .map_err(map_sqlx_error)?;
31903190+ Ok(())
31913191+ }
31923192+31933193+ async fn store_telegram_chat_id(
31943194+ &self,
31953195+ telegram_username: &str,
31963196+ chat_id: i64,
31973197+ handle: Option<&str>,
31983198+ ) -> Result<Option<Uuid>, DbError> {
31993199+ let result = match handle {
32003200+ Some(h) => sqlx::query_scalar!(
32013201+ "UPDATE users SET telegram_chat_id = $2, telegram_verified = TRUE, updated_at = NOW() WHERE LOWER(telegram_username) = LOWER($1) AND telegram_username IS NOT NULL AND handle = $3 RETURNING id",
32023202+ telegram_username,
32033203+ chat_id,
32043204+ h
32053205+ )
32063206+ .fetch_optional(&self.pool)
32073207+ .await
32083208+ .map_err(map_sqlx_error)?,
32093209+ None => sqlx::query_scalar!(
32103210+ r#"UPDATE users SET telegram_chat_id = $2, telegram_verified = TRUE, updated_at = NOW()
32113211+ WHERE id = (
32123212+ SELECT id FROM users
32133213+ WHERE LOWER(telegram_username) = LOWER($1) AND telegram_username IS NOT NULL AND deactivated_at IS NULL
32143214+ LIMIT 1
32153215+ ) RETURNING id"#,
32163216+ telegram_username,
32173217+ chat_id
32183218+ )
32193219+ .fetch_optional(&self.pool)
32203220+ .await
32213221+ .map_err(map_sqlx_error)?,
32223222+ };
32233223+ Ok(result)
32243224+ }
32253225+32263226+ async fn get_telegram_chat_id(&self, user_id: Uuid) -> Result<Option<i64>, DbError> {
32273227+ let row = sqlx::query_scalar!("SELECT telegram_chat_id FROM users WHERE id = $1", user_id)
32283228+ .fetch_optional(&self.pool)
32293229+ .await
32303230+ .map_err(map_sqlx_error)?;
32313231+ Ok(row.flatten())
31383232 }
31393233}
+10-2
crates/tranquil-pds/src/api/identity/account.rs
···208208 _ => return ApiError::MissingDiscordId.into_response(),
209209 },
210210 "telegram" => match &input.telegram_username {
211211- Some(username) if !username.trim().is_empty() => username.trim().to_string(),
211211+ Some(username) if !username.trim().is_empty() => {
212212+ let clean = username.trim().trim_start_matches('@');
213213+ if !crate::api::validation::is_valid_telegram_username(clean) {
214214+ return ApiError::InvalidRequest(
215215+ "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
216216+ ).into_response();
217217+ }
218218+ clean.to_string()
219219+ }
212220 _ => return ApiError::MissingTelegramUsername.into_response(),
213221 },
214222 "signal" => match &input.signal_number {
···634642 telegram_username: input
635643 .telegram_username
636644 .as_deref()
637637- .map(|s| s.trim())
645645+ .map(|s| s.trim().trim_start_matches('@'))
638646 .filter(|s| !s.is_empty())
639647 .map(String::from),
640648 signal_number: input
+1
crates/tranquil-pds/src/api/mod.rs
···1212pub mod repo;
1313pub mod responses;
1414pub mod server;
1515+pub mod telegram_webhook;
1516pub mod temp;
1617pub mod validation;
1718pub mod verification;
+72-13
crates/tranquil-pds/src/api/notification_prefs.rs
···165165 "signal" => tranquil_db_traits::CommsChannel::Signal,
166166 _ => return Err("Invalid channel".to_string()),
167167 };
168168+ let hostname = pds_hostname();
169169+ let encoded_token = urlencoding::encode(&formatted_token);
170170+ let encoded_identifier = urlencoding::encode(identifier);
171171+ let verify_link = format!(
172172+ "https://{}/app/verify?token={}&identifier={}",
173173+ hostname, encoded_token, encoded_identifier
174174+ );
175175+ let body = format!(
176176+ "Your verification code is: {}\n\nOr verify directly:\n{}",
177177+ formatted_token, verify_link
178178+ );
179179+ let recipient = match comms_channel {
180180+ tranquil_db_traits::CommsChannel::Telegram => state
181181+ .user_repo
182182+ .get_telegram_chat_id(user_id)
183183+ .await
184184+ .ok()
185185+ .flatten()
186186+ .map(|id| id.to_string())
187187+ .unwrap_or_else(|| identifier.to_string()),
188188+ _ => identifier.to_string(),
189189+ };
168190 state
169191 .infra_repo
170192 .enqueue_comms(
171193 Some(user_id),
172194 comms_channel,
173195 tranquil_db_traits::CommsType::ChannelVerification,
174174- identifier,
196196+ &recipient,
175197 Some("Verify your channel"),
176176- &format!("Your verification code is: {}", formatted_token),
198198+ &body,
177199 Some(json!({"code": formatted_token})),
178200 )
179201 .await
···199221 let handle = user_row.handle;
200222 let current_email = user_row.email;
201223224224+ let current_prefs = state
225225+ .user_repo
226226+ .get_notification_prefs(&auth.did)
227227+ .await
228228+ .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
229229+ .ok_or(ApiError::AccountNotFound)?;
230230+231231+ let effective_channel = input
232232+ .preferred_channel
233233+ .as_deref()
234234+ .map(|ch| match ch {
235235+ "email" => Ok(CommsChannel::Email),
236236+ "discord" => Ok(CommsChannel::Discord),
237237+ "telegram" => Ok(CommsChannel::Telegram),
238238+ "signal" => Ok(CommsChannel::Signal),
239239+ _ => Err(ApiError::InvalidRequest(
240240+ "Invalid channel. Must be one of: email, discord, telegram, signal".into(),
241241+ )),
242242+ })
243243+ .transpose()?
244244+ .unwrap_or(current_prefs.preferred_channel);
245245+202246 let mut verification_required: Vec<String> = Vec::new();
203247204248 if let Some(ref channel_str) = input.preferred_channel {
···249293250294 if let Some(ref discord_id) = input.discord_id {
251295 if discord_id.is_empty() {
296296+ if effective_channel == CommsChannel::Discord {
297297+ return Err(ApiError::InvalidRequest(
298298+ "Cannot remove Discord while it is the preferred notification channel".into(),
299299+ ));
300300+ }
252301 state
253302 .user_repo
254303 .clear_discord(user_id)
···267316 if let Some(ref telegram) = input.telegram_username {
268317 let telegram_clean = telegram.trim_start_matches('@');
269318 if telegram_clean.is_empty() {
319319+ if effective_channel == CommsChannel::Telegram {
320320+ return Err(ApiError::InvalidRequest(
321321+ "Cannot remove Telegram while it is the preferred notification channel".into(),
322322+ ));
323323+ }
270324 state
271325 .user_repo
272326 .clear_telegram(user_id)
273327 .await
274328 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
275329 info!(did = %auth.did, "Cleared Telegram username");
330330+ } else if !crate::api::validation::is_valid_telegram_username(telegram_clean) {
331331+ return Err(ApiError::InvalidRequest(
332332+ "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore"
333333+ .into(),
334334+ ));
276335 } else {
277277- request_channel_verification(
278278- &state,
279279- user_id,
280280- &auth.did,
281281- "telegram",
282282- telegram_clean,
283283- None,
284284- )
285285- .await
286286- .map_err(|e| ApiError::InternalError(Some(e)))?;
336336+ state
337337+ .user_repo
338338+ .set_unverified_telegram(user_id, telegram_clean)
339339+ .await
340340+ .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
287341 verification_required.push("telegram".to_string());
288288- info!(did = %auth.did, "Requested Telegram verification");
342342+ info!(did = %auth.did, telegram_username = %telegram_clean, "Stored unverified Telegram username");
289343 }
290344 }
291345292346 if let Some(ref signal) = input.signal_number {
293347 if signal.is_empty() {
348348+ if effective_channel == CommsChannel::Signal {
349349+ return Err(ApiError::InvalidRequest(
350350+ "Cannot remove Signal while it is the preferred notification channel".into(),
351351+ ));
352352+ }
294353 state
295354 .user_repo
296355 .clear_signal(user_id)
···439439 "noMessages": "No messages found.",
440440 "discordInUseWarning": "This Discord ID is already associated with another account.",
441441 "telegramInUseWarning": "This Telegram username is already associated with another account.",
442442- "signalInUseWarning": "This Signal number is already associated with another account."
442442+ "signalInUseWarning": "This Signal number is already associated with another account.",
443443+ "telegramStartBot": "Or send /start {handle} to @{botUsername} manually",
444444+ "telegramOpenLink": "Open Telegram to verify"
443445 },
444446 "repoExplorer": {
445447 "collections": "Collections",
+2
frontend/src/locales/fi.json
···436436 "discordInUseWarning": "Tämä Discord-tunnus on jo yhdistetty toiseen tiliin.",
437437 "telegramInUseWarning": "Tämä Telegram-käyttäjänimi on jo yhdistetty toiseen tiliin.",
438438 "signalInUseWarning": "Tämä Signal-numero on jo yhdistetty toiseen tiliin.",
439439+ "telegramStartBot": "Tai lähetä /start {handle} käyttäjälle @{botUsername} manuaalisesti",
440440+ "telegramOpenLink": "Avaa Telegram vahvistaaksesi",
439441 "failedToLoad": "Asetusten lataus epäonnistui",
440442 "failedToSave": "Asetusten tallennus epäonnistui",
441443 "failedToVerify": "Vahvistus epäonnistui",
···436436 "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.",
437437 "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.",
438438 "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다.",
439439+ "telegramStartBot": "또는 @{botUsername}에게 /start {handle}을 직접 보내세요",
440440+ "telegramOpenLink": "Telegram에서 인증하기",
439441 "failedToLoad": "설정 로딩 실패",
440442 "failedToSave": "설정 저장 실패",
441443 "failedToVerify": "인증 실패",
+2
frontend/src/locales/sv.json
···436436 "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.",
437437 "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.",
438438 "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto.",
439439+ "telegramStartBot": "Eller skicka /start {handle} till @{botUsername} manuellt",
440440+ "telegramOpenLink": "Öppna Telegram för att verifiera",
439441 "failedToLoad": "Kunde inte ladda inställningar",
440442 "failedToSave": "Kunde inte spara inställningar",
441443 "failedToVerify": "Verifiering misslyckades",