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>
153 lines
4.0 KiB
Python
153 lines
4.0 KiB
Python
"""Pydantic request/response schemas for the JSON API."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
|
|
class FeedIn(BaseModel):
|
|
url: str
|
|
title: str = ""
|
|
ntfy_server: str = ""
|
|
ntfy_topic: str = ""
|
|
ntfy_token: str = ""
|
|
ntfy_username: str = ""
|
|
ntfy_password: str = ""
|
|
priority: int = 3
|
|
tags: str = ""
|
|
attach_image: bool = True
|
|
to_telegram: bool = False
|
|
to_webhook: bool = False
|
|
filter_include: str = ""
|
|
filter_exclude: str = ""
|
|
interval: int = 0
|
|
enabled: bool = True
|
|
send_full_content: bool = False
|
|
fetch_full_article: bool = False
|
|
digest_enabled: bool = False
|
|
digest_period_hours: int = 24
|
|
category_id: Optional[int] = None
|
|
|
|
@field_validator("url")
|
|
@classmethod
|
|
def _url_required(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not v:
|
|
raise ValueError("URL ленты обязателен")
|
|
if not v.startswith(("http://", "https://")):
|
|
raise ValueError("URL должен начинаться с http:// или https://")
|
|
return v
|
|
|
|
@field_validator("priority")
|
|
@classmethod
|
|
def _priority_range(cls, v: int) -> int:
|
|
return min(5, max(1, v))
|
|
|
|
@field_validator("interval")
|
|
@classmethod
|
|
def _interval_nonneg(cls, v: int) -> int:
|
|
return max(0, v)
|
|
|
|
|
|
class SettingsIn(BaseModel):
|
|
default_ntfy_server: str = "https://ntfy.sh"
|
|
default_ntfy_token: str = ""
|
|
default_ntfy_username: str = ""
|
|
default_ntfy_password: str = ""
|
|
check_interval: int = 5
|
|
auth_enabled: bool = False
|
|
# Telegram
|
|
telegram_enabled: bool = False
|
|
telegram_token: str = ""
|
|
telegram_chat_id: str = ""
|
|
# Webhook
|
|
webhook_enabled: bool = False
|
|
webhook_url: str = ""
|
|
# Admin alerts
|
|
alerts_enabled: bool = False
|
|
alert_topic: str = ""
|
|
alert_threshold: int = 3
|
|
# Default feed values
|
|
default_priority: int = 3
|
|
default_tags: str = ""
|
|
default_attach_image: bool = True
|
|
default_interval: int = 0
|
|
notification_template: str = "**{title}**\n\n{body}\n\n[Открыть →]({link})"
|
|
proxy_url: str = ""
|
|
|
|
@field_validator("check_interval")
|
|
@classmethod
|
|
def _interval_min(cls, v: int) -> int:
|
|
return max(1, v)
|
|
|
|
@field_validator("alert_threshold")
|
|
@classmethod
|
|
def _threshold_min(cls, v: int) -> int:
|
|
return max(1, v)
|
|
|
|
@field_validator("default_priority")
|
|
@classmethod
|
|
def _def_priority_range(cls, v: int) -> int:
|
|
return min(5, max(1, v))
|
|
|
|
@field_validator("default_interval")
|
|
@classmethod
|
|
def _def_interval_nonneg(cls, v: int) -> int:
|
|
return max(0, v)
|
|
|
|
|
|
class TestIn(BaseModel):
|
|
server: str = ""
|
|
topic: str
|
|
# Optional auth from the form; falls back to saved default-server creds.
|
|
token: str | None = None
|
|
username: str | None = None
|
|
password: str | None = None
|
|
|
|
|
|
class PreviewIn(BaseModel):
|
|
url: str
|
|
filter_include: str = ""
|
|
filter_exclude: str = ""
|
|
|
|
@field_validator("url")
|
|
@classmethod
|
|
def _url_required(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not v.startswith(("http://", "https://")):
|
|
raise ValueError("URL должен начинаться с http:// или https://")
|
|
return v
|
|
|
|
|
|
class CategoryIn(BaseModel):
|
|
name: str
|
|
sort_order: int = 0
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def _name_required(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not v:
|
|
raise ValueError("Название категории обязательно")
|
|
return v
|
|
|
|
|
|
class UserIn(BaseModel):
|
|
username: str
|
|
password: str = "" # empty on edit = keep existing
|
|
role: str = "admin"
|
|
|
|
@field_validator("username")
|
|
@classmethod
|
|
def _username_required(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not v:
|
|
raise ValueError("Логин обязателен")
|
|
return v
|
|
|
|
@field_validator("role")
|
|
@classmethod
|
|
def _role_valid(cls, v: str) -> str:
|
|
return v if v in ("admin", "viewer") else "viewer"
|