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:
2026-04-25 14:41:28 +03:00
parent 711f218622
commit ef942b77cc
5 changed files with 286 additions and 47 deletions
@@ -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)