feat(commands): keep chat-action hint alive during slow command fetches

Slow bot commands (/latest, /random, /favorites, /memory, /search,
/find, /person, /place, /summary) spend most of their wall time
fetching assets from the service provider, not uploading to Telegram.
Telegram chat actions expire after ~5s, so the previous one-shot hint
vanished long before media arrived — users saw nothing happening.

- TelegramClient.start_chat_action_keepalive: promoted from private
  helper to public API, posts the action every 4s until cancelled.
- telegram_send.telegram_chat_action: async context manager that
  starts the keep-alive task on enter and cancels + awaits it on
  exit. A None action makes it a no-op so callers don't branch.
- classify_command_chat_action: maps command name to the right
  Telegram action (upload_photo for media-returning commands, typing
  for /summary, None for fast DB-only commands like /status /events).
- webhook.py + telegram_poller.py: wrap handle_command in the context
  manager so the hint persists through the whole fetch+upload window
  in both webhook and long-poll modes.
This commit is contained in:
2026-04-22 18:56:18 +03:00
parent fe38d20b96
commit 69711bbc84
5 changed files with 99 additions and 20 deletions
@@ -41,6 +41,36 @@ _rate_limits: TTLCache = TTLCache(maxsize=10000, ttl=3600)
# Maximum responses per command to avoid Telegram rate limits
_MAX_RESPONSES_PER_COMMAND = 5
# Commands that fetch assets from the service provider and usually reply
# with media — "uploading photo" is the accurate UX hint while we wait on
# the provider API + Telegram upload.
_UPLOAD_PHOTO_COMMANDS = frozenset({
"latest", "random", "favorites", "memory",
"search", "find", "person", "place",
})
# Commands that fetch from the provider but reply with text only.
# "typing" is accurate; we still want an indicator because the fetch is slow.
_TYPING_COMMANDS = frozenset({"summary"})
def classify_command_chat_action(text: str) -> str | None:
"""Return the Telegram chat-action hint to show for this command, or None.
The classification is by command name alone — good enough for the
cases where a chat action is worthwhile (slow provider fetches). Fast
DB-only commands (``/status``, ``/albums``, ``/events``, ``/people``)
return ``None`` and skip the indicator entirely.
"""
cmd, _, _ = parse_command(text)
if not cmd:
return None
if cmd in _UPLOAD_PHOTO_COMMANDS:
return "upload_photo"
if cmd in _TYPING_COMMANDS:
return "typing"
return None
def _check_rate_limit(bot_id: int, chat_id: str, cmd: str, limits: dict[str, int]) -> int | None:
"""Check rate limit. Returns seconds to wait, or None if OK."""