"""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="") language_code: 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) # Gitea event tracking track_push: bool = Field(default=True) track_issue_opened: bool = Field(default=True) track_issue_closed: bool = Field(default=True) track_issue_commented: bool = Field(default=False) track_pr_opened: bool = Field(default=True) track_pr_closed: bool = Field(default=True) track_pr_merged: bool = Field(default=True) track_pr_commented: bool = Field(default=False) track_release_published: bool = Field(default=True) # Planka event tracking track_card_created: bool = Field(default=True) track_card_updated: bool = Field(default=False) track_card_moved: bool = Field(default=True) track_card_deleted: bool = Field(default=False) track_card_commented: bool = Field(default=True) track_comment_updated: bool = Field(default=False) track_board_created: bool = Field(default=True) track_board_updated: bool = Field(default=False) track_board_deleted: bool = Field(default=True) track_list_created: bool = Field(default=False) track_list_updated: bool = Field(default=False) track_list_deleted: bool = Field(default=False) track_attachment_created: bool = Field(default=True) track_card_label_added: bool = Field(default=False) track_task_completed: bool = Field(default=True) # Scheduler event tracking track_scheduled_message: bool = Field(default=True) # Immich asset display 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)) filters: dict[str, Any] = Field(default_factory=dict, 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 Action(SQLModel, table=True): """A scheduled action that mutates an external service.""" __tablename__ = "action" 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="") action_type: str # e.g. "auto_organize" config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) schedule_type: str = Field(default="interval") # "interval" or "cron" schedule_interval: int = Field(default=3600) # seconds schedule_cron: str = Field(default="") enabled: bool = Field(default=False) # default disabled for safety last_run_at: datetime | None = Field(default=None) last_run_status: str = Field(default="") # "success", "partial", "failed", "" created_at: datetime = Field(default_factory=_utcnow) class ActionRule(SQLModel, table=True): """One rule within an Action. Executed in order.""" __tablename__ = "action_rule" id: int | None = Field(default=None, primary_key=True) action_id: int = Field(foreign_key="action.id", index=True) name: str = Field(default="") rule_config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) enabled: bool = Field(default=True) order: int = Field(default=0) created_at: datetime = Field(default_factory=_utcnow) class ActionExecution(SQLModel, table=True): """Log of an action execution (scheduled, manual, or dry-run).""" __tablename__ = "action_execution" id: int | None = Field(default=None, primary_key=True) action_id: int = Field(foreign_key="action.id", index=True) started_at: datetime = Field(default_factory=_utcnow) finished_at: datetime | None = Field(default=None) status: str = Field(default="running") # "running", "success", "partial", "failed" rules_processed: int = Field(default=0) rules_succeeded: int = Field(default=0) rules_failed: int = Field(default=0) total_items_affected: int = Field(default=0) summary: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) error: str = Field(default="") trigger: str = Field(default="scheduled") # "scheduled", "manual", "dry_run" 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="")