Files
notify-bridge/packages/server/src/notify_bridge_server/api/slot_helpers.py
T
alexei.dolgolyov b803d004e1 refactor: comprehensive codebase review — security, performance, quality, UX
Security:
- Fix NUT protocol command injection (validate names against safe regex)
- Enable Jinja2 autoescape=True to prevent HTML injection via external data
- Add WebhookProviderConfig validation model

Performance:
- Shared aiohttp.ClientSession singleton (replaces 40+ per-request sessions)
- Fix 4 N+1 queries with batch IN loads (poller, scheduler, memory, broadcast)
- asyncio.gather for Gitea commands and notification dispatcher
- Add DB indexes on NotificationTrackerState.tracker_id, CommandTrackerListener
- LRU cache for compiled Jinja2 templates
- Daily EventLog cleanup job (90-day retention)
- 30s HTTP timeout on all external calls
- GROUP BY for target type counts (replaces 7 sequential queries)

Code quality:
- Extract get_owned_entity() helper (replaces 11 duplicate functions)
- Extract slot_helpers.py (load_slots, save_slots, render_template_preview)
- Extract command_utils.py (tracker lookup, last event, collection IDs)
- Extract http_session.py (shared session lifecycle)
- Provider connection validation dedup (3x → 1 helper)
- Command dispatch tables replacing if/elif chains
- Album+links fetch helper (fetch_albums_with_links)
- Provider dispatch polymorphism (list_provider_collections)
- Immutable _enrich_assets (no longer mutates in-place)
- Fix _format_assets return type + handler unpacking

Frontend:
- Fix 18+ hardcoded English strings → t() with new i18n keys (en + ru)
- Mobile "More" nav panel with provider filter and search
- Shared Button.svelte component (4 variants, 2 sizes)
- Shared ErrorBanner.svelte component (8 pages updated)
- SvelteKit goto() replacing window.location.href
- Dashboard grid fixed for 4 cards, paginator opacity consistency

Functionality:
- max_instances=1 on scheduler jobs (prevents duplicate events)
- Webhook provider in watcher (prevents error spam)
- Fix stale SQLModel reference in poller
- Gitea get_repo() direct API call
2026-03-28 13:22:26 +03:00

91 lines
3.2 KiB
Python

"""Shared slot load/save and Jinja2 preview helpers for template config APIs."""
from typing import TypeVar
from jinja2 import StrictUndefined, TemplateSyntaxError, UndefinedError
from jinja2.sandbox import SandboxedEnvironment
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession
S = TypeVar("S", bound=SQLModel)
async def load_slots(
session: AsyncSession,
slot_model: type[S],
config_id: int,
) -> dict[str, dict[str, str]]:
"""Load all template slots for a config as {slot_name: {locale: template}}.
Works for both TemplateSlot and CommandTemplateSlot — they share the same
column names (config_id, slot_name, locale, template).
"""
result = await session.exec(
select(slot_model).where(slot_model.config_id == config_id) # type: ignore[attr-defined]
)
slots: dict[str, dict[str, str]] = {}
for s in result.all():
slots.setdefault(s.slot_name, {})[s.locale] = s.template # type: ignore[attr-defined]
return slots
async def save_slots(
session: AsyncSession,
slot_model: type[S],
config_id: int,
slots: dict[str, dict[str, str]],
) -> None:
"""Create or update template slots for a config (locale-aware).
Works for both TemplateSlot and CommandTemplateSlot.
"""
for slot_name, locale_map in slots.items():
for locale, template_text in locale_map.items():
result = await session.exec(
select(slot_model).where(
slot_model.config_id == config_id, # type: ignore[attr-defined]
slot_model.slot_name == slot_name, # type: ignore[attr-defined]
slot_model.locale == locale, # type: ignore[attr-defined]
)
)
existing = result.first()
if existing:
existing.template = template_text # type: ignore[attr-defined]
session.add(existing)
else:
session.add(slot_model(
config_id=config_id,
slot_name=slot_name,
locale=locale,
template=template_text,
))
def render_template_preview(template: str, context: dict) -> dict:
"""Two-pass Jinja2 render: syntax check, then strict render.
Returns a dict with either ``{"rendered": str}`` on success, or
``{"rendered": None, "error": str, ...}`` on failure.
"""
# Pass 1: syntax check (default Undefined — catches parse errors only)
try:
env = SandboxedEnvironment(autoescape=False)
env.from_string(template)
except TemplateSyntaxError as e:
return {
"rendered": None,
"error": e.message,
"error_line": e.lineno,
}
# Pass 2: render with StrictUndefined to catch unknown variables
try:
strict_env = SandboxedEnvironment(autoescape=False, undefined=StrictUndefined)
tmpl = strict_env.from_string(template)
rendered = tmpl.render(**context)
return {"rendered": rendered}
except UndefinedError as e:
return {"rendered": None, "error": str(e), "error_line": None, "error_type": "undefined"}
except Exception as e:
return {"rendered": None, "error": str(e), "error_line": None}