Repository navigation
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() 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 |
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))
returnAttaches 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.
On SIGTERM, sets a shutdown flag. New updates are rejected with a user-friendly message. Existing in-flight handlers drain before the process exits.
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)
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 |