feat: comprehensive code review fixes — security, performance, quality
Backend security: - Reject Gitea webhooks when webhook_secret is empty (was silently skipping) - Add slowapi rate limiting on login (5/min) and setup (3/min) endpoints - Add CORS middleware with configurable origins - Mask telegram_webhook_secret in settings API response - Protect system-owned command template configs from regular user modification - Increase minimum password length to 8 characters Backend performance: - Batch queries in _resolve_command_context (3 queries instead of 3N) - Concurrent album fetching with asyncio.gather in immich commands - Singleton Jinja2 SandboxedEnvironment (reuse instead of per-render creation) - TTLCache for rate limits (bounded memory, auto-eviction) - Optional aiohttp session reuse in send_reply/send_media_group Backend code quality: - Extract dispatch_helpers.py (shared link_data loading + event filtering) - Extract database/seeds.py from main.py (490 lines → dedicated module) - Split immich_handler.py (415 lines) into commands/immich/ subpackage - Replace bare except blocks with logged warnings - Add per-provider config validation (Pydantic models) - Truncate command input to 512 chars - Expose usage_* and desc_* slots in capabilities and variables API Frontend security: - CSS.escape() for user-controlled querySelector in highlight.ts - Client-side password min 8 chars validation on setup and password change Frontend code quality: - Replace any types with proper interfaces across top files - Decompose targets/+page.svelte into TargetForm + ReceiverSection - Fix $derived.by usage, $state mutation patterns - Add console.warn to empty catch blocks Frontend UX: - Auth redirect via goto() with "Redirecting..." state - Platform-aware Ctrl/Cmd K keyboard hint - Remove stat-card hover transform Frontend accessibility: - Modal: role=dialog, aria-modal, focus trap, restore focus - EntitySelect/IconGridSelect: listbox/option roles, aria-selected/disabled
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Album-related Immich bot commands: albums, favorites, summary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from ...database.models import ServiceProvider, TelegramBot
|
||||
from ...services import make_immich_provider
|
||||
from ..handler import _get_notification_trackers_for_providers, _render_cmd_template
|
||||
from .common import _format_assets
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _cmd_albums(
|
||||
bot: TelegramBot, providers_map: dict[int, ServiceProvider], locale: str,
|
||||
) -> dict[str, Any]:
|
||||
provider_ids = set(providers_map.keys())
|
||||
trackers = await _get_notification_trackers_for_providers(provider_ids)
|
||||
if not trackers:
|
||||
return {"albums": []}
|
||||
|
||||
albums_data: list[dict] = []
|
||||
async with aiohttp.ClientSession() as http:
|
||||
for tracker in trackers:
|
||||
provider = providers_map.get(tracker.provider_id)
|
||||
if not provider or provider.type != "immich":
|
||||
continue
|
||||
immich = make_immich_provider(http, provider)
|
||||
album_ids = tracker.collection_ids or []
|
||||
if not album_ids:
|
||||
continue
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[immich.client.get_album(aid) for aid in album_ids],
|
||||
return_exceptions=True,
|
||||
)
|
||||
for album_id, result in zip(album_ids, results):
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.warning("Failed to fetch album %s: %s", album_id, result)
|
||||
albums_data.append({
|
||||
"name": f"{album_id[:8]}...", "asset_count": "?", "id": album_id,
|
||||
})
|
||||
elif result:
|
||||
albums_data.append({
|
||||
"name": result.name, "asset_count": result.asset_count, "id": album_id,
|
||||
})
|
||||
|
||||
return {"albums": albums_data}
|
||||
|
||||
|
||||
async def cmd_favorites(
|
||||
bot: TelegramBot, providers_map: dict[int, ServiceProvider],
|
||||
all_album_ids: list[str], count: int, locale: str,
|
||||
response_mode: str, client: Any,
|
||||
cmd_templates: dict[str, dict[str, str]],
|
||||
) -> str | list[dict[str, Any]]:
|
||||
"""Handle /favorites command with concurrent album fetching."""
|
||||
album_ids = all_album_ids[:10]
|
||||
if not album_ids:
|
||||
return _format_assets([], "favorites", "", locale, response_mode, client, cmd_templates)
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[client.get_album(aid) for aid in album_ids],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
fav_assets: list[dict[str, Any]] = []
|
||||
for album_id, result in zip(album_ids, results):
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.warning("Failed to fetch album %s: %s", album_id, result)
|
||||
continue
|
||||
if result:
|
||||
for aid, asset in list(result.assets.items())[:50]:
|
||||
if asset.is_favorite and len(fav_assets) < count:
|
||||
fav_assets.append({
|
||||
"id": asset.id, "originalFileName": asset.filename,
|
||||
"type": asset.type,
|
||||
})
|
||||
if len(fav_assets) >= count:
|
||||
break
|
||||
|
||||
return _format_assets(fav_assets, "favorites", "", locale, response_mode, client, cmd_templates)
|
||||
|
||||
|
||||
async def cmd_summary(
|
||||
client: Any, all_album_ids: list[str], locale: str,
|
||||
cmd_templates: dict[str, dict[str, str]],
|
||||
) -> str:
|
||||
"""Handle /summary command with concurrent album fetching."""
|
||||
if not all_album_ids:
|
||||
return _render_cmd_template(cmd_templates, "summary", locale, {"albums": []})
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[client.get_album(aid) for aid in all_album_ids],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
albums_data: list[dict] = []
|
||||
for album_id, result in zip(all_album_ids, results):
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.warning("Failed to fetch album %s: %s", album_id, result)
|
||||
continue
|
||||
if result:
|
||||
albums_data.append({
|
||||
"name": result.name, "asset_count": result.asset_count, "id": album_id,
|
||||
})
|
||||
|
||||
return _render_cmd_template(cmd_templates, "summary", locale, {"albums": albums_data})
|
||||
Reference in New Issue
Block a user