feat: provider-strict configs, slot-based templates, broadcast targets, email bots, command templates

Major architectural improvements:
- Provider-type enforcement: configs validated against provider type at assignment
- TemplateConfig migrated to slot-based pattern (TemplateSlot child table)
- Broadcast targets: TargetReceiver child table for multi-receiver dispatch
- EmailBot: first-class email sender entity with SMTP config, test connection
- CommandTemplateConfig: generic slot-based command response templates
- Provider capability registry: dynamic slot/event/command definitions per provider
- CommandTracker play/pause button matches NotificationTracker style
This commit is contained in:
2026-03-21 16:33:24 +03:00
parent 371ea70756
commit 846d480d38
27 changed files with 2355 additions and 205 deletions
@@ -0,0 +1,148 @@
"""Email bot management API routes."""
import logging
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 EmailBot, User
_LOGGER = logging.getLogger(__name__)
router = APIRouter(prefix="/api/email-bots", tags=["email-bots"])
class EmailBotCreate(BaseModel):
name: str
icon: str = ""
email: str
smtp_host: str
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
smtp_use_tls: bool = True
class EmailBotUpdate(BaseModel):
name: str | None = None
icon: str | None = None
email: str | None = None
smtp_host: str | None = None
smtp_port: int | None = None
smtp_username: str | None = None
smtp_password: str | None = None
smtp_use_tls: bool | None = None
@router.get("")
async def list_email_bots(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
result = await session.exec(
select(EmailBot).where(EmailBot.user_id == user.id)
)
return [_response(b) for b in result.all()]
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_email_bot(
body: EmailBotCreate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
bot = EmailBot(user_id=user.id, **body.model_dump())
session.add(bot)
await session.commit()
await session.refresh(bot)
return _response(bot)
@router.get("/{bot_id}")
async def get_email_bot(
bot_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
bot = await _get_user_bot(session, bot_id, user.id)
return _response(bot)
@router.put("/{bot_id}")
async def update_email_bot(
bot_id: int,
body: EmailBotUpdate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
bot = await _get_user_bot(session, bot_id, user.id)
for field, value in body.model_dump(exclude_unset=True).items():
setattr(bot, field, value)
session.add(bot)
await session.commit()
await session.refresh(bot)
return _response(bot)
@router.delete("/{bot_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_email_bot(
bot_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
bot = await _get_user_bot(session, bot_id, user.id)
await session.delete(bot)
await session.commit()
@router.post("/{bot_id}/test")
async def test_email_bot(
bot_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Send a test email to the bot's own address to verify SMTP connection."""
bot = await _get_user_bot(session, bot_id, user.id)
from notify_bridge_core.notifications.email.client import EmailClient, SmtpConfig
client = EmailClient(SmtpConfig(
host=bot.smtp_host,
port=bot.smtp_port,
username=bot.smtp_username,
password=bot.smtp_password,
from_address=bot.email,
from_name=bot.name,
use_tls=bot.smtp_use_tls,
))
result = await client.send(
to_email=bot.email,
subject="Notify Bridge — Test Connection",
body_text="This is a test email from Notify Bridge. Your SMTP settings are working correctly.",
)
return result
def _response(bot: EmailBot) -> dict:
return {
"id": bot.id,
"name": bot.name,
"icon": bot.icon,
"email": bot.email,
"smtp_host": bot.smtp_host,
"smtp_port": bot.smtp_port,
"smtp_username": bot.smtp_username,
"smtp_password": "***" if bot.smtp_password else "",
"smtp_use_tls": bot.smtp_use_tls,
"created_at": bot.created_at.isoformat(),
}
async def _get_user_bot(session: AsyncSession, bot_id: int, user_id: int) -> EmailBot:
bot = await session.get(EmailBot, bot_id)
if not bot or bot.user_id != user_id:
raise HTTPException(status_code=404, detail="Email bot not found")
return bot