"""Alternative delivery channels and a unified dispatcher. A single feed entry can fan out to several channels: ntfy (always, if a topic is set), Telegram, and a generic webhook. Each channel is independent — one failing does not block the others. dispatch() returns which channels succeeded and an error string describing any failures (for the history log). """ from __future__ import annotations import logging from dataclasses import dataclass, field import httpx from . import ntfy from .models import Feed, Settings log = logging.getLogger("delivery") @dataclass class Message: source: str # feed title title: str # entry title body: str # plain-text summary link: str = "" image: str = "" # image URL, if any @dataclass class DispatchResult: channels: list[str] = field(default_factory=list) # succeeded channels errors: list[str] = field(default_factory=list) @property def ok(self) -> bool: return not self.errors and bool(self.channels) @property def detail(self) -> str: return "; ".join(self.errors) async def _send_telegram(settings: Settings, msg: Message) -> None: token = settings.telegram_token.strip() chat_id = settings.telegram_chat_id.strip() if not token or not chat_id: raise ValueError("Telegram не настроен (токен/chat_id)") text = f"{_esc(msg.title)}" if msg.source: text = f"📡 {_esc(msg.source)}\n{text}" if msg.body: text += f"\n\n{_esc(msg.body[:600])}" if msg.link: text += f'\n\nОткрыть →' async with httpx.AsyncClient(timeout=20) as client: resp = await client.post( f"https://api.telegram.org/bot{token}/sendMessage", json={ "chat_id": chat_id, "text": text, "parse_mode": "HTML", "disable_web_page_preview": False, }, ) resp.raise_for_status() async def _send_webhook(settings: Settings, feed: Feed, msg: Message) -> None: url = settings.webhook_url.strip() if not url: raise ValueError("Webhook URL не задан") payload = { "feed": msg.source, "feed_url": feed.url, "title": msg.title, "body": msg.body, "link": msg.link, "image": msg.image, } async with httpx.AsyncClient(timeout=20) as client: resp = await client.post(url, json=payload) resp.raise_for_status() def _esc(text: str) -> str: return text.replace("&", "&").replace("<", "<").replace(">", ">") async def dispatch(feed: Feed, settings: Settings, msg: Message) -> DispatchResult: """Send a message across every channel enabled for this feed.""" result = DispatchResult() server = feed.ntfy_server.strip() or settings.default_ntfy_server full_title = f"{msg.source}: {msg.title}" if msg.source else msg.title # Per-feed auth wins; otherwise fall back to the default-server credentials. has_feed_auth = bool(feed.ntfy_token.strip() or feed.ntfy_username.strip()) token = feed.ntfy_token if has_feed_auth else settings.default_ntfy_token username = feed.ntfy_username if has_feed_auth else settings.default_ntfy_username password = feed.ntfy_password if has_feed_auth else settings.default_ntfy_password # --- ntfy (default channel; requires a topic) --- if feed.ntfy_topic.strip(): try: await ntfy.publish( server=server, topic=feed.ntfy_topic, title=full_title, message=msg.body or "(нет описания)", click=msg.link, tags=feed.tags, priority=feed.priority, attach=msg.image if feed.attach_image else "", token=token, username=username, password=password, ) result.channels.append("ntfy") except Exception as exc: # noqa: BLE001 result.errors.append(f"ntfy: {exc}") # --- Telegram --- if feed.to_telegram and settings.telegram_enabled: try: await _send_telegram(settings, msg) result.channels.append("telegram") except Exception as exc: # noqa: BLE001 result.errors.append(f"telegram: {exc}") # --- Webhook --- if feed.to_webhook and settings.webhook_enabled: try: await _send_webhook(settings, feed, msg) result.channels.append("webhook") except Exception as exc: # noqa: BLE001 result.errors.append(f"webhook: {exc}") return result async def send_admin_alert(settings: Settings, text: str) -> None: """Best-effort health alert to the admin ntfy topic.""" if not settings.alerts_enabled or not settings.alert_topic.strip(): return try: await ntfy.publish( server=settings.default_ntfy_server, topic=settings.alert_topic, title="RSS to ntfy — alert", message=text, tags="warning", priority=4, token=settings.default_ntfy_token, username=settings.default_ntfy_username, password=settings.default_ntfy_password, ) except Exception as exc: # noqa: BLE001 log.warning("admin alert failed: %s", exc)