ef942b77cc
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.
196 lines
6.3 KiB
Python
196 lines
6.3 KiB
Python
"""Target receiver management API routes (nested under targets)."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel
|
|
from sqlmodel import select
|
|
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 (
|
|
_get_test_message,
|
|
resolve_telegram_chat_locale,
|
|
send_to_receiver,
|
|
)
|
|
from .helpers import get_owned_entity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/targets/{target_id}/receivers", tags=["target-receivers"])
|
|
|
|
|
|
class ReceiverCreate(BaseModel):
|
|
name: str = ""
|
|
config: dict[str, Any] = {}
|
|
locale: str = ""
|
|
enabled: bool = True
|
|
|
|
|
|
class ReceiverUpdate(BaseModel):
|
|
name: str | None = None
|
|
config: dict[str, Any] | None = None
|
|
locale: str | None = None
|
|
enabled: bool | None = None
|
|
|
|
|
|
def _receiver_key(target_type: str, config: dict[str, Any]) -> str:
|
|
"""Derive a unique key for deduplication from receiver config."""
|
|
key_fields = {
|
|
"telegram": "chat_id",
|
|
"webhook": "url",
|
|
"email": "email",
|
|
"discord": "webhook_url",
|
|
"slack": "webhook_url",
|
|
"ntfy": "topic",
|
|
"matrix": "room_id",
|
|
}
|
|
field = key_fields.get(target_type, "")
|
|
return str(config.get(field, "")) if field else ""
|
|
|
|
|
|
@router.get("")
|
|
async def list_receivers(
|
|
target_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
target = await _get_user_target(session, target_id, user.id)
|
|
result = await session.exec(
|
|
select(TargetReceiver).where(TargetReceiver.target_id == target.id)
|
|
)
|
|
return [_response(r) for r in result.all()]
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
async def create_receiver(
|
|
target_id: int,
|
|
body: ReceiverCreate,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
target = await _get_user_target(session, target_id, user.id)
|
|
key = _receiver_key(target.type, body.config)
|
|
if not key:
|
|
raise HTTPException(status_code=400, detail="Receiver config must include a delivery endpoint (chat_id, url, or email)")
|
|
|
|
# Check for duplicate
|
|
existing = await session.exec(
|
|
select(TargetReceiver).where(
|
|
TargetReceiver.target_id == target.id,
|
|
TargetReceiver.receiver_key == key,
|
|
)
|
|
)
|
|
if existing.first():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Receiver already exists for this target")
|
|
|
|
receiver = TargetReceiver(
|
|
target_id=target.id,
|
|
name=body.name,
|
|
config=body.config,
|
|
receiver_key=key,
|
|
locale=body.locale,
|
|
enabled=body.enabled,
|
|
)
|
|
session.add(receiver)
|
|
await session.commit()
|
|
await session.refresh(receiver)
|
|
return _response(receiver)
|
|
|
|
|
|
@router.put("/{receiver_id}")
|
|
async def update_receiver(
|
|
target_id: int,
|
|
receiver_id: int,
|
|
body: ReceiverUpdate,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
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")
|
|
|
|
for field, value in body.model_dump(exclude_unset=True).items():
|
|
setattr(receiver, field, value)
|
|
# Update receiver_key if config changed
|
|
if body.config is not None:
|
|
target = await session.get(NotificationTarget, target_id)
|
|
receiver.receiver_key = _receiver_key(target.type, receiver.config)
|
|
session.add(receiver)
|
|
await session.commit()
|
|
await session.refresh(receiver)
|
|
return _response(receiver)
|
|
|
|
|
|
@router.post("/{receiver_id}/test")
|
|
async def test_receiver(
|
|
target_id: int,
|
|
receiver_id: int,
|
|
locale: str = Query("en"),
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""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")
|
|
|
|
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)
|
|
|
|
|
|
@router.delete("/{receiver_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_receiver(
|
|
target_id: int,
|
|
receiver_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
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")
|
|
await session.delete(receiver)
|
|
await session.commit()
|
|
|
|
|
|
def _response(r: TargetReceiver) -> dict:
|
|
return {
|
|
"id": r.id,
|
|
"target_id": r.target_id,
|
|
"name": r.name,
|
|
"config": dict(r.config),
|
|
"receiver_key": r.receiver_key,
|
|
"locale": getattr(r, 'locale', '') or '',
|
|
"enabled": r.enabled,
|
|
"created_at": r.created_at.isoformat(),
|
|
}
|
|
|
|
|
|
async def _get_user_target(session: AsyncSession, target_id: int, user_id: int) -> NotificationTarget:
|
|
return await get_owned_entity(
|
|
session, NotificationTarget, target_id, user_id,
|
|
not_found_msg="Target not found",
|
|
)
|