Skip to content

Bot Layer

Ingwald edited this page Aug 15, 2026 · 2 revisions

Bot Layer

app/bot/
β”œβ”€β”€ actions.py            β†’ send_search_results(), send_track_card(), show_main_menu()
β”œβ”€β”€ callbacks.py          β†’ callback_router() β€” dispatches all inline button events
β”œβ”€β”€ constants.py          β†’ CB_* callback prefixes, button text strings, make_callback()
β”œβ”€β”€ context.py            β†’ in-memory search context (paginated results per user)
β”œβ”€β”€ correlation_middleware.py β†’ attaches X-Correlation-ID to every update
β”œβ”€β”€ handlers.py           β†’ command handlers (/start, /help, admin commands)
β”œβ”€β”€ keyboard_favorites.py β†’ favorites inline keyboard builder
β”œβ”€β”€ keyboard_menus.py     β†’ main menu, admin menu keyboard builders
β”œβ”€β”€ language_callbacks.py β†’ language selection handler
β”œβ”€β”€ rate_limit.py         β†’ per-user sliding-window rate limiter
β”œβ”€β”€ shutdown_middleware.py β†’ graceful drain on SIGTERM
β”œβ”€β”€ similar_callbacks.py  β†’ 🎯 Similar button handler
└── track_callbacks.py    β†’ track selection handler

Callback Router

callback_router() in callbacks.py is the single entry point for all CallbackQuery events. It dispatches based on CB_* prefix:

Prefix Handler
CB_PAGE pagination β€” send_current_results_page()
CB_TRACK track detail β€” handle_track_callback()
CB_SIMILAR similar artists β€” handle_similar_callback()
CB_FAV_* favorites CRUD
CB_LANG language selection
CB_ADMIN_* admin actions

Rate Limiter

File: app/bot/rate_limit.py

Per-user, in-memory sliding-window limiter (v3.1.2+). Backed by Redis when available, falls back to in-process dict. Applied at the top of every handler that calls an external API. Blocked requests increment bot_rate_limit_blocked_total Prometheus counter.

# Usage pattern in handlers
if not await check_rate_limit(user_id):
    await message.answer(t("rate_limited", lang))
    return

Middlewares

CorrelationMiddleware

Attaches a UUID X-Correlation-ID to every incoming update. Propagated through structured JSON logs so all log lines for one request share the same ID.

ShutdownMiddleware

On SIGTERM, sets a shutdown flag. New updates are rejected with a user-friendly message. Existing in-flight handlers drain before the process exits.

Search Context

File: app/bot/context.py

Holds paginated search results per user (no Deezer round-trip for pagination).

Since v3.7.8, contexts are stored in Redis (sc:{user_id}, 1h TTL via SETEX) when Redis is available, so pagination survives a bot restart. Redis expires the keys itself. Falls back to the in-memory dict when Redis is down or not configured β€” same fallback pattern as rate_limit.py.

  • get_search_context(user_id) β€” fetch current context
  • get_page_tracks(user_id, page_size, page) β€” slice for a page
  • set_search_page(user_id, page, page_size) β€” set page, clamped to range
  • cleanup_expired_search_contexts() β€” prunes the in-memory tier only (Redis self-expires)

Admin Commands

Registered in handlers.py. All guarded by is_admin_user(). Admin IDs loaded from config/admins.json (never committed) or ADMIN_ID env variable.

Command Action
/stats database stats, table row counts
/maintenance DB size, schema version, table summary
/health bot, DB, Redis, Deezer, Spotify, Genius status
/cleanup_errors delete saved error log entries
/cleanup_history prune old search history
/audit recent admin action audit log

Clone this wiki locally