feat: provider-strict configs, slot-based templates, broadcast targets, email bots, command templates
Major architectural improvements: - Provider-type enforcement: configs validated against provider type at assignment - TemplateConfig migrated to slot-based pattern (TemplateSlot child table) - Broadcast targets: TargetReceiver child table for multi-receiver dispatch - EmailBot: first-class email sender entity with SMTP config, test connection - CommandTemplateConfig: generic slot-based command response templates - Provider capability registry: dynamic slot/event/command definitions per provider - CommandTracker play/pause button matches NotificationTracker style
This commit is contained in:
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy import UniqueConstraint, Text
|
||||
from sqlmodel import JSON, Column, Field, SQLModel
|
||||
|
||||
|
||||
@@ -53,6 +53,24 @@ class TelegramBot(SQLModel, table=True):
|
||||
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"
|
||||
|
||||
@@ -124,7 +142,10 @@ class TrackingConfig(SQLModel, table=True):
|
||||
|
||||
|
||||
class TemplateConfig(SQLModel, table=True):
|
||||
"""Jinja2 message templates. Tied to a provider type."""
|
||||
"""Jinja2 message templates. Tied to a provider type.
|
||||
|
||||
Template content is stored in TemplateSlot child rows (one per slot).
|
||||
"""
|
||||
|
||||
__tablename__ = "template_config"
|
||||
|
||||
@@ -135,32 +156,41 @@ class TemplateConfig(SQLModel, table=True):
|
||||
description: str = Field(default="")
|
||||
icon: str = Field(default="")
|
||||
|
||||
# Event-driven notification templates
|
||||
message_assets_added: str = Field(default="")
|
||||
message_assets_removed: str = Field(default="")
|
||||
message_collection_renamed: str = Field(default="")
|
||||
message_collection_deleted: str = Field(default="")
|
||||
message_sharing_changed: str = Field(default="")
|
||||
|
||||
# Scheduled notification templates
|
||||
periodic_summary_message: str = Field(default="")
|
||||
scheduled_assets_message: str = Field(default="")
|
||||
memory_mode_message: str = Field(default="")
|
||||
|
||||
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."""
|
||||
"""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" or "webhook"
|
||||
type: str # "telegram", "webhook", or "email"
|
||||
name: str
|
||||
icon: str = Field(default="")
|
||||
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
@@ -168,6 +198,28 @@ class NotificationTarget(SQLModel, table=True):
|
||||
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."""
|
||||
|
||||
@@ -246,9 +298,43 @@ class CommandConfig(SQLModel, table=True):
|
||||
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="")
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class CommandTemplateSlot(SQLModel, table=True):
|
||||
"""One Jinja2 template for a specific command response slot.
|
||||
|
||||
Slot names match command names (e.g. 'status', 'help', 'albums').
|
||||
"""
|
||||
|
||||
__tablename__ = "command_template_slot"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("config_id", "slot_name", name="uq_command_template_slot"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
config_id: int = Field(foreign_key="command_template_config.id", index=True)
|
||||
slot_name: str
|
||||
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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user