8 major features: trafilatura, digest, ntfy actions, templates, FTS5 search, backup/restore, proxy, RSS reader
build-and-push / docker (push) Has been cancelled

- Full article extraction via trafilatura (fetch_full_article)
- Digest mode with configurable period (digest_enabled, digest_period_hours)
- ntfy Actions buttons (Open article, Open feed)
- Notification templates with {title}, {body}, {link}, {source}, {image_url}
- FTS5 full-text search in notification history
- Database backup/restore (download/upload .db)
- HTTP/SOCKS proxy for RSS feed fetching (proxy_url setting)
- Built-in RSS reader tab with categories, unread counts, article detail view
- Auto-category 'Общее' for feeds without a category
- Article storage (Article table) for reader
- DigestEntry model for pending digest entries

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dimon
2026-06-03 20:47:46 +08:00
parent f8d2c31658
commit 834092a3ec
13 changed files with 1414 additions and 44 deletions
+75 -15
View File
@@ -24,7 +24,11 @@ class Message:
title: str # entry title
body: str # plain-text summary
link: str = ""
image: str = "" # image URL, if any
image: str = "" # first image URL, for the Attach header
images: list[str] = field(default_factory=list) # all images (full_content mode)
full_html: str = "" # raw HTML body (full_content mode)
videos: list[str] = field(default_factory=list) # video URLs (full_content mode)
full_content: bool = False
@dataclass
@@ -51,7 +55,13 @@ async def _send_telegram(settings: Settings, msg: Message) -> None:
if msg.source:
text = f"📡 <i>{_esc(msg.source)}</i>\n{text}"
if msg.body:
text += f"\n\n{_esc(msg.body[:600])}"
limit = 3500 if msg.full_content else 600
text += f"\n\n{_esc(msg.body[:limit])}"
if msg.full_content:
for img_url in msg.images[:5]:
text += f'\n<a href="{_esc(img_url)}">🖼️ image</a>'
for vid_url in msg.videos[:3]:
text += f'\n<a href="{_esc(vid_url)}">🎬 video</a>'
if msg.link:
text += f'\n\n<a href="{_esc(msg.link)}">Открыть →</a>'
@@ -80,6 +90,10 @@ async def _send_webhook(settings: Settings, feed: Feed, msg: Message) -> None:
"link": msg.link,
"image": msg.image,
}
if msg.full_content:
payload["images"] = msg.images
payload["videos"] = msg.videos
payload["full_html"] = msg.full_html
async with httpx.AsyncClient(timeout=20) as client:
resp = await client.post(url, json=payload)
resp.raise_for_status()
@@ -95,6 +109,26 @@ async def dispatch(feed: Feed, settings: Settings, msg: Message) -> DispatchResu
server = feed.ntfy_server.strip() or settings.default_ntfy_server
full_title = f"{msg.source}: {msg.title}" if msg.source else msg.title
# Apply notification template to body
template = settings.notification_template or "{title}\n\n{body}\n\n{link}"
try:
formatted_body = template.replace("\\n", "\n").format(
title=msg.title,
body=msg.body,
link=msg.link,
source=msg.source,
image_url=msg.image,
)
except (KeyError, ValueError):
formatted_body = msg.body # fallback on template error
# Build default actions for ntfy
ntfy_actions = []
if msg.link:
ntfy_actions.append({"action": "view", "label": "Открыть статью", "url": msg.link})
if feed.url:
ntfy_actions.append({"action": "view", "label": "Открыть ленту", "url": feed.url})
# Per-feed auth wins; otherwise fall back to the default-server credentials.
has_feed_auth = bool(feed.ntfy_token.strip() or feed.ntfy_username.strip())
token = feed.ntfy_token if has_feed_auth else settings.default_ntfy_token
@@ -104,19 +138,45 @@ async def dispatch(feed: Feed, settings: Settings, msg: Message) -> DispatchResu
# --- ntfy (default channel; requires a topic) ---
if feed.ntfy_topic.strip():
try:
await ntfy.publish(
server=server,
topic=feed.ntfy_topic,
title=full_title,
message=msg.body or "(нет описания)",
click=msg.link,
tags=feed.tags,
priority=feed.priority,
attach=msg.image if feed.attach_image else "",
token=token,
username=username,
password=password,
)
if msg.full_content:
# Build markdown body with all images inlined + template applied.
md_body = formatted_body
if msg.images:
for img_url in msg.images:
md_body += f"\n\n![image]({img_url})"
if msg.videos:
for vid_url in msg.videos:
md_body += f"\n\n📹 {vid_url}"
await ntfy.publish(
server=server,
topic=feed.ntfy_topic,
title=full_title,
message=md_body or "(нет описания)",
click=msg.link,
tags=feed.tags,
priority=feed.priority,
attach=msg.image if feed.attach_image else "",
token=token,
username=username,
password=password,
markdown=True,
actions=ntfy_actions,
)
else:
await ntfy.publish(
server=server,
topic=feed.ntfy_topic,
title=full_title,
message=formatted_body or "(нет описания)",
click=msg.link,
tags=feed.tags,
priority=feed.priority,
attach=msg.image if feed.attach_image else "",
token=token,
username=username,
password=password,
actions=ntfy_actions,
)
result.channels.append("ntfy")
except Exception as exc: # noqa: BLE001
result.errors.append(f"ntfy: {exc}")