"""Independent Telegram transport for the sidecar (D7, FR-8, BR-8). Reads its OWN ``WATCHDOG_TG_BOT_TOKEN`` / ``WATCHDOG_TG_CHAT_ID`` and POSTs via ``urllib`` to ``api.telegram.org``. It is FORBIDDEN to import ``src/notifications.py`` or to use the orchestrator's token / chat / functions — otherwise a crash or refactor of the orchestrator would drag down the alert channel (a direct violation of C-1 / BR-8). Missing token/chat -> log and skip (fail-safe), never raise (NFR-3). """ from __future__ import annotations import logging import urllib.parse import urllib.request logger = logging.getLogger("watchdog.notify") _TELEGRAM_API = "https://api.telegram.org" def send_telegram( bot_token: str, chat_id: str, text: str, timeout_s: float = 5.0, *, api_base: str = _TELEGRAM_API, opener=urllib.request.urlopen, ) -> bool: """Send one Telegram message over the sidecar's own bot. never-raise (D8). Returns ``True`` on a delivered message, ``False`` on any failure (missing credentials, network error, non-2xx). ``opener`` / ``api_base`` are injected so tests never touch the real network. """ if not bot_token or not chat_id: logger.warning("watchdog: telegram token/chat not configured -> skip send") return False try: url = f"{api_base}/bot{bot_token}/sendMessage" payload = urllib.parse.urlencode( { "chat_id": chat_id, "text": text, "parse_mode": "HTML", "disable_web_page_preview": "true", } ).encode("utf-8") req = urllib.request.Request(url, data=payload, method="POST") with opener(req, timeout=timeout_s) as resp: status = getattr(resp, "status", None) or resp.getcode() return 200 <= int(status) < 300 except Exception as e: # noqa: BLE001 - delivery is best-effort logger.warning("watchdog: telegram send failed: %s", e) return False class Notifier: """Thin stateful wrapper binding the sidecar credentials for the tick loop.""" def __init__(self, bot_token: str, chat_id: str, timeout_s: float = 5.0): self._token = bot_token self._chat = chat_id self._timeout = timeout_s def send(self, text: str) -> bool: """Best-effort send through the sidecar's own channel (never raises).""" return send_telegram(self._token, self._chat, text, self._timeout)