feat: entity relationship refactor — notification trackers, command system, chat actions
Rework entity schema: rename Tracker→NotificationTracker, add CommandConfig/ CommandTracker/CommandTrackerListener entities for decoupled command handling. Commands now resolve through CommandTracker→CommandConfig instead of TelegramBot.commands_config. Smart ref-counted bot polling based on active listeners. Add chat_action to telegram targets. Full frontend CRUD pages for command configs and command trackers. Idempotent SQLite migrations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlmodel import JSON, Column, Field, SQLModel
|
||||
|
||||
|
||||
@@ -47,7 +48,8 @@ class TelegramBot(SQLModel, table=True):
|
||||
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"
|
||||
commands_config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -162,13 +164,14 @@ class NotificationTarget(SQLModel, table=True):
|
||||
name: str
|
||||
icon: str = Field(default="")
|
||||
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
chat_action: str | None = Field(default=None) # e.g. "typing", "upload_photo"
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class Tracker(SQLModel, table=True):
|
||||
class NotificationTracker(SQLModel, table=True):
|
||||
"""Watches a provider's collections for changes."""
|
||||
|
||||
__tablename__ = "tracker"
|
||||
__tablename__ = "notification_tracker"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
@@ -182,13 +185,18 @@ class Tracker(SQLModel, table=True):
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class TrackerTarget(SQLModel, table=True):
|
||||
"""Junction between Tracker and NotificationTarget with per-link config."""
|
||||
class NotificationTrackerTarget(SQLModel, table=True):
|
||||
"""Junction between NotificationTracker and NotificationTarget with per-link config."""
|
||||
|
||||
__tablename__ = "tracker_target"
|
||||
__tablename__ = "notification_tracker_target"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
tracker_id: int = Field(foreign_key="tracker.id", index=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"
|
||||
@@ -199,19 +207,22 @@ class TrackerTarget(SQLModel, table=True):
|
||||
enabled: bool = Field(default=True)
|
||||
quiet_hours_start: str | None = None
|
||||
quiet_hours_end: str | None = None
|
||||
commands_config: dict[str, Any] | None = Field(
|
||||
default=None, sa_column=Column(JSON)
|
||||
)
|
||||
# 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 TrackerState(SQLModel, table=True):
|
||||
class NotificationTrackerState(SQLModel, table=True):
|
||||
"""Persisted state for change detection."""
|
||||
|
||||
__tablename__ = "tracker_state"
|
||||
__tablename__ = "notification_tracker_state"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
tracker_id: int = Field(foreign_key="tracker.id")
|
||||
# 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)
|
||||
@@ -220,13 +231,70 @@ class TrackerState(SQLModel, table=True):
|
||||
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))
|
||||
locale: str = Field(default="en")
|
||||
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))
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
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)
|
||||
tracker_id: int | None = Field(default=None, foreign_key="tracker.id", index=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="")
|
||||
|
||||
Reference in New Issue
Block a user