feat(telegram): per-chat command localization + unified locale resolver
Two related Telegram changes:
1. Per-chat command localization. setMyCommands now accepts a scope
(BotCommandScopeChat) and deleteMyCommands clears scoped bindings.
Command registration runs three tiers: default → per-language
(Telegram client language) → per-chat (UI override). Saving a
chat's language_override or commands_enabled toggle pushes the
binding to Telegram inline rather than waiting on the 30s
debounced bot-wide sync.
2. Unified Telegram locale resolution. Three test paths (bot test_chat,
target receiver test, target-level fan-out) used to disagree on
locale priority — the target receiver test in particular only
consulted receiver.locale and ignored the chat's language_override.
Introduced pick_telegram_locale (pure) and
resolve_telegram_chat_locale (async DB lookup) in services/notifier
so all three paths share one priority order:
receiver.locale → chat.language_override → chat.language_code → fallback
Fan-out keeps batch-loading TelegramChat rows for efficiency, just
runs them through the same priority function now.
This commit is contained in:
@@ -43,6 +43,62 @@ def _get_test_message(locale: str, target_type: str) -> str:
|
||||
return msgs.get(target_type, msgs.get("webhook", "Test"))
|
||||
|
||||
|
||||
def pick_telegram_locale(
|
||||
*,
|
||||
receiver_locale: str = "",
|
||||
chat_override: str = "",
|
||||
chat_language_code: str = "",
|
||||
fallback: str = "en",
|
||||
) -> str:
|
||||
"""Pick the effective 2-letter locale for a Telegram chat.
|
||||
|
||||
Priority (highest first):
|
||||
1. ``receiver_locale`` — explicit per-receiver override on a target.
|
||||
2. ``chat_override`` — explicit ``TelegramChat.language_override``
|
||||
set in the bot/chat manager UI.
|
||||
3. ``chat_language_code`` — Telegram-provided ``language_code``.
|
||||
4. ``fallback`` — caller-supplied default (e.g. query param).
|
||||
|
||||
All inputs are coerced to lowercase 2-letter codes.
|
||||
"""
|
||||
for candidate in (receiver_locale, chat_override, chat_language_code, fallback):
|
||||
if candidate:
|
||||
return candidate[:2].lower()
|
||||
return "en"
|
||||
|
||||
|
||||
async def resolve_telegram_chat_locale(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
bot_id: int | None,
|
||||
chat_id: str | int | None,
|
||||
receiver: TargetReceiver | None = None,
|
||||
fallback: str = "en",
|
||||
) -> str:
|
||||
"""Look up a Telegram chat and resolve its effective locale.
|
||||
|
||||
Single source of truth for "what language should I send to this chat in?".
|
||||
Used by every Telegram test/preview path (bot test_chat, target test
|
||||
receiver, per-receiver fan-out) so they stay in lockstep.
|
||||
"""
|
||||
from ..database.models import TelegramChat
|
||||
|
||||
chat_row = None
|
||||
if bot_id and chat_id:
|
||||
chat_row = (await session.exec(
|
||||
select(TelegramChat).where(
|
||||
TelegramChat.bot_id == bot_id,
|
||||
TelegramChat.chat_id == str(chat_id),
|
||||
)
|
||||
)).first()
|
||||
return pick_telegram_locale(
|
||||
receiver_locale=getattr(receiver, "locale", "") if receiver else "",
|
||||
chat_override=getattr(chat_row, "language_override", "") if chat_row else "",
|
||||
chat_language_code=getattr(chat_row, "language_code", "") if chat_row else "",
|
||||
fallback=fallback,
|
||||
)
|
||||
|
||||
|
||||
async def _load_receivers(target_id: int) -> list[dict]:
|
||||
"""Load enabled receivers for a target from DB."""
|
||||
engine = get_engine()
|
||||
@@ -343,9 +399,12 @@ async def _send_telegram_test_per_receiver(
|
||||
if not recv_rows:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
# Resolve per-receiver locale
|
||||
# Batch-load TelegramChat rows so per-receiver locale picks don't
|
||||
# round-trip the DB N times. Priority resolution then runs through the
|
||||
# shared pick_telegram_locale() helper so single-shot test endpoints
|
||||
# and this fan-out agree on the same rules.
|
||||
chat_ids = [str(r.config.get("chat_id", "")) for r in recv_rows if r.config.get("chat_id")]
|
||||
chat_locale_map: dict[str, str] = {}
|
||||
chat_row_map: dict[str, TelegramChat] = {}
|
||||
if bot_id and chat_ids:
|
||||
chat_rows = (await session.exec(
|
||||
select(TelegramChat).where(
|
||||
@@ -353,13 +412,7 @@ async def _send_telegram_test_per_receiver(
|
||||
TelegramChat.chat_id.in_(chat_ids),
|
||||
)
|
||||
)).all()
|
||||
for chat in chat_rows:
|
||||
override = (
|
||||
getattr(chat, "language_override", "") or
|
||||
getattr(chat, "language_code", "") or ""
|
||||
)
|
||||
if override:
|
||||
chat_locale_map[chat.chat_id] = override[:2].lower()
|
||||
chat_row_map = {chat.chat_id: chat for chat in chat_rows}
|
||||
|
||||
http = await get_http_session()
|
||||
client = TelegramClient(http, bot_token)
|
||||
@@ -374,9 +427,14 @@ async def _send_telegram_test_per_receiver(
|
||||
chat_id = str(r.config.get("chat_id", ""))
|
||||
if not chat_id:
|
||||
return None
|
||||
explicit = getattr(r, "locale", "") or ""
|
||||
locale = explicit or chat_locale_map.get(chat_id) or default_locale
|
||||
message = _get_test_message(locale[:2].lower(), "telegram")
|
||||
chat_row = chat_row_map.get(chat_id)
|
||||
locale = pick_telegram_locale(
|
||||
receiver_locale=getattr(r, "locale", "") or "",
|
||||
chat_override=getattr(chat_row, "language_override", "") if chat_row else "",
|
||||
chat_language_code=getattr(chat_row, "language_code", "") if chat_row else "",
|
||||
fallback=default_locale,
|
||||
)
|
||||
message = _get_test_message(locale, "telegram")
|
||||
async with sem:
|
||||
return await client.send_message(
|
||||
chat_id=chat_id,
|
||||
|
||||
Reference in New Issue
Block a user