834092a3ec
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>
176 lines
5.9 KiB
Python
176 lines
5.9 KiB
Python
"""Database models."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from sqlmodel import Field, SQLModel
|
|
|
|
|
|
def _utcnow() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
class Feed(SQLModel, table=True):
|
|
"""A single RSS/Atom feed to monitor."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
title: str = ""
|
|
url: str = Field(index=True)
|
|
|
|
# --- ntfy target (per-feed; empty server falls back to global default) ---
|
|
ntfy_server: str = ""
|
|
ntfy_topic: str = ""
|
|
# Optional access token / Basic-auth for private ntfy servers.
|
|
ntfy_token: str = "" # bearer token (tk_...)
|
|
ntfy_username: str = "" # OR basic auth user
|
|
ntfy_password: str = "" # OR basic auth password
|
|
priority: int = 3 # 1=min .. 5=max
|
|
tags: str = "" # comma separated ntfy tags/emojis
|
|
attach_image: bool = True # attach first image found in the entry
|
|
|
|
# --- alternative delivery channels (per-feed opt-in) ---
|
|
to_telegram: bool = False
|
|
to_webhook: bool = False
|
|
|
|
# --- keyword filters ---
|
|
# Only entries containing at least one include keyword (if any) AND
|
|
# none of the exclude keywords are forwarded. Comma separated, case-insensitive.
|
|
filter_include: str = ""
|
|
filter_exclude: str = ""
|
|
|
|
# --- scheduling ---
|
|
# Per-feed interval in minutes. 0 = use the global default.
|
|
interval: int = 0
|
|
|
|
enabled: bool = True
|
|
send_full_content: bool = False # send entire article: all text, images, videos
|
|
fetch_full_article: bool = False # trafilatura: extract full page content from link
|
|
digest_enabled: bool = False # accumulate entries instead of real-time dispatch
|
|
digest_period_hours: int = 24 # how often to send the digest (1=hourly, 24=daily, 168=weekly)
|
|
last_digest_at: Optional[datetime] = None
|
|
|
|
# --- category ---
|
|
category_id: Optional[int] = Field(default=None, foreign_key="category.id")
|
|
|
|
# --- state ---
|
|
last_checked: Optional[datetime] = None
|
|
last_status: str = "" # human readable result of last check
|
|
error_streak: int = 0 # consecutive failures (for admin alerts)
|
|
created_at: datetime = Field(default_factory=_utcnow)
|
|
|
|
|
|
class Category(SQLModel, table=True):
|
|
"""Admin-defined category for organizing feeds."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True, unique=True)
|
|
sort_order: int = 0
|
|
|
|
|
|
class Article(SQLModel, table=True):
|
|
"""Stored feed entries for the built-in RSS reader."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
feed_id: int = Field(index=True, foreign_key="feed.id")
|
|
feed_title: str = ""
|
|
title: str = ""
|
|
body: str = ""
|
|
full_html: str = ""
|
|
link: str = ""
|
|
image: str = ""
|
|
published_at: Optional[datetime] = None
|
|
is_read: bool = False
|
|
created_at: datetime = Field(default_factory=_utcnow, index=True)
|
|
|
|
|
|
class DigestEntry(SQLModel, table=True):
|
|
"""Pending entries for digest-enabled feeds."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
feed_id: int = Field(index=True, foreign_key="feed.id")
|
|
title: str = ""
|
|
link: str = ""
|
|
body: str = ""
|
|
image: str = ""
|
|
full_html: str = ""
|
|
images: str = "" # JSON array
|
|
videos: str = "" # JSON array
|
|
full_content: bool = False
|
|
created_at: datetime = Field(default_factory=_utcnow)
|
|
|
|
|
|
class SeenEntry(SQLModel, table=True):
|
|
"""Tracks which feed entries have already been pushed to avoid duplicates."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
feed_id: int = Field(index=True, foreign_key="feed.id")
|
|
entry_uid: str = Field(index=True)
|
|
seen_at: datetime = Field(default_factory=_utcnow)
|
|
|
|
|
|
class Notification(SQLModel, table=True):
|
|
"""History of dispatched (or failed) notifications."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
feed_id: int = Field(index=True, foreign_key="feed.id")
|
|
feed_title: str = ""
|
|
title: str = ""
|
|
link: str = ""
|
|
channels: str = "" # e.g. "ntfy,telegram"
|
|
ok: bool = True
|
|
detail: str = "" # error text when ok is False
|
|
created_at: datetime = Field(default_factory=_utcnow, index=True)
|
|
|
|
|
|
class User(SQLModel, table=True):
|
|
"""A web-panel user. Roles: 'admin' (full) or 'viewer' (read-only)."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
username: str = Field(index=True)
|
|
password_hash: str = ""
|
|
role: str = "admin" # admin | viewer
|
|
created_at: datetime = Field(default_factory=_utcnow)
|
|
|
|
|
|
class Settings(SQLModel, table=True):
|
|
"""Singleton settings row (id == 1)."""
|
|
|
|
id: Optional[int] = Field(default=1, primary_key=True)
|
|
default_ntfy_server: str = "https://ntfy.sh"
|
|
# Default-server auth, used as a fallback for feeds without their own and
|
|
# for the "send test" action (for ntfy servers with access control).
|
|
default_ntfy_token: str = ""
|
|
default_ntfy_username: str = ""
|
|
default_ntfy_password: str = ""
|
|
check_interval: int = 5 # minutes (global default)
|
|
|
|
# Auth toggle (per-user credentials live in the User table).
|
|
auth_enabled: bool = False
|
|
|
|
# --- Telegram channel ---
|
|
telegram_enabled: bool = False
|
|
telegram_token: str = ""
|
|
telegram_chat_id: str = ""
|
|
|
|
# --- Generic webhook channel ---
|
|
webhook_enabled: bool = False
|
|
webhook_url: str = ""
|
|
|
|
# --- Admin health alerts ---
|
|
alerts_enabled: bool = False
|
|
alert_topic: str = "" # ntfy topic to notify when a feed keeps failing
|
|
alert_threshold: int = 3 # consecutive failures before alerting
|
|
|
|
# --- Default values for new feeds (pre-fill the "Add feed" form) ---
|
|
default_priority: int = 3
|
|
default_tags: str = ""
|
|
default_attach_image: bool = True
|
|
default_interval: int = 0
|
|
|
|
# --- Notification template ---
|
|
notification_template: str = "**{title}**\n\n{body}\n\n[Открыть \u2192]({link})"
|
|
|
|
# --- Proxy for RSS fetching ---
|
|
proxy_url: str = ""
|