refactor: provider-agnostic bot command system + Gitea commands
Refactored the monolithic command handler (707 lines) into a pluggable provider-handler architecture: - Abstract ProviderCommandHandler interface (base.py) - Handler dispatch registry routes commands by provider type - Extracted all Immich logic into ImmichCommandHandler - New GiteaCommandHandler with /status, /repos, /issues, /prs, /commits - Multi-provider routing: groups context by provider type, finds handler - handler.py reduced to ~280 line thin orchestrator Gitea commands: - Extended GiteaClient with get_repo_issues, get_repo_pulls, get_repo_commits - 30 Jinja2 command templates (15 EN + 15 RU) - Gitea capabilities updated with 6 commands + 15 command_slots - Default command config + command template config seeded on startup - Rate limiting: Gitea API commands share "api" category (15s cooldown) Also: - Command configs API accepts "gitea" provider type - System command configs (user_id=0) visible to all users - Webhook URL shown on Gitea provider card and edit form - Scan interval hidden for webhook-based providers
This commit is contained in:
@@ -53,6 +53,7 @@ async def lifespan(app: FastAPI):
|
||||
await _seed_default_templates()
|
||||
await _seed_default_command_templates()
|
||||
await _seed_default_tracking_configs()
|
||||
await _seed_default_command_configs()
|
||||
# Configure webhook secret from DB setting (falls back to env var)
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession as _AS
|
||||
from .api.app_settings import get_setting as _get_setting
|
||||
@@ -393,7 +394,7 @@ async def _seed_default_command_templates():
|
||||
|
||||
# Upsert slots for each locale
|
||||
for locale in ("en", "ru"):
|
||||
slots = load_default_command_templates(locale)
|
||||
slots = load_default_command_templates(locale, provider_type="immich")
|
||||
if not slots:
|
||||
continue
|
||||
for slot_name, template_text in slots.items():
|
||||
@@ -416,6 +417,51 @@ async def _seed_default_command_templates():
|
||||
template=template_text,
|
||||
))
|
||||
|
||||
# --- Seed Gitea default command templates ---
|
||||
gitea_cmd_result = await session.exec(
|
||||
select(CommandTemplateConfig).where(
|
||||
CommandTemplateConfig.user_id == 0,
|
||||
CommandTemplateConfig.provider_type == "gitea",
|
||||
)
|
||||
)
|
||||
gitea_cmd_configs = gitea_cmd_result.all()
|
||||
|
||||
if not gitea_cmd_configs:
|
||||
gitea_cmd_config = CommandTemplateConfig(
|
||||
user_id=0,
|
||||
provider_type="gitea",
|
||||
name="Default Gitea Commands",
|
||||
description="Default Gitea command templates",
|
||||
)
|
||||
session.add(gitea_cmd_config)
|
||||
await session.flush()
|
||||
else:
|
||||
gitea_cmd_config = gitea_cmd_configs[0]
|
||||
|
||||
for locale in ("en", "ru"):
|
||||
gitea_cmd_slots = load_default_command_templates(locale, provider_type="gitea")
|
||||
if not gitea_cmd_slots:
|
||||
continue
|
||||
for slot_name, template_text in gitea_cmd_slots.items():
|
||||
slot_result = await session.exec(
|
||||
select(CommandTemplateSlot).where(
|
||||
CommandTemplateSlot.config_id == gitea_cmd_config.id,
|
||||
CommandTemplateSlot.slot_name == slot_name,
|
||||
CommandTemplateSlot.locale == locale,
|
||||
)
|
||||
)
|
||||
existing = slot_result.first()
|
||||
if existing:
|
||||
existing.template = template_text
|
||||
session.add(existing)
|
||||
else:
|
||||
session.add(CommandTemplateSlot(
|
||||
config_id=gitea_cmd_config.id,
|
||||
slot_name=slot_name,
|
||||
locale=locale,
|
||||
template=template_text,
|
||||
))
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -464,6 +510,85 @@ async def _seed_default_tracking_configs():
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _seed_default_command_configs():
|
||||
"""Seed system-owned default command configs for each provider type."""
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from .database.engine import get_engine
|
||||
from .database.models import CommandConfig, CommandTemplateConfig
|
||||
|
||||
engine = get_engine()
|
||||
async with AsyncSession(engine) as session:
|
||||
# Find existing system-owned command configs
|
||||
result = await session.exec(
|
||||
select(CommandConfig).where(CommandConfig.user_id == 0)
|
||||
)
|
||||
existing = {c.provider_type: c for c in result.all()}
|
||||
|
||||
# Find system command template configs to link
|
||||
tmpl_result = await session.exec(
|
||||
select(CommandTemplateConfig).where(CommandTemplateConfig.user_id == 0)
|
||||
)
|
||||
tmpl_by_type = {t.provider_type: t.id for t in tmpl_result.all()}
|
||||
|
||||
defaults = [
|
||||
{
|
||||
"provider_type": "immich",
|
||||
"name": "Default Immich",
|
||||
"enabled_commands": [
|
||||
"help", "status", "albums", "events", "latest",
|
||||
"random", "favorites", "summary", "memory",
|
||||
],
|
||||
"response_mode": "media",
|
||||
"default_count": 5,
|
||||
"rate_limits": {"search": 30, "default": 10},
|
||||
},
|
||||
{
|
||||
"provider_type": "gitea",
|
||||
"name": "Default Gitea",
|
||||
"enabled_commands": [
|
||||
"help", "status", "repos", "issues", "prs", "commits",
|
||||
],
|
||||
"response_mode": "text",
|
||||
"default_count": 10,
|
||||
"rate_limits": {"api": 15, "default": 10},
|
||||
},
|
||||
]
|
||||
|
||||
for cfg in defaults:
|
||||
ptype = cfg["provider_type"]
|
||||
if ptype in existing:
|
||||
continue
|
||||
cmd_tmpl_id = tmpl_by_type.get(ptype)
|
||||
# Use raw SQL to handle legacy NOT NULL columns
|
||||
import json as _json2
|
||||
from sqlalchemy import text as _text2
|
||||
from datetime import datetime as _dt3, timezone as _tz3
|
||||
await session.execute(
|
||||
_text2(
|
||||
"INSERT INTO command_config "
|
||||
"(user_id, provider_type, name, icon, enabled_commands, locale, "
|
||||
"response_mode, default_count, rate_limits, command_template_config_id, created_at) "
|
||||
"VALUES (:uid, :pt, :name, :icon, :cmds, :locale, :rm, :dc, :rl, :ctid, :ca)"
|
||||
),
|
||||
{
|
||||
"uid": 0,
|
||||
"pt": ptype,
|
||||
"name": cfg["name"],
|
||||
"icon": "",
|
||||
"cmds": _json2.dumps(cfg["enabled_commands"]),
|
||||
"locale": "en",
|
||||
"rm": cfg["response_mode"],
|
||||
"dc": cfg["default_count"],
|
||||
"rl": _json2.dumps(cfg["rate_limits"]),
|
||||
"ctid": cmd_tmpl_id,
|
||||
"ca": _dt3.now(_tz3.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
def run():
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8420)
|
||||
|
||||
Reference in New Issue
Block a user