Lead status change
Who is this for
Section titled “Who is this for”Developers of the leads module and team members who press the buttons and want to know what each answer means.
- Each card has five buttons. Callback data is
stickers_lead:<lead_id>:<status>. - Only Telegram user ids listed in
LEADS_ADMIN_IDSmay change a status. Everyone else gets a pop-upНет доступа(“No access”) and the card does not change. - Every real change writes one row to
lead_status_historyand rewrites the card. Pressing the current status changes nothing. - If the card cannot be edited (for example, it was deleted from the group), the status is still saved and the pop-up tells the user how to get a fresh card.
- A separate fallback router answers buttons that no one else handles, so a button never spins forever.
- Status buttons keep working during maintenance mode.
Buttons and statuses
Section titled “Buttons and statuses”Defined in STATUSES and BUTTON_ROWS in
services/leads.py.
| Code | Card label | Button text |
|---|---|---|
new |
🆕 новая (new) |
— (cannot be set by a button) |
in_work |
🔧 в работе (in work) |
🔧 Взял в работу (Took it) |
contacted |
📞 связались (contacted) |
📞 Связались (Contacted) |
deal |
✅ сделка (deal) |
✅ Сделка (Deal) |
refused |
❌ отказ (refused) |
❌ Отказ (Refused) |
spam |
🚫 спам (spam) |
🚫 Спам (Spam) |
Row 1 holds in_work and contacted; row 2 holds deal, refused and spam. The button of the current status is
prefixed with • . A status can be changed in any direction. An anonymised lead has no buttons.
The prefix stickers_ is deliberate: the group may host bots of other products, so commands, callback data and the
hashtag #stickers_<id> all carry it.
Sequence
Section titled “Sequence”sequenceDiagram
autonumber
participant U as Group member
participant T as Telegram
participant B as Bot leads router
participant D as SQLite
U->>T: Press a status button
T->>B: callback_query with stickers_lead data
alt user id not in LEADS_ADMIN_IDS
B-->>U: Pop-up No access
else callback data malformed
B-->>U: Pop-up asking to refresh the card
else lead missing or anonymised
B-->>U: Pop-up with the reason
else allowed
B->>D: BEGIN IMMEDIATE, compare and update status, insert history row
alt status unchanged
B-->>U: Toast Already so
else status changed
B->>T: editMessageText with new card and keyboard
alt edit accepted or not modified
B-->>U: Toast Done
else edit failed
B-->>U: Pop-up Saved, card unavailable
end
end
end
Steps, tied to code
Section titled “Steps, tied to code”All in on_status in
handlers/leads.py.
- Routing. The leads router is registered only when the module is enabled (
LEADS_ENABLED=1andLEADS_CHAT_IDset). Its callback filter ismessage.chat.id == LEADS_CHAT_ID, and the handler matches data starting withstickers_lead:. - Access.
cb.from_user.idmust be inLEADS_ADMIN_IDS. Otherwise the log getsLEAD_STATUS_DENIEDand the user getsНет доступаas an alert. This check runs before the data is parsed. - Parse.
leads.parse_callbackrequires exactly three parts, the right prefix, a known status other thannew, and a numeric id. A malformed value givesLEAD_STATUS_BAD_BUTTONand the alertНе понял кнопку — обнови карточку: /stickers_lead <номер>(“Did not understand the button — refresh the card”). - Lead lookup. A missing lead gives
Заявки #<id> нет в базе(“No such lead”). An anonymised lead givesДанные заявки удалены — статус не меняется(“Lead data was erased — the status does not change”). - Write.
db.lead_set_statusopensBEGIN IMMEDIATE, compares the stored status with the new one and, if they differ, updatesleads.status,status_by,status_by_name,status_atand inserts alead_status_historyrow. Two admins pressing different buttons at the same moment produce two history rows in press order instead of one overwriting the other. - Same status. No history row; toast
Уже так: <label>(“Already so”); logLEAD_STATUS_SAME. - Card edit. The card is rebuilt from the database (
leads_bot.card_for): the first icon changes and the status block lists the last 3 changes with the admin’s name and the time in Tashkent. LogLEAD_STATUS #<id> -> <status> by user=<id>. - Answer. Toast
Готово: <label>(“Done”).
Card unavailable
Section titled “Card unavailable”If editMessageText fails with anything other than “message is not modified”, the handler logs
LEAD_CARD_EDIT_FAILED and answers with the alert
Сохранено: <label>. Карточка недоступна — /stickers_lead <id> (“Saved. The card is unavailable”). The status and
the history row are already committed.
/stickers_lead <id> posts the card again as a new message and stores the new chat_id / message_id
(db.lead_card_moved). Later reminders and repeat pings reply to that fresh card. The old message keeps its old
text.
Fallback router
Section titled “Fallback router”register_fallback adds a last router that matches any callback starting with stickers_lead: or
stickers_pick:. It fires when the leads router did not take the callback:
- the module is switched off (
LEADS_ENABLED=0, orLEADS_CHAT_IDnot set); - the card lives in a chat other than the configured
LEADS_CHAT_ID.
It logs LEAD_BUTTON_ORPHAN and 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”). Without this router Telegram would show an endless spinner on the button.
Other buttons in the same router
Section titled “Other buttons in the same router”/stickers_lead and /stickers_forget without a number show the last 5 leads as buttons with callback data
stickers_pick:<action>:<lead_id>, where the action is lead, forget or forgettest. The same access check
applies (LEAD_PICK_DENIED). After a forget action the picker message is rewritten without buttons, so a second
press cannot erase anything again.
Isolation from the guest flow
Section titled “Isolation from the guest flow”- Guest routers (
start,consent,maintenance,admin) filter on private chats, so nothing a group member types reaches the guest flow. - The maintenance middleware lets every non-private event through. Status buttons and
/stickers_*commands work while generation is paused, including the automatic pause after a 402. /stickers_*commands are accepted only in the leads group and only fromLEADS_ADMIN_IDS; for anyone else the bot stays silent.
Where to go next
Section titled “Where to go next”- Lead submission — how the card got there.
- Reminders and digest — the ping for leads left without a status.
- Bot commands — the
/stickers_*commands. - Lead process — the team’s working rules for statuses.
- Security — who can see what in the group.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.