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
@@ -17,13 +17,16 @@ from notify_bridge_core.storage import JsonFileBackend
from ..database.engine import get_engine
from ..database.models import (
EmailBot,
EventLog,
NotificationTarget,
NotificationTracker,
NotificationTrackerState,
NotificationTrackerTarget,
ServiceProvider,
TargetReceiver,
TemplateConfig,
TemplateSlot,
TrackingConfig,
)
@@ -129,19 +132,59 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
if not target:
continue
# Load receivers for this target
recv_result = await session.exec(
select(TargetReceiver).where(
TargetReceiver.target_id == target.id,
TargetReceiver.enabled == True,
)
)
receivers = [dict(r.config) for r in recv_result.all()]
tracking_config = None
if tt.tracking_config_id:
tracking_config = await session.get(TrackingConfig, tt.tracking_config_id)
template_config = None
template_slots: dict[str, str] | None = None
if tt.template_config_id:
template_config = await session.get(TemplateConfig, tt.template_config_id)
if template_config:
slot_result = await session.exec(
select(TemplateSlot).where(TemplateSlot.config_id == template_config.id)
)
raw_slots = {s.slot_name: s.template for s in slot_result.all()}
# Map slot names to event_type values for dispatcher lookup
template_slots = {}
for slot_name, tmpl_text in raw_slots.items():
# Strip "message_" prefix for event-type slots
event_key = slot_name.removeprefix("message_") if slot_name.startswith("message_") else slot_name
template_slots[event_key] = tmpl_text
target_config = dict(target.config)
# Inject SMTP config for email targets from EmailBot
if target.type == "email":
email_bot_id = target.config.get("email_bot_id")
if email_bot_id:
email_bot = await session.get(EmailBot, email_bot_id)
if email_bot:
target_config["smtp"] = {
"host": email_bot.smtp_host,
"port": email_bot.smtp_port,
"username": email_bot.smtp_username,
"password": email_bot.smtp_password,
"from_address": email_bot.email,
"from_name": email_bot.name,
"use_tls": email_bot.smtp_use_tls,
}
link_data.append({
"target_type": target.type,
"target_config": dict(target.config),
"target_config": target_config,
"receivers": receivers,
"tracking_config": tracking_config,
"template_config": template_config,
"template_slots": template_slots,
})
# Snapshot the data we need
@@ -249,26 +292,17 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
_LOGGER.info(" Skipped by tracking config filter")
continue
# Build template slots from template config
tmpl = ld["template_config"]
slots = None
if tmpl:
slots = {
"assets_added": tmpl.message_assets_added,
"assets_removed": tmpl.message_assets_removed,
"collection_renamed": tmpl.message_collection_renamed,
"collection_deleted": tmpl.message_collection_deleted,
"sharing_changed": tmpl.message_sharing_changed,
}
target_configs.append(TargetConfig(
type=ld["target_type"],
config=ld["target_config"],
template_slots=slots,
template_slots=ld["template_slots"],
date_format=tmpl.date_format if tmpl else "%d.%m.%Y, %H:%M UTC",
date_only_format=tmpl.date_only_format if tmpl and tmpl.date_only_format else "%d.%m.%Y",
provider_api_key=provider_config.get("api_key"),
provider_internal_url=provider_config.get("url", ""),
provider_external_url=provider_config.get("external_domain", ""),
receivers=ld["receivers"],
))
if target_configs: