Features: feed CRUD, per-feed ntfy target (incl. private servers), Telegram/webhook channels, keyword filters, image attachments, per-feed intervals, OPML import/export, notification history & stats, users with roles, admin alerts, RU/EN i18n, light/dark theme, notification preview, history search, activity chart. Dockerized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
"""Publishing notifications to an ntfy server."""
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _topic_url(server: str, topic: str) -> str:
|
||||
server = (server or "https://ntfy.sh").rstrip("/")
|
||||
return f"{server}/{quote(topic.strip('/'))}"
|
||||
|
||||
|
||||
def _auth_headers(token: str, username: str, password: str) -> dict[str, str]:
|
||||
"""Build an Authorization header for private ntfy servers."""
|
||||
if token.strip():
|
||||
return {"Authorization": f"Bearer {token.strip()}"}
|
||||
if username.strip():
|
||||
import base64
|
||||
|
||||
raw = f"{username}:{password}".encode()
|
||||
return {"Authorization": "Basic " + base64.b64encode(raw).decode()}
|
||||
return {}
|
||||
|
||||
|
||||
async def publish(
|
||||
*,
|
||||
server: str,
|
||||
topic: str,
|
||||
title: str,
|
||||
message: str,
|
||||
click: str = "",
|
||||
tags: str = "",
|
||||
priority: int = 3,
|
||||
attach: str = "",
|
||||
token: str = "",
|
||||
username: str = "",
|
||||
password: str = "",
|
||||
) -> None:
|
||||
"""Send one notification to ntfy. Raises httpx.HTTPStatusError on failure.
|
||||
|
||||
Title and click URLs must be ASCII (ntfy header limitation), so non-ASCII
|
||||
titles are pushed into the body and the title is best-effort stripped.
|
||||
"""
|
||||
url = _topic_url(server, topic)
|
||||
headers: dict[str, str] = {"Priority": str(priority)}
|
||||
headers.update(_auth_headers(token, username, password))
|
||||
|
||||
ascii_title = title.encode("ascii", "ignore").decode().strip()
|
||||
if ascii_title:
|
||||
headers["Title"] = ascii_title
|
||||
elif title.strip():
|
||||
# Title had only non-ASCII chars — prepend it to the body instead.
|
||||
message = f"{title}\n\n{message}"
|
||||
|
||||
if click:
|
||||
try:
|
||||
click.encode("ascii")
|
||||
headers["Click"] = click
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
if tags.strip():
|
||||
clean = ",".join(t.strip() for t in tags.split(",") if t.strip())
|
||||
if clean:
|
||||
headers["Tags"] = clean
|
||||
if attach:
|
||||
try:
|
||||
attach.encode("ascii")
|
||||
headers["Attach"] = attach
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
resp = await client.post(url, content=message.encode("utf-8"), headers=headers)
|
||||
resp.raise_for_status()
|
||||
Reference in New Issue
Block a user