Skip to content

Bot commands

A developer changing bot handlers, and an operator who needs to know exactly what the bot says and to whom. The Russian strings are quoted verbatim so you can search logs and code for them.

  • Guests have one command: /start, normally with a session token from the kiosk QR, and one button: ✅ Согласен (“I agree”). Everything else a guest receives is sent by the system.
  • Admin commands work only in a private chat and only for ids in ADMIN_USER_IDS: /stats, /admin_stats, /pause, /resume.
  • Lead commands work only in the requests group (LEADS_CHAT_ID) and only for ids in LEADS_ADMIN_IDS: /stickers_stats, /stickers_new, /stickers_lead, /stickers_export, /stickers_forget.
  • Anyone without the right gets silence on a command and a Нет доступа (“no access”) pop-up on a button. The bot never reveals that a command exists.
  • In maintenance every guest message or button gets one fixed text. Admin commands and the whole requests group keep working.
  • There is no command to choose an outfit, reset a guest or re-run a job. Those are done on the kiosk and in the admin panel.

The dispatcher is built in main.build_dispatcher (source), in this order:

  1. Maintenance middleware on all messages and callbacks.
  2. maintenance router — /pause, /resume. Private chat + ADMIN_USER_IDS.
  3. start router — /start. Private chat only.
  4. consent router — the consent button. Private chat only.
  5. admin router — /stats, /admin_stats. Private chat + ADMIN_USER_IDS.
  6. leads router — only if LEADS_ENABLED=1 and LEADS_CHAT_ID is set. Sees only that group.
  7. leads_fallback router — answers lead buttons nobody else handled.

A message that matches no handler gets no answer. That is deliberate: free text from a guest is ignored.

Source: bot/handlers/start.py, bot/handlers/consent.py.

Section titled “/start <token> — deep link from the kiosk QR”

The QR encodes https://t.me/<bot_username>?start=<token>. Telegram turns it into /start <token>. The handler stores the guest’s first name, last name and username, then checks in this order:

# Condition Bot answers Side effect
1 The account has used its pack allowance and a pack is ready У тебя уже есть набор: <link> (“You already have a set”) The kiosk session is closed with notice pack_ready; the kiosk shows a friendly screen. Log marker PACK_LIMIT.
2 The allowance is used and a pack is still being made Твой набор уже готовится ⏳ Пришлю его сюда, как только он будет готов. (“Your set is already being prepared; I will send it here when ready”) Session closed with notice pack_in_progress.
3 No session with this token Не нашёл сессию с планшета. Подойди к станции и отсканируй QR-код ещё раз. (“I could not find the tablet session. Come to the station and scan the QR again”) —
4 The kiosk already closed the session (idle, cancel, QR timeout) Сессия закрыта, нажми «СТАРТ» на планшете. (“The session is closed, press START on the tablet”) The guest’s allowance is not consumed.
5 The session belongs to another guest, or is already past consent Этот QR-код уже устарел. Подойди к станции и отсканируй QR-код ещё раз. (“This QR code is out of date…”) —
6 Otherwise The consent text with one inline button (below) Session → linked. Repeating /start by the same guest is harmless.

Consent message:

Привет! Чтобы сделать твой персональный набор стикеров, нужно твоё согласие на обработку фото лица. Фото используется только для генерации стикеров и автоматически удаляется в течение 72 часов.

(“Hi! To make your personal sticker set I need your consent to process a photo of your face. The photo is used only to generate stickers and is deleted automatically within 72 hours.”)

Button: ✅ Согласен (“I agree”), callback data consent:<token>.

The pack-limit check runs first, before the session is even looked up, so a guest who already has a pack never reaches the consent screen.

Привет! Чтобы начать, отсканируй QR-код на планшете станции — без него я не смогу привязать тебя к сессии фотографирования.

(“Hi! To begin, scan the QR code on the station’s tablet — without it I cannot link you to a photo session.”)

Outcome Bot does
Success Stores the profile, writes users.consented_at, moves the session to consented, and edits the consent message into Спасибо! Теперь вернись к планшету, выбери стиль и сфотографируйся. Как только набор стикеров будет готов, я пришлю его сюда 📸. The button disappears. Log line consent: user=… дал согласие на обработку фото.
The kiosk closed the session meanwhile Pop-up alert Сессия закрыта, нажми «СТАРТ» на планшете.
The session is not this guest’s, or is in another state Pop-up alert Сессия устарела — отсканируй QR-код на планшете ещё раз. (“The session is out of date — scan the QR on the tablet again”)

The consent is recorded before the session changes state, because the kiosk opens the outfit screen the moment it sees consented.

/start and the consent button are filtered to private chats. In any group, including the requests group, the bot stays silent.

Admin commands (private chat, ADMIN_USER_IDS)

Section titled “Admin commands (private chat, ADMIN_USER_IDS)”

Source: bot/handlers/admin.py, bot/handlers/maintenance.py.

The filter sits on the router: a non-admin, or an admin writing in a group, never reaches the handler. The bot answers nothing.

Command Answer
/stats Counters from the database, starting with 📊 Статистика: users (total, consented, with a pack), sessions by status, jobs by status, photo-to-pack time (average and maximum over finished jobs), age of the oldest queued job.
/admin_stats Summary starting with 📊 Сводка: estimated spend (total and today), balance estimate, packs ready / in progress / failed, queue, rate-limit and other errors in 24 hours, image requests in the provider’s current day against the quota, render mode, a warning if prices are not set, and the panel link <BASE_URL>/admin. If maintenance is on, a line 🛠 РЕЖИМ ОБСЛУЖИВАНИЯ (<source>): <reason> with how to lift it. Same data as GET /admin/api/summary.
/pause Turns maintenance on by writing a record to the database; no restart. Answer: Режим обслуживания включён: гостям — «техобслуживание», новые job не берутся. Снять — /resume. If already on: Режим обслуживания уже включён. Снять — /resume.
/resume Removes the database pause. Answers: Режим обслуживания снят — гости снова могут начать, очередь пошла. Упавшие из-за 402 паки — «Повторить» в админке. (was on); Режим обслуживания не был включён. (was not on); or, when MAINTENANCE_MODE=1 is set in the environment, Пауза из БД снята, но в bot/.env стоит MAINTENANCE_MODE=1 — режим остаётся. Поставь 0 и перезапусти сервисы (update.sh).

Notes:

  • /resume cannot lift a pause that comes from MAINTENANCE_MODE=1. That needs the variable set to 0 and a restart of both services.
  • After an automatic pause caused by an “out of credit” answer, /resume restarts the queue, but jobs that already failed must be retried from the admin panel.
  • These commands are not registered in the bot’s command menu.
  • There is no bot command to reset a guest. Use the panel button or tools/reset_users.py.

Requests group commands (LEADS_CHAT_ID, LEADS_ADMIN_IDS)

Section titled “Requests group commands (LEADS_CHAT_ID, LEADS_ADMIN_IDS)”

Source: bot/handlers/leads.py, texts in bot/services/leads.py.

Both conditions must hold: the message is in the configured group and the sender is in LEADS_ADMIN_IDS. Otherwise the bot is silent. Everything visible in this group carries the prefix stickers_, because bots of other products share the group.

At start the bot registers these five commands as a menu visible only to administrators of that group, checks that it is a member and an administrator there (a warning in the log if not), and logs the result. The real permission check is always LEADS_ADMIN_IDS, not the menu.

Command What it does Answer
/stickers_stats Summary of leads and packs. A message starting with 📊 Заявки AI Sticker Pack — <time> (Ташкент): totals (all time, today, 7 days, 30 days), by status, by language, by utm_source, repeat requests, and packs generated (excluding packs of ADMIN_USER_IDS). “Today” starts at local midnight, UTC+5; 7 and 30 days are rolling.
/stickers_new Leads still in status new, oldest first, at most 20. 🆕 Без статуса: N followed by one line per lead: #stickers_<id> · date · name · handle · phone. If there are more: …и ещё K — старые сверху, разберите эти. If none: ✅ Необработанных заявок нет.
/stickers_lead <number> Posts the card of that lead again as a new message with its buttons. Later reminders reply to this fresh card. The card. Unknown number: Заявки #<n> нет (“there is no lead #n”).
/stickers_lead (no number) Offers the 5 most recent non-anonymised leads as buttons. Какую карточку показать? Последние заявки: + buttons #<id> <name>. If none: Заявок с контактами нет.
/stickers_export Sends a CSV of all leads to the caller’s private chat, never to the group. In the group: CSV отправлен в личку. In private: a file stickers_leads_<YYYY-MM-DD>.csv with caption Заявки AI Sticker Pack: N шт. If the bot cannot write to the caller: Не могу написать в личку — откройте @<bot_username> и нажмите Start, потом повторите /stickers_export.
/stickers_forget <number> Erasure on a person’s request: irreversibly blanks name, Telegram handle, phone and comment. The lead stays in statistics without contacts. The card is replaced by a stub. Данные заявки #<n> удалены (имя, Telegram, телефон, комментарий). Из бэкапов сервера уйдут за 14 дней. В статистике заявка остаётся без контактов. Already erased: Данные заявки #<n> уже удалены. Unknown: Заявки #<n> нет.
/stickers_forget <number> тест Deletes a test lead completely, including from statistics. Works only if the lead’s name or comment contains тест or test (any case). The card text becomes 🧪 Тестовая заявка #stickers_<n> — удалена. Тестовая заявка #<n> удалена целиком — в статистике её больше нет. Not a test lead: #<n> — не тестовая (нет «ТЕСТ» в имени или комментарии): целиком не удаляю. Обезличить по запросу: /stickers_forget <n>.
/stickers_forget (no number) Offers the 5 most recent leads as buttons with a bin icon. Чьи данные удалить? Необратимо: имя, Telegram, телефон, комментарий (в статистике заявка останется). If none: Заявок с контактами нет.
/stickers_forget тест (no number) The same, but only test leads, for complete deletion. Какую тестовую заявку удалить целиком (и из статистики)? If none: Тестовых заявок нет.

Details:

  • Number formats. 42, #42 and #stickers_42 are all accepted.
  • The word тест / test may stand before or after the number.
  • /stickers_forget with an argument that is not a number answers Номер — цифрами: /stickers_forget 42 (тестовую: /stickers_forget 42 тест). /stickers_lead with a non-number falls back to the picker.
  • CSV format: UTF-8 with a byte-order mark, ; separator, CRLF line ends, so it opens directly in a spreadsheet. Columns: number, created (local time), name, telegram, phone, date, guests, comment, language, utm, utm_source, utm_medium, utm_campaign, status, who set it, when, data erased. A cell starting with =, +, -, @, tab or carriage return is prefixed with an apostrophe to prevent formula injection.
  • /stickers_new prints names, handles and phones regardless of LEADS_CARD_PII. Only cards and picker buttons honour that switch.
  • Every answer is logged without personal data: LEADS_CMD …, LEAD_STATUS …, LEAD_PICK …, LEAD_ERASED, LEAD_DELETED_TEST.

The bot also posts to this group on its own: new cards, a reminder for leads left without a status, a daily digest, and a ping on a repeat request. Those are described in Lead status and Reminders and digest.

Code Icon and label on the card Button caption
new 🆕 новая (new) — (initial state, no button)
in_work 🔧 в работе (in progress) Взял в работу
contacted 📞 связались (contacted) Связались
deal ✅ сделка (deal) Сделка
refused ❌ отказ (declined) Отказ
spam 🚫 спам (spam) Спам

Buttons are laid out in two rows: in_work, contacted / deal, refused, spam. The current status is marked with • before the caption. An erased lead has no buttons.

Pop-up answers to a status button:

Situation Answer
Presser not in LEADS_ADMIN_IDS Нет доступа (alert)
Status changed Готово: <icon> <label>; the card is edited in place and shows who changed it and when
Same status pressed again Уже так: <icon> <label> (“already so”)
Lead not in the database Заявки #<n> нет в базе (alert)
Lead erased Данные заявки удалены — статус не меняется (alert)
Unparseable button Не понял кнопку — обнови карточку: /stickers_lead <номер> (alert)
Saved, but the card message cannot be edited Сохранено: <label>. Карточка недоступна — /stickers_lead <n> (alert)

Pop-up answers to a picker button: Карточка #<n> ниже (“card below”) for lead; for forget / forgettest the picker message is replaced by the result text and its buttons are removed, so a second press cannot delete anything again.

If a lead button arrives and the leads router did not take it — the module is off, the card is in another chat, or the bot runs old code — the fallback router answers with an alert:

Заявки сейчас не ведутся этим ботом: модуль выключен или карточка из другого чата. Когда модуль включён — /stickers_lead <номер> пришлёт карточку заново.

(“Leads are not handled by this bot right now: the module is off or the card is from another chat…”). Log marker LEAD_BUTTON_ORPHAN. Without this the button would spin forever.

Format Example Where Who may press Parsed by
consent:<session_token> consent:<token> Private chat, consent message The guest the session is linked to handlers/consent.py
stickers_lead:<lead_id>:<status> stickers_lead:42:in_work Requests group, lead card LEADS_ADMIN_IDS leads.parse_callback — status must be one of in_work, contacted, deal, refused, spam (never new)
stickers_pick:<action>:<lead_id> stickers_pick:forget:42 Requests group, picker message LEADS_ADMIN_IDS leads.parse_pick — action is lead, forget or forgettest; id must be digits

Maintenance is on when MAINTENANCE_MODE=1, after /pause, or automatically after an “out of credit” answer from the AI provider.

Who / where What happens
Guest, private chat, any message Сейчас идёт техобслуживание, загляни чуть позже 🛠 (“Maintenance in progress, come back a little later”). No handler runs: no session is linked.
Guest, private chat, any button The same text as a pop-up alert. Consent is not accepted.
Admin (ADMIN_USER_IDS), private chat, a message starting with / other than /start Passes through: /stats, /admin_stats, /pause, /resume work.
Admin, /start Treated like a guest: maintenance text.
Any group, including the requests group Not affected. Status buttons and /stickers_* keep working, because leads do not depend on generation.
Kiosk 503 on new session, outfit choice and photo; see API reference.
Worker Takes no new jobs; finishes the ones already started. Delivery deadlines keep running.

If the maintenance state cannot be read from the database, the middleware lets the update through: a working bot is preferred to a silent one. Log marker for every maintenance answer: MAINTENANCE: ответ user=….

All guest-facing texts sent outside the two handlers above live in bot/texts.py, so the bot, the API and the worker use the same wording. The product speaks Russian only and addresses the guest informally.

Constant Russian text (verbatim) English gloss When it is sent
CONSENT_DONE Спасибо! Теперь вернись к планшету, выбери стиль и сфотографируйся. Как только набор стикеров будет готов, я пришлю его сюда 📸 Thanks! Now go back to the tablet, choose a style and take the photo. As soon as the sticker set is ready I will send it here. Replaces the consent message right after the guest presses ✅ Согласен. Sent by the bot.
GENERATING Генерирую твой персональный набор стикеров... обычно это занимает пару минут ⏳ Generating your personal sticker set… this usually takes a couple of minutes. Once per job, right after the photo is accepted and the job is queued. Sent by the API process in a background task.
DELAY_NOTICE Дорабатываем детали твоего набора ✨ Скоро пришлю! Finishing the details of your set. I will send it soon! Exactly once per job, when 5 minutes have passed since the photo and no link has been sent. Sent by the delivery sweeper, together with an admin alert.
pack_ready(link) Готово! 🎉 Вот твой персональный набор стикеров: + new line + <link> Done! Here is your personal sticker set: When the pack is confirmed: 12 stickers reported and the public pack page confirmed twice.
already_have_pack(link) У тебя уже есть набор: <link> You already have a set: The pack allowance is used and a pack is ready: on /start, on outfit choice, or on photo upload.
PACK_IN_PROGRESS Твой набор уже готовится ⏳ Пришлю его сюда, как только он будет готов. Your set is already being prepared. I will send it here as soon as it is ready. The allowance is used and a pack is still in the queue, being generated or being verified.
SESSION_CLOSED Сессия закрыта, нажми «СТАРТ» на планшете. The session is closed, press START on the tablet. The guest scanned a QR or pressed the consent button of a session the kiosk already closed.
GENERATION_FAILED Что-то пошло не так с генерацией твоего набора. Команда уже разбирается, напиши нам на станции — поможем вручную. Something went wrong with generating your set. The team is already looking into it; tell us at the station and we will help manually. The job failed after all attempts, or 15 minutes passed since the photo without a confirmed link.
SERVICE_UNAVAILABLE Не получилось сделать твой набор: генерация временно недоступна, мы уже чиним 🛠 Твоё фото сохранено — как только всё заработает, сделаем набор заново и пришлём сюда. Если долго нет — подойди к станции. We could not make your set: generation is temporarily unavailable and we are fixing it. Your photo is saved — as soon as everything works we will make the set again and send it here. If it takes long, come to the station. The job failed because the AI provider reported that the account is out of credit. The photo stays on the server until the TTL so an admin can retry.
MAINTENANCE Сейчас идёт техобслуживание, загляни чуть позже 🛠 Maintenance in progress, come back a little later. Any guest message or button while maintenance is on. Also the message of the API’s 503 body.
LINK_HINT Если ссылка не открывается сразу — подожди 10-15 секунд и нажми ещё раз 🙂 If the link does not open at once, wait 10–15 seconds and tap again. Not sent. Kept for the case where the public-page check has to be switched off.

Texts defined inside handlers rather than in texts.py:

Where Russian text (verbatim) When
handlers/start.py Привет! Чтобы сделать твой персональный набор стикеров, нужно твоё согласие на обработку фото лица. Фото используется только для генерации стикеров и автоматически удаляется в течение 72 часов. Consent screen
handlers/start.py Привет! Чтобы начать, отсканируй QR-код на планшете станции — без него я не смогу привязать тебя к сессии фотографирования. /start without a token
handlers/start.py Не нашёл сессию с планшета. Подойди к станции и отсканируй QR-код ещё раз. Unknown token
handlers/start.py Этот QR-код уже устарел. Подойди к станции и отсканируй QR-код ещё раз. Session taken or too far along
handlers/consent.py Сессия устарела — отсканируй QR-код на планшете ещё раз. Consent on a session that is not this guest’s

The normal sequence for one guest is: consent text → CONSENT_DONE → GENERATING → pack_ready. There is at most one delay message per job, and no other message about waiting.


Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.