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:
@@ -0,0 +1,179 @@
|
||||
"""Delete protection — prevents deletion of entities that are in use.
|
||||
|
||||
Each check function returns a list of consumer descriptions. If non-empty,
|
||||
the entity cannot be deleted.
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from ..database.models import (
|
||||
CommandConfig,
|
||||
CommandTracker,
|
||||
CommandTrackerListener,
|
||||
NotificationTarget,
|
||||
NotificationTracker,
|
||||
NotificationTrackerTarget,
|
||||
TargetReceiver,
|
||||
TelegramChat,
|
||||
)
|
||||
|
||||
|
||||
def raise_if_used(consumers: list[str], entity_name: str) -> None:
|
||||
"""Raise 409 Conflict if the entity has consumers."""
|
||||
if consumers:
|
||||
detail = f"Cannot delete {entity_name}: used by {len(consumers)} consumer(s). " + "; ".join(consumers)
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)
|
||||
|
||||
|
||||
async def check_service_provider(session: AsyncSession, provider_id: int) -> list[str]:
|
||||
"""Check if a ServiceProvider is used by any trackers."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTracker).where(NotificationTracker.provider_id == provider_id)
|
||||
)
|
||||
for t in result.all():
|
||||
consumers.append(f"Notification Tracker: {t.name}")
|
||||
result = await session.exec(
|
||||
select(CommandTracker).where(CommandTracker.provider_id == provider_id)
|
||||
)
|
||||
for t in result.all():
|
||||
consumers.append(f"Command Tracker: {t.name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_telegram_bot(session: AsyncSession, bot_id: int) -> list[str]:
|
||||
"""Check if a TelegramBot is used by any targets or command listeners."""
|
||||
consumers = []
|
||||
# Check notification targets with this bot in config
|
||||
result = await session.exec(select(NotificationTarget))
|
||||
for t in result.all():
|
||||
if t.config.get("bot_id") == bot_id or t.config.get("bot_token"):
|
||||
# Need to verify it's actually this bot
|
||||
if t.config.get("bot_id") == bot_id:
|
||||
consumers.append(f"Target: {t.name}")
|
||||
# Check command tracker listeners
|
||||
result = await session.exec(
|
||||
select(CommandTrackerListener).where(
|
||||
CommandTrackerListener.listener_type == "telegram_bot",
|
||||
CommandTrackerListener.listener_id == bot_id,
|
||||
)
|
||||
)
|
||||
for listener in result.all():
|
||||
tracker = await session.get(CommandTracker, listener.command_tracker_id)
|
||||
name = tracker.name if tracker else f"#{listener.command_tracker_id}"
|
||||
consumers.append(f"Command Tracker Listener: {name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_email_bot(session: AsyncSession, bot_id: int) -> list[str]:
|
||||
"""Check if an EmailBot is used by any targets."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTarget).where(NotificationTarget.type == "email")
|
||||
)
|
||||
for t in result.all():
|
||||
if t.config.get("email_bot_id") == bot_id:
|
||||
consumers.append(f"Target: {t.name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_matrix_bot(session: AsyncSession, bot_id: int) -> list[str]:
|
||||
"""Check if a MatrixBot is used by any targets."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTarget).where(NotificationTarget.type == "matrix")
|
||||
)
|
||||
for t in result.all():
|
||||
if t.config.get("matrix_bot_id") == bot_id:
|
||||
consumers.append(f"Target: {t.name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_tracking_config(session: AsyncSession, config_id: int) -> list[str]:
|
||||
"""Check if a TrackingConfig is used by any tracker-target links."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.tracking_config_id == config_id
|
||||
)
|
||||
)
|
||||
for tt in result.all():
|
||||
tracker = await session.get(NotificationTracker, tt.tracker_id)
|
||||
target = await session.get(NotificationTarget, tt.target_id)
|
||||
tracker_name = tracker.name if tracker else f"#{tt.tracker_id}"
|
||||
target_name = target.name if target else f"#{tt.target_id}"
|
||||
consumers.append(f"Tracker Link: {tracker_name} → {target_name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_template_config(session: AsyncSession, config_id: int) -> list[str]:
|
||||
"""Check if a TemplateConfig is used by any tracker-target links."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.template_config_id == config_id
|
||||
)
|
||||
)
|
||||
for tt in result.all():
|
||||
tracker = await session.get(NotificationTracker, tt.tracker_id)
|
||||
target = await session.get(NotificationTarget, tt.target_id)
|
||||
tracker_name = tracker.name if tracker else f"#{tt.tracker_id}"
|
||||
target_name = target.name if target else f"#{tt.target_id}"
|
||||
consumers.append(f"Tracker Link: {tracker_name} → {target_name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_command_template_config(session: AsyncSession, config_id: int) -> list[str]:
|
||||
"""Check if a CommandTemplateConfig is used by any command configs."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(CommandConfig).where(
|
||||
CommandConfig.command_template_config_id == config_id
|
||||
)
|
||||
)
|
||||
for c in result.all():
|
||||
consumers.append(f"Command Config: {c.name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_command_config(session: AsyncSession, config_id: int) -> list[str]:
|
||||
"""Check if a CommandConfig is used by any command trackers."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(CommandTracker).where(CommandTracker.command_config_id == config_id)
|
||||
)
|
||||
for t in result.all():
|
||||
consumers.append(f"Command Tracker: {t.name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_notification_target(session: AsyncSession, target_id: int) -> list[str]:
|
||||
"""Check if a NotificationTarget is used by any tracker-target links."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.target_id == target_id
|
||||
)
|
||||
)
|
||||
for tt in result.all():
|
||||
tracker = await session.get(NotificationTracker, tt.tracker_id)
|
||||
name = tracker.name if tracker else f"#{tt.tracker_id}"
|
||||
consumers.append(f"Notification Tracker: {name}")
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_notification_tracker(session: AsyncSession, tracker_id: int) -> list[str]:
|
||||
"""Check if a NotificationTracker has any linked targets."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.tracker_id == tracker_id
|
||||
)
|
||||
)
|
||||
for tt in result.all():
|
||||
target = await session.get(NotificationTarget, tt.target_id)
|
||||
name = target.name if target else f"#{tt.target_id}"
|
||||
consumers.append(f"Linked Target: {name}")
|
||||
return consumers
|
||||
Reference in New Issue
Block a user