from __future__ import annotations import logging from abc import ABC, abstractmethod from contextlib import suppress import httpx from app.config import Settings logger = logging.getLogger(__name__) class Notifier(ABC): @abstractmethod async def send(self, level: str, message: str) -> None: raise NotImplementedError async def close(self) -> None: return None class TelegramNotifier(Notifier): def __init__( self, token: str, chat_id: str, base_url: str = "https://api.telegram.org", timeout: float = 10.0, ) -> None: self.token = token self.chat_id = chat_id self._client = httpx.AsyncClient(base_url=base_url, timeout=timeout) async def send(self, level: str, message: str) -> None: response = await self._client.post( f"/bot{self.token}/sendMessage", json={ "chat_id": self.chat_id, "text": f"[{level.upper()}] {message}", "disable_web_page_preview": True, }, ) response.raise_for_status() async def close(self) -> None: await self._client.aclose() class WebhookNotifier(Notifier): def __init__(self, url: str, timeout: float = 10.0) -> None: self.url = url self._client = httpx.AsyncClient(timeout=timeout) async def send(self, level: str, message: str) -> None: response = await self._client.post(self.url, json={"level": level, "text": message}) response.raise_for_status() async def close(self) -> None: await self._client.aclose() class FanoutNotifier(Notifier): """Sends to every configured channel, never letting one failure block others.""" def __init__(self, notifiers: list[Notifier]) -> None: self.notifiers = [notifier for notifier in notifiers if notifier is not None] async def send(self, level: str, message: str) -> None: for notifier in self.notifiers: try: await notifier.send(level, message) except Exception as exc: # notifications must never break trading logger.warning("notification via %s failed: %s", type(notifier).__name__, exc) async def close(self) -> None: for notifier in self.notifiers: with suppress(Exception): await notifier.close() def build_notifier(settings: Settings) -> Notifier | None: notifiers: list[Notifier] = [] if settings.telegram_bot_token and settings.telegram_chat_id: notifiers.append(TelegramNotifier(settings.telegram_bot_token, settings.telegram_chat_id)) if settings.notify_webhook_url: notifiers.append(WebhookNotifier(settings.notify_webhook_url)) if not notifiers: return None return FanoutNotifier(notifiers)