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
+57
View File
@@ -0,0 +1,57 @@
"""OPML import/export for feed subscriptions."""
from __future__ import annotations
from xml.etree import ElementTree as ET
from .models import Feed
def export_opml(feeds: list[Feed]) -> str:
"""Render feeds as an OPML 2.0 document."""
opml = ET.Element("opml", version="2.0")
head = ET.SubElement(opml, "head")
ET.SubElement(head, "title").text = "RSS → ntfy subscriptions"
body = ET.SubElement(opml, "body")
for feed in feeds:
attrs = {
"type": "rss",
"text": feed.title or feed.url,
"title": feed.title or feed.url,
"xmlUrl": feed.url,
}
# Stash the ntfy topic so a re-import keeps the routing.
if feed.ntfy_topic:
attrs["ntfyTopic"] = feed.ntfy_topic
if feed.ntfy_server:
attrs["ntfyServer"] = feed.ntfy_server
ET.SubElement(body, "outline", attrs)
return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
opml, encoding="unicode"
)
def parse_opml(content: str) -> list[dict]:
"""Extract feed definitions from an OPML document.
Returns a list of dicts ready to build Feed rows. Raises ValueError on
malformed XML.
"""
try:
root = ET.fromstring(content)
except ET.ParseError as exc:
raise ValueError(f"Некорректный OPML: {exc}") from exc
feeds: list[dict] = []
for outline in root.iter("outline"):
url = outline.get("xmlUrl") or outline.get("xmlurl")
if not url:
continue
feeds.append(
{
"url": url.strip(),
"title": (outline.get("title") or outline.get("text") or "").strip(),
"ntfy_topic": (outline.get("ntfyTopic") or "").strip(),
"ntfy_server": (outline.get("ntfyServer") or "").strip(),
}
)
return feeds