From 31526319118a1e38cca2232abee8bc4e1887ce98 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Tue, 19 May 2026 00:07:32 +0300 Subject: [PATCH] fix: preserve Telegram usernames with underscores --- backend/bot/utils/text_sanitizer.py | 15 +++++++++++---- frontend/src/lib/webapp/formatters.js | 4 ++-- tests/test_text_sanitizer.py | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 tests/test_text_sanitizer.py diff --git a/backend/bot/utils/text_sanitizer.py b/backend/bot/utils/text_sanitizer.py index 3ee81ce..18d63b4 100644 --- a/backend/bot/utils/text_sanitizer.py +++ b/backend/bot/utils/text_sanitizer.py @@ -199,10 +199,17 @@ def sanitize_display_name(value: Optional[str]) -> Optional[str]: def sanitize_username(value: Optional[str]) -> Optional[str]: if value is None: return None - clean = value.strip() - clean = clean.lstrip("@") - clean = _remove_patterns(clean) - return _finalize(clean) + clean = unicodedata.normalize("NFKC", str(value)) + clean = clean.strip().lstrip("@").strip() + if not clean: + return None + + # Telegram usernames are already a constrained identifier, not free-form display text. + # Do not apply display-name anti-spoofing filters here: they remove words like + # "service" or "support" from valid usernames such as "name_service". + if re.fullmatch(r"[A-Za-z0-9_-]{1,64}", clean): + return clean + return None def username_for_display(username: Optional[str], with_at: bool = False) -> str: diff --git a/frontend/src/lib/webapp/formatters.js b/frontend/src/lib/webapp/formatters.js index a1ecf6e..ff7b100 100644 --- a/frontend/src/lib/webapp/formatters.js +++ b/frontend/src/lib/webapp/formatters.js @@ -47,10 +47,10 @@ export function normalizedEmail(value) { } export function telegramName(profile, fallback) { + const username = String(profile?.username || "").trim(); + if (username) return `@${username}`; const first = String(profile?.first_name || "").trim(); const last = String(profile?.last_name || "").trim(); if (first || last) return `${first} ${last}`.trim(); - const username = String(profile?.username || "").trim(); - if (username) return `@${username}`; return fallback; } diff --git a/tests/test_text_sanitizer.py b/tests/test_text_sanitizer.py new file mode 100644 index 0000000..c53f7ef --- /dev/null +++ b/tests/test_text_sanitizer.py @@ -0,0 +1,24 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "backend")) + +from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username, username_for_display + + +def test_sanitize_username_preserves_underscore_suffixes(): + assert sanitize_username("ik_end") == "ik_end" + assert sanitize_username("name_service") == "name_service" + assert sanitize_username("@client_support") == "client_support" + assert sanitize_username("telegram_user") == "telegram_user" + assert username_for_display("ik_end", with_at=True) == "@ik_end" + assert username_for_display("name_service", with_at=True) == "@name_service" + + +def test_sanitize_username_rejects_free_form_values_instead_of_truncating(): + assert sanitize_username("https://t.me/name_service") is None + assert sanitize_username("name service") is None + + +def test_display_name_filters_still_apply_to_free_form_names(): + assert sanitize_display_name("Name service") == "Name"