Files
notify-bridge/packages/server/src/notify_bridge_server/commands/base.py
T
alexei.dolgolyov a7a2b4efa4 feat: large polish pass — UX fixes, per-chat scope, restore/backup, action events
Backend
- Per-chat album scope for Immich commands (search/latest/memory/...): new
  allowed_album_ids on CommandTrackerListener, threaded listener/page kwargs
  through ProviderCommandHandler.handle; PATCH listener-scope endpoint.
- /search and /find accept a trailing page number; Immich client search_smart
  / search_metadata take a page param.
- Immich person-asset lookup switched from removed GET /api/people/{id}/assets
  to POST /api/search/metadata with personIds (fixes /person command and
  auto_organize rules silently returning zero candidates on Immich 1.106+).
- Auto_organize rule now sets the target album's thumbnail to the first added
  image when missing (falls back to any asset type); failures do not fail the
  rule. add_assets_to_album surfaces the Immich error body on non-2xx.
- EventLog.user_id / action_id / action_name columns with defensive migration
  + backfill. Status query filters by user_id directly; Immich/webhook paths
  emit user_id explicitly. action_runner writes an action_success/partial/
  failed event on each non-dry-run.
- Dashboard DELETE /api/status/events (scoped to user_id) + rendering live
  tracker/provider/action names via FK join with snapshot fallback.
- PATCH /api/users/{id} for username/role change with last-admin guard.
- Deletion protection returns structured {message, entity, blocked_by}
  (ApiError carries .blockedBy; frontend opens BlockedByModal).
- Backup prepare-restore → AppSetting markers + atomic write of
  pending_restore.json; lifespan hook applies on next startup and archives
  under data/applied_restores/. apply-restart sends SIGTERM so the lifespan
  shutdown runs; NOTIFY_BRIDGE_SUPERVISED env override gates the button.
  Manual POST /api/backup/files (same format as scheduled).
- New periodic-summary test path reuses shared collect_scheduled_assets
  (limit=0) so test and future production code go through one primitive.
- Per-receiver locale for Telegram test messages (resolves
  TelegramChat.language_override per chat instead of applying the first
  receiver's locale to everyone).
- Bounded concurrency (semaphores) in NotificationDispatcher._preload_asset_data
  and _refresh_telegram_chat_titles; chat title sweep extended to 24h since
  save_chat_from_webhook covers active chats opportunistically.
- Telegram poller detects the \"webhook is active\" 409 and auto-calls
  deleteWebhook for bots whose DB update_mode is polling (throttled per bot).
- TelegramClient.get_chat added (CLAUDE.md rule 6); set_album_thumbnail added.
- Seeds: rename \"Default Commands\" → \"Default Immich Commands\";
  track_assets_removed default False.

Frontend
- Global provider selector visible when there is only one provider.
- Clear-events button + i18n + ConfirmModal on the dashboard; new icons/
  labels/filters/colors for action_success / action_partial / action_failed.
- Auto-select first available tracking/template/command/config + bot on
  create forms (trackers, command-trackers, targets, template/command
  configs).
- Telegram target disable_url_preview defaults to true.
- BlockedByModal wired into 8 deletion flows; fetchAuth helper for
  multipart/binary calls (reuses api()'s refresh + ApiError mapping).
- Immich tracker 'Checking links' parallelised (concurrency cap 6).
- Backup page: pending-restore banner + Apply-now / Apply-later modal,
  restarting overlay polling /api/health, manual 'Create backup' button.
- Command-trackers listener row gets an 'Edit album scope' modal with
  inherit/explicit multiselect.
- Users page: Edit user modal (username + role).
- parseDate helper for consistent UTC date rendering.

Migrations / schema
- event_log: + user_id, action_id, action_name (+ backfill user_id from
  notification_tracker).
- command_tracker_listener: + allowed_album_ids.
2026-04-22 01:13:11 +03:00

86 lines
2.7 KiB
Python

"""Abstract provider command handler interface."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
from ..database.models import (
CommandConfig, CommandTracker, CommandTrackerListener,
ServiceProvider, TelegramBot,
)
@dataclass(frozen=True)
class CommandResponse:
"""A single response from one tracker's command execution."""
text: str | None = None
media: list[dict[str, Any]] = field(default_factory=list)
class ProviderCommandHandler(ABC):
"""Base class for provider-specific bot command handlers.
Each provider (Immich, Gitea, etc.) implements this interface to handle
its own set of commands. The dispatch layer routes commands to the
correct handler based on the provider type.
Each handler call receives a single (tracker, config, provider) context.
"""
provider_type: str
@abstractmethod
def get_provider_commands(self) -> set[str]:
"""Return the set of command names this handler owns.
These are provider-specific commands (e.g., 'albums' for Immich,
'repos' for Gitea). Universal commands like 'help' and 'start'
are handled by the main dispatcher.
"""
@abstractmethod
async def handle(
self,
cmd: str,
args: str,
count: int,
locale: str,
response_mode: str,
provider: ServiceProvider,
cmd_templates: dict[str, dict[str, str]],
bot: TelegramBot,
tracker: CommandTracker,
config: CommandConfig,
*,
listener: CommandTrackerListener | None = None,
page: int = 1,
) -> CommandResponse | None:
"""Handle a provider-specific command for a single tracker.
Args:
cmd: The command name (without '/').
args: Arguments after the command.
count: Number of results to return.
locale: User's locale ('en', 'ru').
response_mode: 'media' or 'text' (from this tracker's config).
provider: The service provider instance for this tracker.
cmd_templates: Template slots for this tracker's command template config.
bot: The Telegram bot instance.
tracker: The command tracker being dispatched.
config: The command config for this tracker.
Returns:
A CommandResponse, or None if unhandled.
"""
def get_rate_categories(self) -> dict[str, str]:
"""Return rate limit category mapping for this provider's commands.
Keys are command names, values are category strings.
Commands not listed default to 'default' category.
"""
return {}