60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
|
|
"""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 <redacted>: %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
|