3b76a09759
The "per-chat album scope" feature stored on CommandTrackerListener was
really per-bot: listener_id = bot.id, and every chat that bot served
shared the same scope. Commands like /albums, /random, /status,
/events leaked the full provider catalog into chats that were never
wired up to receive notifications from those trackers.
New model: the album scope for /commands in a given chat is derived
from the notification-routing graph. For a (provider, bot, chat_id)
triple we walk TargetReceiver (chat_id match, enabled) →
NotificationTarget (telegram or broadcast parent) →
NotificationTrackerTarget → NotificationTracker (provider match) and
union their collection_ids. That's the natural "what does this chat
get notifications about" set, and it becomes the command scope.
- New helper: command_utils.resolve_chat_album_scope(provider_id,
bot_id, chat_id) -> set[str]. Empty set is the default for chats
with no routing — commands return nothing rather than leaking the
provider's catalog.
- Dispatcher computes the scope per (tracker, bot, chat) and threads
it through handler.handle(..., allowed_album_ids=...). Explicit
CommandTrackerListener.allowed_album_ids override, when set, still
wins verbatim (kept as an escape hatch for users who want a divergent
scope for a whole bot).
- /status, /albums, /events, and all /_cmd_immich-routed commands
(/random, /search, /find, /latest, /memory, /summary, /favorites,
/place, /person) now intersect with the resolved scope.
- UI scope modal relabeled: it's an explicit *override for this bot*,
not a per-chat setting. Default is "derive from notification
routing", which matches what users already configured elsewhere.
Also:
- /search, /find, /person, /place — _enrich_assets return value was
discarded, dropping public_url enrichment. Assign the return value.
- search_smart / search_metadata — consolidated into _search_items
helper that logs non-200 responses and transport errors instead of
silently returning []. Makes "always no results" bugs actually
diagnosable. Also accepts the alternate {"assets": [...]} flat-list
shape from older Immich versions.
- Immich search error bodies go through _redact_body so credentials
echoed by authenticating proxies don't land in server logs.
94 lines
3.2 KiB
Python
94 lines
3.2 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,
|
|
allowed_album_ids: set[str] | 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.
|
|
listener: The listener row for this (tracker, bot) pair.
|
|
allowed_album_ids: Precomputed album scope for this (bot, chat)
|
|
pair. Resolved by the dispatcher from the listener override
|
|
(if set) or the notification-routing graph. ``None`` means
|
|
"no scope restriction" (rarely the right default for album
|
|
providers — empty set is the common case).
|
|
page: 1-based page number for paginated commands (/search, /find).
|
|
|
|
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 {}
|