Files
notify-bridge/packages/server/src/notify_bridge_server/database/models.py
T
alexei.dolgolyov 1167d138a3 feat: locale-aware command templates, debounced auto-sync, entity pickers
- Locale-aware templates: CommandTemplateSlot now has a locale column,
  allowing each slot to have per-language variants (EN/RU). Templates
  are resolved at runtime from the Telegram user's language_code.

- Merged system configs: "Default Commands (EN)" and "(RU)" merged
  into a single "Default Commands" config with locale-aware slots.
  Migration handles existing data automatically.

- Configurable command descriptions: hardcoded COMMAND_DESCRIPTIONS
  replaced with desc_* template slots (desc_status, desc_help, etc.)
  that users can customize per locale. setMyCommands registers all
  locales explicitly.

- Removed locale from CommandConfig: no longer needed since locale
  is derived from the Telegram user's language at runtime.

- Debounced command auto-sync: after command config/tracker changes,
  affected bots are marked dirty and synced after a 30s debounce
  window. Manual "Sync with Telegram" button still works.

- Entity pickers in LinkedTargetsSection: replaced 6 plain <select>
  elements with EntitySelect components (search, icons, keyboard nav).
  Added onselect callback and size="sm" props to EntitySelect.
2026-03-22 03:14:51 +03:00

421 lines
15 KiB
Python

"""SQLModel database table definitions for Notify Bridge."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from sqlalchemy import UniqueConstraint, Text
from sqlmodel import JSON, Column, Field, SQLModel
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(index=True, unique=True)
hashed_password: str
role: str = Field(default="user")
created_at: datetime = Field(default_factory=_utcnow)
class ServiceProvider(SQLModel, table=True):
"""A service provider instance (e.g., an Immich server)."""
__tablename__ = "service_provider"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
type: str # ServiceProviderType value ("immich")
name: str
icon: str = Field(default="")
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
created_at: datetime = Field(default_factory=_utcnow)
class TelegramBot(SQLModel, table=True):
__tablename__ = "telegram_bot"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
name: str
token: str
icon: str = Field(default="")
bot_username: str = Field(default="")
bot_id: int = Field(default=0)
webhook_path_id: str = Field(default_factory=lambda: uuid4().hex)
update_mode: str = Field(default="polling") # "polling" or "webhook"
# NOTE: commands_config column remains in the DB for backward compat,
# but is no longer part of the SQLModel class. Data migrated to CommandConfig.
created_at: datetime = Field(default_factory=_utcnow)
class MatrixBot(SQLModel, table=True):
"""Matrix bot — homeserver connection for sending messages to rooms."""
__tablename__ = "matrix_bot"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
name: str
icon: str = Field(default="")
homeserver_url: str # e.g. https://matrix.org
access_token: str
display_name: str = Field(default="")
created_at: datetime = Field(default_factory=_utcnow)
class EmailBot(SQLModel, table=True):
"""Email sender — SMTP connection for sending email notifications."""
__tablename__ = "email_bot"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
name: str
icon: str = Field(default="")
email: str # From address
smtp_host: str
smtp_port: int = Field(default=587)
smtp_username: str = Field(default="")
smtp_password: str = Field(default="")
smtp_use_tls: bool = Field(default=True)
created_at: datetime = Field(default_factory=_utcnow)
class TelegramChat(SQLModel, table=True):
__tablename__ = "telegram_chat"
id: int | None = Field(default=None, primary_key=True)
bot_id: int = Field(foreign_key="telegram_bot.id")
chat_id: str
title: str = Field(default="")
chat_type: str = Field(default="private")
username: str = Field(default="")
discovered_at: datetime = Field(default_factory=_utcnow)
class TrackingConfig(SQLModel, table=True):
"""What events to track + scheduling rules. Tied to a provider type."""
__tablename__ = "tracking_config"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
provider_type: str # Must match provider's type
name: str
icon: str = Field(default="")
# Event-driven tracking
track_assets_added: bool = Field(default=True)
track_assets_removed: bool = Field(default=False)
track_collection_renamed: bool = Field(default=True)
track_collection_deleted: bool = Field(default=True)
track_sharing_changed: bool = Field(default=False)
track_images: bool = Field(default=True)
track_videos: bool = Field(default=True)
notify_favorites_only: bool = Field(default=False)
# Asset display
include_tags: bool = Field(default=True)
include_asset_details: bool = Field(default=False)
max_assets_to_show: int = Field(default=5)
assets_order_by: str = Field(default="none")
assets_order: str = Field(default="descending")
# Periodic summary
periodic_enabled: bool = Field(default=False)
periodic_interval_days: int = Field(default=1)
periodic_start_date: str = Field(default="2025-01-01")
periodic_times: str = Field(default="12:00")
# Scheduled assets
scheduled_enabled: bool = Field(default=False)
scheduled_times: str = Field(default="09:00")
scheduled_collection_mode: str = Field(default="per_collection")
scheduled_limit: int = Field(default=10)
scheduled_favorite_only: bool = Field(default=False)
scheduled_asset_type: str = Field(default="all")
scheduled_min_rating: int = Field(default=0)
scheduled_order_by: str = Field(default="random")
scheduled_order: str = Field(default="descending")
# Memory mode
memory_enabled: bool = Field(default=False)
memory_source: str = Field(default="albums") # "albums" or "native"
memory_times: str = Field(default="09:00")
memory_collection_mode: str = Field(default="combined")
memory_limit: int = Field(default=10)
memory_favorite_only: bool = Field(default=False)
memory_asset_type: str = Field(default="all")
memory_min_rating: int = Field(default=0)
created_at: datetime = Field(default_factory=_utcnow)
class TemplateConfig(SQLModel, table=True):
"""Jinja2 message templates. Tied to a provider type.
Template content is stored in TemplateSlot child rows (one per slot).
"""
__tablename__ = "template_config"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(default=0) # 0 = system-owned (no FK to allow sentinel)
provider_type: str # Must match provider's type
name: str
description: str = Field(default="")
icon: str = Field(default="")
locale: str = Field(default="") # e.g. "en", "ru"; empty = unspecified
date_format: str = Field(default="%d.%m.%Y, %H:%M UTC")
date_only_format: str = Field(default="%d.%m.%Y")
created_at: datetime = Field(default_factory=_utcnow)
class TemplateSlot(SQLModel, table=True):
"""One Jinja2 template for a specific slot within a TemplateConfig.
Slot names are provider-specific (e.g. 'message_assets_added' for Immich).
"""
__tablename__ = "template_slot"
__table_args__ = (
UniqueConstraint("config_id", "slot_name", name="uq_template_slot"),
)
id: int | None = Field(default=None, primary_key=True)
config_id: int = Field(foreign_key="template_config.id", index=True)
slot_name: str
template: str = Field(default="", sa_column=Column(Text, default=""))
class NotificationTarget(SQLModel, table=True):
"""Where to send notifications. Pure delivery endpoint.
Target-level config holds connection/display settings (e.g. bot_token,
disable_url_preview). Actual delivery endpoints live in TargetReceiver rows.
"""
__tablename__ = "notification_target"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
type: str # "telegram", "webhook", "email", "discord", "slack", "ntfy", "matrix"
name: str
icon: str = Field(default="")
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
chat_action: str | None = Field(default="typing") # e.g. "typing", "upload_photo"
created_at: datetime = Field(default_factory=_utcnow)
class TargetReceiver(SQLModel, table=True):
"""One delivery endpoint within a NotificationTarget (broadcast support).
For Telegram: config = {"chat_id": "12345"}
For Webhook: config = {"url": "https://...", "headers": {...}}
For Email: config = {"email": "user@example.com", "name": "..."}
"""
__tablename__ = "target_receiver"
__table_args__ = (
UniqueConstraint("target_id", "receiver_key", name="uq_target_receiver"),
)
id: int | None = Field(default=None, primary_key=True)
target_id: int = Field(foreign_key="notification_target.id", index=True)
name: str = Field(default="")
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
receiver_key: str = Field(default="") # dedup key (e.g. chat_id, url, email)
enabled: bool = Field(default=True)
created_at: datetime = Field(default_factory=_utcnow)
class NotificationTracker(SQLModel, table=True):
"""Watches a provider's collections for changes."""
__tablename__ = "notification_tracker"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
provider_id: int = Field(foreign_key="service_provider.id")
name: str
icon: str = Field(default="")
collection_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
scan_interval: int = Field(default=60)
batch_duration: int = Field(default=0) # seconds to accumulate events before dispatch (0=immediate)
enabled: bool = Field(default=True)
created_at: datetime = Field(default_factory=_utcnow)
class NotificationTrackerTarget(SQLModel, table=True):
"""Junction between NotificationTracker and NotificationTarget with per-link config."""
__tablename__ = "notification_tracker_target"
id: int | None = Field(default=None, primary_key=True)
# Python attr stays as tracker_id for backward compat; DB column is notification_tracker_id
tracker_id: int = Field(
foreign_key="notification_tracker.id",
index=True,
sa_column_kwargs={"name": "notification_tracker_id"},
)
target_id: int = Field(foreign_key="notification_target.id", index=True)
tracking_config_id: int | None = Field(
default=None, foreign_key="tracking_config.id"
)
template_config_id: int | None = Field(
default=None, foreign_key="template_config.id"
)
enabled: bool = Field(default=True)
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
# NOTE: commands_config column remains in the DB for backward compat,
# but is no longer part of the SQLModel class. Data migrated to CommandConfig.
created_at: datetime = Field(default_factory=_utcnow)
class NotificationTrackerState(SQLModel, table=True):
"""Persisted state for change detection."""
__tablename__ = "notification_tracker_state"
id: int | None = Field(default=None, primary_key=True)
# Python attr stays as tracker_id for backward compat; DB column is notification_tracker_id
tracker_id: int = Field(
foreign_key="notification_tracker.id",
sa_column_kwargs={"name": "notification_tracker_id"},
)
collection_id: str
collection_name: str = Field(default="")
shared: bool = Field(default=False)
asset_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
pending_asset_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
last_updated: datetime = Field(default_factory=_utcnow)
class CommandConfig(SQLModel, table=True):
"""Configuration for bot commands (e.g., which commands are enabled, rate limits)."""
__tablename__ = "command_config"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
provider_type: str
name: str
icon: str = Field(default="")
enabled_commands: list[str] = Field(default_factory=list, sa_column=Column(JSON))
response_mode: str = Field(default="media") # "media" or "text"
default_count: int = Field(default=5)
rate_limits: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
command_template_config_id: int | None = Field(
default=None, foreign_key="command_template_config.id"
)
created_at: datetime = Field(default_factory=_utcnow)
class CommandTemplateConfig(SQLModel, table=True):
"""Jinja2 templates for command responses. Provider-specific via slots."""
__tablename__ = "command_template_config"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(default=0) # 0 = system-owned
provider_type: str
name: str
description: str = Field(default="")
icon: str = Field(default="")
locale: str = Field(default="") # e.g. "en", "ru"; empty = unspecified
created_at: datetime = Field(default_factory=_utcnow)
class CommandTemplateSlot(SQLModel, table=True):
"""One Jinja2 template for a specific command response slot and locale.
Slot names match command names (e.g. 'status', 'help', 'albums').
Description slots use 'desc_' prefix (e.g. 'desc_status', 'desc_help').
Each (config, slot, locale) triple holds a separate template.
"""
__tablename__ = "command_template_slot"
__table_args__ = (
UniqueConstraint("config_id", "slot_name", "locale", name="uq_cmd_slot_locale"),
)
id: int | None = Field(default=None, primary_key=True)
config_id: int = Field(foreign_key="command_template_config.id", index=True)
slot_name: str
locale: str = Field(default="en")
template: str = Field(default="", sa_column=Column(Text, default=""))
class CommandTracker(SQLModel, table=True):
"""Links a provider to a command config for interactive bot commands."""
__tablename__ = "command_tracker"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
provider_id: int = Field(foreign_key="service_provider.id")
command_config_id: int = Field(foreign_key="command_config.id")
name: str
icon: str = Field(default="")
enabled: bool = Field(default=True)
created_at: datetime = Field(default_factory=_utcnow)
class CommandTrackerListener(SQLModel, table=True):
"""Links a CommandTracker to a listener (e.g., a telegram bot chat)."""
__tablename__ = "command_tracker_listener"
__table_args__ = (
UniqueConstraint(
"command_tracker_id", "listener_type", "listener_id",
name="uq_command_tracker_listener",
),
)
id: int | None = Field(default=None, primary_key=True)
command_tracker_id: int = Field(foreign_key="command_tracker.id")
listener_type: str # e.g. "telegram_bot"
listener_id: int
created_at: datetime = Field(default_factory=_utcnow)
class EventLog(SQLModel, table=True):
"""Log of detected events."""
__tablename__ = "event_log"
id: int | None = Field(default=None, primary_key=True)
# Python attr stays as tracker_id for backward compat; DB column is notification_tracker_id
tracker_id: int | None = Field(
default=None,
foreign_key="notification_tracker.id",
index=True,
sa_column_kwargs={"name": "notification_tracker_id"},
)
tracker_name: str = Field(default="")
provider_id: int | None = Field(default=None, index=True)
provider_name: str = Field(default="")
event_type: str = Field(index=True)
collection_id: str
collection_name: str
assets_count: int = Field(default=0)
details: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
created_at: datetime = Field(default_factory=_utcnow)
class AppSetting(SQLModel, table=True):
"""Key-value app-level settings (admin-configurable)."""
__tablename__ = "app_setting"
key: str = Field(primary_key=True)
value: str = Field(default="")