Bot commands
Who is this for
Section titled “Who is this for”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 inLEADS_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.
How routing works
Section titled “How routing works”The dispatcher is built in main.build_dispatcher
(source), in this order:
- Maintenance middleware on all messages and callbacks.
maintenancerouter —/pause,/resume. Private chat +ADMIN_USER_IDS.startrouter —/start. Private chat only.consentrouter — the consent button. Private chat only.adminrouter —/stats,/admin_stats. Private chat +ADMIN_USER_IDS.leadsrouter — only ifLEADS_ENABLED=1andLEADS_CHAT_IDis set. Sees only that group.leads_fallbackrouter — 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.
Guest commands (private chat)
Section titled “Guest commands (private chat)”Source: bot/handlers/start.py,
bot/handlers/consent.py.
/start <token> — deep link from the kiosk QR
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.
/start without a token
Section titled “/start without a token”
Привет! Чтобы начать, отсканируй QR-код на планшете станции — без него я не смогу привязать тебя к сессии фотографирования.
(“Hi! To begin, scan the QR code on the station’s tablet — without it I cannot link you to a photo session.”)
Consent button — consent:<token>
Section titled “Consent button — consent:<token>”| 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.
In groups
Section titled “In groups”/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:
/resumecannot lift a pause that comes fromMAINTENANCE_MODE=1. That needs the variable set to0and a restart of both services.- After an automatic pause caused by an “out of credit” answer,
/resumerestarts 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,#42and#stickers_42are all accepted. - The word
тест/testmay stand before or after the number. /stickers_forgetwith an argument that is not a number answersНомер — цифрами: /stickers_forget 42 (тестовую: /stickers_forget 42 тест)./stickers_leadwith 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_newprints names, handles and phones regardless ofLEADS_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.
Lead statuses and card buttons
Section titled “Lead statuses and card buttons”| 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.
Orphan buttons
Section titled “Orphan buttons”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.
Callback data formats
Section titled “Callback data formats”| 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 mode
Section titled “Maintenance mode”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=….
Guest message catalogue
Section titled “Guest message catalogue”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.
Where to go next
Section titled “Where to go next”- Sticker bot component — handlers, loops and middleware in context.
- Guest session flow and Pack delivery — when each message is sent.
- Lead status and Data erasure — the group commands in their process.
- Lead process — how the sales side uses the group.
- API reference — the HTTP side of the same states.
- Runbook: bot silent — when the bot answers nothing.
- i18n — why the product’s texts are Russian only.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.