"""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 = ""