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:
@@ -0,0 +1,151 @@
|
||||
"""Command config management API routes."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import CommandConfig, CommandTracker, User
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/command-configs", tags=["command-configs"])
|
||||
|
||||
|
||||
class CommandConfigCreate(BaseModel):
|
||||
provider_type: str
|
||||
name: str
|
||||
icon: str = ""
|
||||
enabled_commands: list[str] = []
|
||||
locale: str = "en"
|
||||
response_mode: str = "media"
|
||||
default_count: int = 5
|
||||
rate_limits: dict[str, Any] = {}
|
||||
|
||||
|
||||
class CommandConfigUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
icon: str | None = None
|
||||
enabled_commands: list[str] | None = None
|
||||
locale: str | None = None
|
||||
response_mode: str | None = None
|
||||
default_count: int | None = None
|
||||
rate_limits: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_command_configs(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""List all command configs for the current user."""
|
||||
result = await session.exec(
|
||||
select(CommandConfig).where(CommandConfig.user_id == user.id)
|
||||
)
|
||||
return [_config_response(c) for c in result.all()]
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_command_config(
|
||||
body: CommandConfigCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Create a new command config."""
|
||||
# Validate provider_type
|
||||
valid_types = ("immich",)
|
||||
if body.provider_type not in valid_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid provider_type. Must be one of: {', '.join(valid_types)}",
|
||||
)
|
||||
|
||||
config = CommandConfig(user_id=user.id, **body.model_dump())
|
||||
session.add(config)
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
return _config_response(config)
|
||||
|
||||
|
||||
@router.get("/{config_id}")
|
||||
async def get_command_config(
|
||||
config_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Get a single command config."""
|
||||
config = await _get_user_config(session, config_id, user.id)
|
||||
return _config_response(config)
|
||||
|
||||
|
||||
@router.put("/{config_id}")
|
||||
async def update_command_config(
|
||||
config_id: int,
|
||||
body: CommandConfigUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Update a command config."""
|
||||
config = await _get_user_config(session, config_id, user.id)
|
||||
for field, value in body.model_dump(exclude_unset=True).items():
|
||||
setattr(config, field, value)
|
||||
session.add(config)
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
return _config_response(config)
|
||||
|
||||
|
||||
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_command_config(
|
||||
config_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Delete a command config. Fails if in use by any command tracker."""
|
||||
config = await _get_user_config(session, config_id, user.id)
|
||||
|
||||
# Check if any command tracker references this config
|
||||
result = await session.exec(
|
||||
select(CommandTracker).where(CommandTracker.command_config_id == config_id)
|
||||
)
|
||||
if result.first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Cannot delete: command config is in use by a command tracker",
|
||||
)
|
||||
|
||||
await session.delete(config)
|
||||
await session.commit()
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
|
||||
def _config_response(c: CommandConfig) -> dict:
|
||||
return {
|
||||
"id": c.id,
|
||||
"user_id": c.user_id,
|
||||
"provider_type": c.provider_type,
|
||||
"name": c.name,
|
||||
"icon": c.icon,
|
||||
"enabled_commands": c.enabled_commands or [],
|
||||
"locale": c.locale,
|
||||
"response_mode": c.response_mode,
|
||||
"default_count": c.default_count,
|
||||
"rate_limits": c.rate_limits or {},
|
||||
"created_at": c.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def _get_user_config(
|
||||
session: AsyncSession, config_id: int, user_id: int
|
||||
) -> CommandConfig:
|
||||
config = await session.get(CommandConfig, config_id)
|
||||
if not config or config.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Command config not found")
|
||||
return config
|
||||
Reference in New Issue
Block a user