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:
@@ -11,7 +11,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import NotificationTarget, TargetReceiver, User
|
||||
from ..services.notifier import send_to_receiver
|
||||
from ..services.notifier import (
|
||||
_get_test_message,
|
||||
resolve_telegram_chat_locale,
|
||||
send_to_receiver,
|
||||
)
|
||||
from .helpers import get_owned_entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -130,14 +134,28 @@ async def test_receiver(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Send a test notification to a single receiver."""
|
||||
"""Send a test notification to a single receiver.
|
||||
|
||||
For Telegram targets, locale resolution goes through the shared
|
||||
``resolve_telegram_chat_locale`` helper so the per-chat ``language_override``
|
||||
set in the bot manager is respected here too — previously this endpoint
|
||||
only consulted ``receiver.locale`` and ignored chat-side overrides.
|
||||
"""
|
||||
target = await _get_user_target(session, target_id, user.id)
|
||||
receiver = await session.get(TargetReceiver, receiver_id)
|
||||
if not receiver or receiver.target_id != target_id:
|
||||
raise HTTPException(status_code=404, detail="Receiver not found")
|
||||
|
||||
from ..services.notifier import _get_test_message
|
||||
effective_locale = getattr(receiver, 'locale', '') or locale
|
||||
if target.type == "telegram":
|
||||
effective_locale = await resolve_telegram_chat_locale(
|
||||
session,
|
||||
bot_id=target.config.get("bot_id"),
|
||||
chat_id=receiver.config.get("chat_id"),
|
||||
receiver=receiver,
|
||||
fallback=locale,
|
||||
)
|
||||
else:
|
||||
effective_locale = (getattr(receiver, "locale", "") or locale)[:2].lower()
|
||||
message = _get_test_message(effective_locale, target.type)
|
||||
return await send_to_receiver(target, dict(receiver.config), message)
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from notify_bridge_core.notifications.telegram.client import TelegramClient
|
||||
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..commands.handler import register_commands_with_telegram
|
||||
from ..commands.handler import register_commands_with_telegram, sync_chat_command_binding
|
||||
from ..commands.webhook import register_webhook, unregister_webhook
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import AppSetting, NotificationTarget, TargetReceiver, TelegramBot, TelegramChat, User
|
||||
from ..services.notifier import _get_test_message
|
||||
from ..services.notifier import _get_test_message, resolve_telegram_chat_locale
|
||||
from ..services.telegram_poller import schedule_bot_polling, unschedule_bot_polling
|
||||
from .app_settings import get_setting
|
||||
from .helpers import get_owned_entity
|
||||
@@ -300,26 +300,14 @@ async def test_chat(
|
||||
):
|
||||
"""Send a test message to a chat via the bot.
|
||||
|
||||
Locale resolution: prefer the chat row's ``language_override`` (explicit
|
||||
user choice in the UI), fall back to Telegram's ``language_code`` sent
|
||||
with the chat, and only use the ``?locale=`` query param if neither is
|
||||
set. Otherwise users who set RU on a chat would still see an EN test.
|
||||
Locale resolution is delegated to ``resolve_telegram_chat_locale`` so this
|
||||
endpoint, the per-receiver fan-out, and the target receiver test all
|
||||
apply the same priority order (override → language_code → fallback).
|
||||
"""
|
||||
bot = await _get_user_bot(session, bot_id, user.id)
|
||||
chat_row = (await session.exec(
|
||||
select(TelegramChat).where(
|
||||
TelegramChat.bot_id == bot_id,
|
||||
TelegramChat.chat_id == chat_id,
|
||||
)
|
||||
)).first()
|
||||
effective_locale = locale
|
||||
if chat_row:
|
||||
chat_locale = (
|
||||
getattr(chat_row, 'language_override', '') or
|
||||
getattr(chat_row, 'language_code', '') or ''
|
||||
)
|
||||
if chat_locale:
|
||||
effective_locale = chat_locale[:2].lower()
|
||||
effective_locale = await resolve_telegram_chat_locale(
|
||||
session, bot_id=bot_id, chat_id=chat_id, fallback=locale,
|
||||
)
|
||||
from ..services.http_session import get_http_session
|
||||
message = _get_test_message(effective_locale, "telegram")
|
||||
http = await get_http_session()
|
||||
@@ -347,11 +335,37 @@ async def update_chat(
|
||||
if not chat or chat.bot_id != bot_id:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
updates = body.model_dump(exclude_unset=True)
|
||||
# Track whether anything changed that affects the chat-scoped command
|
||||
# binding registered with Telegram (so the per-chat language_override
|
||||
# actually takes effect on the bot's command list, not just the reply
|
||||
# locale). We push it inline rather than via the debounced auto-sync
|
||||
# so the user sees the change reflected on Telegram immediately —
|
||||
# Telegram clients still cache the menu until the next "/" or chat
|
||||
# re-open, but the source of truth is correct from the moment save
|
||||
# returns.
|
||||
sync_relevant_keys = {"language_override", "commands_enabled"}
|
||||
needs_sync = any(
|
||||
key in updates and getattr(chat, key) != value
|
||||
for key, value in updates.items()
|
||||
if key in sync_relevant_keys
|
||||
)
|
||||
for key, value in updates.items():
|
||||
setattr(chat, key, value)
|
||||
session.add(chat)
|
||||
await session.commit()
|
||||
await session.refresh(chat)
|
||||
if needs_sync:
|
||||
bot = await session.get(TelegramBot, bot_id)
|
||||
if bot is not None:
|
||||
try:
|
||||
await sync_chat_command_binding(bot, chat)
|
||||
except Exception:
|
||||
# Telegram-side failure shouldn't block the save — the
|
||||
# debounced bot-wide sync will retry on the next change.
|
||||
_LOGGER.warning(
|
||||
"Inline command sync failed for bot=%d chat=%s",
|
||||
bot_id, chat.chat_id, exc_info=True,
|
||||
)
|
||||
return _chat_response(chat)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user