feat(commands): per-chat album scope derived from notification routing
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.
This commit is contained in:
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ...database.models import CommandTrackerListener, ServiceProvider
|
||||
from ...database.models import ServiceProvider
|
||||
from ...services import make_immich_provider
|
||||
from ...services.http_session import get_http_session
|
||||
from ..command_utils import get_trackers_for_provider
|
||||
@@ -20,7 +20,7 @@ async def _cmd_albums(
|
||||
provider: ServiceProvider,
|
||||
locale: str,
|
||||
*,
|
||||
listener: CommandTrackerListener | None = None,
|
||||
allowed_album_ids: set[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
trackers = await get_trackers_for_provider(provider.id)
|
||||
if not trackers:
|
||||
@@ -35,12 +35,11 @@ async def _cmd_albums(
|
||||
seen.add(aid)
|
||||
album_ids.append(aid)
|
||||
|
||||
# Per-chat album scope — match what _cmd_immich does for media commands.
|
||||
# Without this, /albums leaks the full list of tracked albums into chats
|
||||
# that were explicitly scoped to a subset.
|
||||
if listener is not None and listener.allowed_album_ids is not None:
|
||||
allowed = set(listener.allowed_album_ids)
|
||||
album_ids = [aid for aid in album_ids if aid in allowed]
|
||||
# Intersect with the dispatcher-resolved scope (listener override, else
|
||||
# derived from notification routing for this chat). Without this,
|
||||
# /albums leaks the full tracked-album list into chats never wired up.
|
||||
if allowed_album_ids is not None:
|
||||
album_ids = [aid for aid in album_ids if aid in allowed_album_ids]
|
||||
|
||||
if not album_ids:
|
||||
return {"albums": []}
|
||||
|
||||
Reference in New Issue
Block a user