feat: Receiver OOP hierarchy with per-receiver locale resolution

- Introduce Receiver base class + typed subclasses (TelegramReceiver,
  WebhookReceiver, EmailReceiver, etc.) in core/notifications/receiver.py
- Dispatcher uses typed Receiver objects instead of raw dicts, with
  per-receiver locale-aware template rendering
- load_link_data resolves locale from TelegramChat.language_override at
  load time: TargetReceiver.locale || chat.language_override || chat.language_code
- Add language_override field to TelegramChat (separate from auto-detected
  language_code), with per-chat commands toggle and command dispatch using
  override language
- Add locale field to TargetReceiver for explicit per-receiver overrides
This commit is contained in:
2026-03-23 21:20:31 +03:00
parent b3b6c31c4d
commit 1cfa72888c
16 changed files with 334 additions and 94 deletions
@@ -10,6 +10,7 @@ from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from notify_bridge_core.models.events import ServiceEvent
from notify_bridge_core.notifications.receiver import Receiver, build_receiver
from ..database.models import (
EmailBot,
@@ -17,6 +18,8 @@ from ..database.models import (
NotificationTarget,
NotificationTrackerTarget,
TargetReceiver,
TelegramBot,
TelegramChat,
TemplateConfig,
TemplateSlot,
TrackingConfig,
@@ -115,14 +118,44 @@ async def load_link_data(
if not target:
continue
# Load receivers
# Load receivers as typed Receiver objects
recv_result = await session.exec(
select(TargetReceiver).where(
TargetReceiver.target_id == target.id,
TargetReceiver.enabled == True,
)
)
receivers = [dict(r.config) for r in recv_result.all()]
recv_rows = recv_result.all()
# For Telegram targets, resolve locale from TelegramChat
chat_locale_map: dict[str, str] = {}
if target.type == "telegram":
bot_id = target.config.get("bot_id")
if bot_id:
chat_ids = [str(r.config.get("chat_id", "")) for r in recv_rows if r.config.get("chat_id")]
if chat_ids:
chat_result = await session.exec(
select(TelegramChat).where(
TelegramChat.bot_id == bot_id,
TelegramChat.chat_id.in_(chat_ids),
)
)
for chat in chat_result.all():
resolved = (
getattr(chat, 'language_override', '') or
getattr(chat, 'language_code', '') or ''
)
if resolved:
chat_locale_map[chat.chat_id] = resolved[:2].lower()
receivers: list[Receiver] = []
for r in recv_rows:
explicit_locale = getattr(r, 'locale', '') or ''
locale = explicit_locale
if not locale and target.type == "telegram":
chat_id = str(r.config.get("chat_id", ""))
locale = chat_locale_map.get(chat_id, "")
receivers.append(build_receiver(target.type, dict(r.config), locale))
tracking_config = None
if tt.tracking_config_id: