✨ 8 major features: trafilatura, digest, ntfy actions, templates, FTS5 search, backup/restore, proxy, RSS reader
build-and-push / docker (push) Has been cancelled
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:
+75
-15
@@ -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"
|
||||
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}")
|
||||
|
||||
Reference in New Issue
Block a user