feat: Discord/Slack/ntfy/Matrix targets, command templates, delete protection, email/matrix bots

- Discord, Slack, ntfy, Matrix notification target types with clients and dispatch
- MatrixBot model + API + frontend in Bots tab
- Command template system fully wired into all handler commands
- Default command templates seeded (EN/RU, 14 slots each)
- Command template editor with variables reference including child fields
- Delete protection on all 10 entity types (409 with consumer details)
- Provider type selector on template config forms
- Target type selector as dropdown with all 7 types
- Response template selector on command config form
- CLAUDE.md: mandatory server restart rule, child properties rule
This commit is contained in:
2026-03-21 20:36:12 +03:00
parent 846d480d38
commit 3e3a6f0777
64 changed files with 1861 additions and 180 deletions
@@ -23,6 +23,7 @@ from .api.targets import router as targets_router
from .api.target_receivers import router as target_receivers_router
from .api.telegram_bots import router as telegram_bots_router
from .api.email_bots import router as email_bots_router
from .api.matrix_bots import router as matrix_bots_router
from .api.users import router as users_router
from .api.status import router as status_router
from .api.template_vars import router as template_vars_router
@@ -46,6 +47,7 @@ async def lifespan(app: FastAPI):
await migrate_template_slots(engine)
await migrate_target_receivers(engine)
await _seed_default_templates()
await _seed_default_command_templates()
# 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
@@ -71,6 +73,7 @@ app.include_router(targets_router)
app.include_router(target_receivers_router)
app.include_router(telegram_bots_router)
app.include_router(email_bots_router)
app.include_router(matrix_bots_router)
app.include_router(users_router)
app.include_router(status_router)
app.include_router(app_settings_router)
@@ -155,6 +158,72 @@ async def _seed_default_templates():
await session.commit()
async def _seed_default_command_templates():
"""Seed or update default command response templates on startup."""
from sqlmodel import func, select
from sqlmodel.ext.asyncio.session import AsyncSession
from .database.engine import get_engine
from .database.models import CommandTemplateConfig, CommandTemplateSlot
from notify_bridge_core.templates.command_defaults import load_default_command_templates
engine = get_engine()
async with AsyncSession(engine) as session:
result = await session.exec(select(func.count()).select_from(CommandTemplateConfig))
count = result.one()
if count == 0:
# First startup — seed EN and RU defaults
for locale in ("en", "ru"):
slots = load_default_command_templates(locale)
if not slots:
continue
name = f"Default Commands ({locale.upper()})"
config = CommandTemplateConfig(
user_id=0,
provider_type="immich",
name=name,
description=f"Default Immich command templates ({locale.upper()})",
)
session.add(config)
await session.flush()
for slot_name, template_text in slots.items():
session.add(CommandTemplateSlot(
config_id=config.id,
slot_name=slot_name,
template=template_text,
))
else:
# Update existing system-owned command templates from files
result = await session.exec(
select(CommandTemplateConfig).where(CommandTemplateConfig.user_id == 0)
)
system_configs = result.all()
for config in system_configs:
locale = "ru" if "(RU)" in config.name else "en"
slots = load_default_command_templates(locale)
if not slots:
continue
for slot_name, template_text in slots.items():
slot_result = await session.exec(
select(CommandTemplateSlot).where(
CommandTemplateSlot.config_id == config.id,
CommandTemplateSlot.slot_name == slot_name,
)
)
existing = slot_result.first()
if existing:
existing.template = template_text
session.add(existing)
else:
session.add(CommandTemplateSlot(
config_id=config.id,
slot_name=slot_name,
template=template_text,
))
await session.commit()
def run():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8420)