"""ntfy.sh notifications. Topic is loaded from settings (auto-generated on first run). The topic is functionally a password — anyone subscribed to the topic URL receives notifications. We use cryptographically-random topics (secrets.token_urlsafe(12)) to make brute-force discovery impractical. Notifications are best-effort: a failure to deliver (network down, ntfy.sh outage) is logged but does NOT block the daemon's main loop. """ from __future__ import annotations import logging import requests logger = logging.getLogger(__name__) NTFY_BASE = "https://ntfy.sh" def topic_url(topic: str) -> str: return f"{NTFY_BASE}/{topic}" def notify( topic: str, title: str, message: str, *, priority: str = "default", tags: list[str] | None = None, ) -> bool: """Post one notification. Returns True on HTTP 200, False otherwise. Never raises on transport errors — this path runs from the daemon's main loop and a failed notification should not stop work. """ if not topic: logger.warning("ntfy topic is empty; skipping notification: %s", title) return False headers = { "Title": title, "Priority": priority, } if tags: headers["Tags"] = ",".join(tags) try: resp = requests.post( topic_url(topic), data=message.encode("utf-8"), headers=headers, timeout=10 ) except requests.RequestException as exc: logger.warning("ntfy delivery failed for topic : %s", exc) return False if resp.status_code != 200: logger.warning("ntfy non-200 (%s): %s", resp.status_code, resp.text[:200]) return False return True