RSS → ntfy bridge with modern web UI
build-and-push / docker (push) Has been cancelled

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:
dimon
2026-06-02 21:11:57 +08:00
commit bf52bc3079
28 changed files with 3396 additions and 0 deletions
+75
View File
@@ -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()