"""Реестр настраиваемых через UI параметров Synapse.
.env задаёт дефолты, admin UI перебивает их оверрайдами в таблице
app_settings (app/settings_store.py). Значения секретов хранятся в БД
write-only: никогда не возвращаются через API. Каналы Telegram/SMTP пока
заглушки (#29) — конфиги сохраняем заранее.
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class SettingDef:
key: str
section: str # routing | push | telegram | smtp
label: str
kind: str # "str" | "int" | "bool" | "secret"
options: tuple[str, ...] = () # для enum — допустимые значения
help: str = ""
EDITABLE: dict[str, SettingDef] = {d.key: d for d in (
SettingDef(
"routing_match_mode", "routing", "Режим маршрутизации", "str",
options=("all", "first"),
help="all — все подошедшие правила; first — только первое по weight",
),
SettingDef(
"dedup_window_seconds", "routing", "Окно дедупликации, сек", "int",
help="Дубль с тем же dedup_key внутри окна вернёт id первого события",
),
SettingDef(
"retention_days", "routing", "Ретеншн, дней", "int",
help="События старше N дней удаляются; 0 — отключить окно",
),
SettingDef(
"vapid_public_key", "push", "VAPID public key", "str",
help="URLsafe base64 публичный ключ (p256dh)",
),
SettingDef(
"vapid_private_key", "push", "VAPID private key", "secret",
help="Секрет хранится в БД write-only, в API возвращается только факт «задано»",
),
SettingDef(
"push_subject", "push", "Subject (отправитель)", "str",
help="Контакты отправителя для веб-пушей, например mailto:ops@gnexus.space",
),
SettingDef(
"telegram_bot_token", "telegram", "Токен тг-бота", "secret",
help="Канал ещё не реализован (#29) — токен сохраняется впрок",
),
SettingDef("smtp_host", "smtp", "SMTP host", "str"),
SettingDef("smtp_port", "smtp", "SMTP port", "int"),
SettingDef("smtp_user", "smtp", "SMTP user", "str"),
SettingDef("smtp_password", "smtp", "SMTP password", "secret"),
SettingDef(
"smtp_from", "smtp", "From-адрес", "str",
help='Например "Synapse <synapse@gnexus.space>"',
),
)}
SECRET_KINDS = ("secret",)
INT_RANGES = {
"dedup_window_seconds": (1, 10**6),
"retention_days": (0, 366),
"smtp_port": (1, 65535),
}
def is_secret(defn: SettingDef) -> bool:
return defn.kind in SECRET_KINDS
def validate(defn: SettingDef, raw: str) -> str | None:
"""Валидация значения из UI; None — ок, иначе текст ошибки."""
if defn.kind == "int":
# диапазоны все неотрицательные — минус не нужен; "" сразу отвергаем
if not raw.isdigit():
return f"{defn.key}: значение должно быть числом"
low, high = INT_RANGES.get(defn.key, (0, 10**9))
if not low <= int(raw) <= high:
return f"{defn.key}: значение вне допустимого диапазона {low}..{high}"
if defn.kind == "bool" and raw not in ("true", "false"):
return f"{defn.key}: значение должно быть true/false"
if defn.options and raw not in defn.options:
return f"{defn.key}: допустимо {' или '.join(defn.options)}"
return None