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:
@@ -17,6 +17,8 @@ from ..database.engine import get_engine
|
||||
from ..services import make_immich_provider
|
||||
from ..database.models import (
|
||||
CommandConfig,
|
||||
CommandTemplateConfig,
|
||||
CommandTemplateSlot,
|
||||
CommandTracker,
|
||||
CommandTrackerListener,
|
||||
EventLog,
|
||||
@@ -51,6 +53,23 @@ def _check_rate_limit(bot_id: int, chat_id: str, cmd: str, limits: dict[str, int
|
||||
return None
|
||||
|
||||
|
||||
def _render_cmd_template(
|
||||
templates: dict[str, str], slot_name: str, context: dict[str, Any]
|
||||
) -> str | None:
|
||||
"""Try to render a command template. Returns None if no template or error."""
|
||||
template_str = templates.get(slot_name)
|
||||
if not template_str:
|
||||
return None
|
||||
try:
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
env = SandboxedEnvironment(autoescape=False)
|
||||
tmpl = env.from_string(template_str)
|
||||
return tmpl.render(**context)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to render command template '%s': %s", slot_name, e)
|
||||
return None
|
||||
|
||||
|
||||
async def _resolve_command_context(
|
||||
bot: TelegramBot,
|
||||
) -> list[tuple[CommandTracker, CommandConfig, ServiceProvider]]:
|
||||
@@ -87,7 +106,20 @@ async def _resolve_command_context(
|
||||
continue
|
||||
tuples.append((tracker, config, provider))
|
||||
|
||||
return tuples
|
||||
# Load command template slots from the first config that has one
|
||||
cmd_template_slots: dict[str, str] = {}
|
||||
for _, config, _ in tuples:
|
||||
if config.command_template_config_id:
|
||||
slot_result = await session.exec(
|
||||
select(CommandTemplateSlot).where(
|
||||
CommandTemplateSlot.config_id == config.command_template_config_id
|
||||
)
|
||||
)
|
||||
cmd_template_slots = {s.slot_name: s.template for s in slot_result.all()}
|
||||
if cmd_template_slots:
|
||||
break
|
||||
|
||||
return tuples, cmd_template_slots
|
||||
|
||||
|
||||
def _merge_command_context(
|
||||
@@ -125,10 +157,13 @@ async def handle_command(
|
||||
if not cmd:
|
||||
return None
|
||||
|
||||
ctx = await _resolve_command_context(bot)
|
||||
enabled, locale, response_mode, default_count, rate_limits = _merge_command_context(ctx)
|
||||
ctx_tuples, cmd_templates = await _resolve_command_context(bot)
|
||||
enabled, locale, response_mode, default_count, rate_limits = _merge_command_context(ctx_tuples)
|
||||
|
||||
if cmd == "start":
|
||||
result = _render_cmd_template(cmd_templates, "start", {"locale": locale, "bot_name": bot.name})
|
||||
if result:
|
||||
return result
|
||||
msgs = {
|
||||
"en": "Hi! I'm your Notify Bridge bot. Use /help to see available commands.",
|
||||
"ru": "Привет! Я бот Notify Bridge. Используйте /help для списка команд.",
|
||||
@@ -141,6 +176,9 @@ async def handle_command(
|
||||
# Rate limit check
|
||||
wait = _check_rate_limit(bot.id, chat_id, cmd, rate_limits)
|
||||
if wait is not None:
|
||||
result = _render_cmd_template(cmd_templates, "rate_limited", {"wait": wait, "locale": locale})
|
||||
if result:
|
||||
return result
|
||||
msgs = {
|
||||
"en": f"Please wait {wait}s before using this command again.",
|
||||
"ru": f"Подождите {wait} сек. перед повторным использованием.",
|
||||
@@ -151,7 +189,7 @@ async def handle_command(
|
||||
|
||||
# Build providers map from command context
|
||||
providers_map: dict[int, ServiceProvider] = {}
|
||||
for _, _, provider in ctx:
|
||||
for _, _, provider in ctx_tuples:
|
||||
providers_map[provider.id] = provider
|
||||
|
||||
# Dispatch
|
||||
|
||||
Reference in New Issue
Block a user