Pack delivery
Who is this for
Section titled “Who is this for”Developers and operators who investigate “the guest did not get the link” or change anything around
createNewStickerSet.
- All 12 stickers go to Telegram in one
createNewStickerSetcall, so a pack is never half built. - Pack name:
pack<N>_<telegram_user_id>_by_<bot_username>. A pack is never deleted or recreated under the same name. - The guest gets the link only after a double check:
getStickerSetreturns exactly 12 stickers and the public paget.me/addstickers/<name>confirms the pack twice in a row. - Checks run every 2 s for the first 90 s after pack creation, then every 10 s. They do not occupy a worker slot.
- Deadlines count from job creation and live in the database: 5 minutes without a link → one “still working” message; 15 minutes → the job fails and the guest is told to come to the station.
- Conditional
UPDATEstatements guarantee that a job sends either the link or the failure text, never both, and each at most once.
Sequence
Section titled “Sequence”sequenceDiagram
autonumber
participant W as Worker
participant D as SQLite
participant T as Telegram Bot API
participant P as t.me page
participant V as Delivery task
participant S as Sweeper
participant G as Guest
W->>T: createNewStickerSet, 12 stickers
T-->>W: ok, or name occupied then next number
W->>D: processing to delivering, pack_name, pack_created_at
W->>V: spawn check for the job
loop every 2 s for 90 s, then every 10 s
V->>T: getStickerSet
alt 12 stickers
V->>P: GET addstickers page
opt first answer says present
Note over V: wait 1.75 s
V->>P: GET addstickers page again
end
end
end
V->>D: delivering to done, link_sent_at, users.pack_name
V->>G: Pack link
loop every 20 s
S->>D: jobs older than 15 min without a link
S->>D: jobs older than 5 min without a notice
S->>V: respawn checks for delivering jobs without a task
end
Steps, tied to code
Section titled “Steps, tied to code”- Number.
telegram_stickers.next_pack_numbertakes every pack name the guest ever had —jobs.pack_name,users.pack_name/pack_link, and names kept inaudit_logafter an admin reset — and returns the next number. Gaps are not filled. - Title.
worker.pack_title_forasks Telegram for the guest’s current username (getChat), falls back to the profile saved at/start, and buildsAI стикер пак @<username>(“AI sticker pack”). Without a username it uses the first name; without a name justAI стикер пак. The title is cut to 64 UTF-16 units. - Create.
telegram_stickers.create_packintelegram_stickers.pytriespack<N>,pack<N+1>… up to 50 names (MAX_NAME_ATTEMPTS). A name is skipped whengetStickerSetfinds it or whencreateNewStickerSetanswers “occupied”. The pre-check is skipped only for a guest’s first pack on a fresh job. The_by_<bot>suffix uses the bot’s real username fromgetMe. - Hand-off. Back in
worker._process_one_jobthe selfie file is deleted anddb.start_deliverymoves the jobprocessing → deliveringwithpack_name,pack_created_atandprovider_used. If the job was already failed by the deadline, the transition does not happen and no link is sent. - Verify.
delivery.verify_and_deliverindelivery.pyloops overtelegram_stickers.check_pack_ready:- (a)
getStickerSetmust return exactlystickers_per_pack(12) stickers; - (b) the page
https://t.me/addstickers/<name>must containog:titleof the formAdd sticker set <title> on Telegram. The page always answers HTTP 200; an absent pack has the titleAdd sticker set on Telegram. A network error, a non-200 answer or any other title counts as “unknown”, which is not “present”; - (b) is checked a second time after 1.75 s (
TME_RECHECK_PAUSE_SEC). Both must say “present”.
- (a)
- Send exactly once.
db.mark_link_sentruns one transaction:delivering → done,link_sent_at, clearsappearance_json, and upsertsusers.pack_name/pack_link. Only the caller that getsTruesendsГотово! 🎉 Вот твой персональный набор стикеров:(“Done! Here is your personal sticker pack”) with the link. The log line isSTICKERSET_CONFIRMED. - Sweeper.
delivery.run_sweeperruns every 20 s (SWEEP_INTERVAL_SEC) in the bot process and enforces the deadlines below. It also restarts the check for anydeliveringjob that has no live task, which is how delivery survives a bot restart.
Timing
Section titled “Timing”| Constant | Value | Meaning |
|---|---|---|
FAST_POLL_SEC |
2 s | Poll interval during the fast phase |
FAST_PHASE_SEC |
90 s | Length of the fast phase, counted from pack_created_at |
SLOW_POLL_SEC |
10 s | Poll interval afterwards |
TME_TIMEOUT_SEC |
10 s | Timeout of one t.me page request |
TME_RECHECK_PAUSE_SEC |
1.75 s | Pause between the two page checks |
DELAY_NOTICE_AFTER_SEC |
5 min | From jobs.created_at |
DELIVERY_DEADLINE_SEC |
15 min | From jobs.created_at |
SWEEP_INTERVAL_SEC |
20 s | Sweeper period |
SEND_ATTEMPTS / SEND_RETRY_SEC |
3 / 2 s | Attempts to send a message to the guest |
jobs.created_at is the moment the photo was accepted, so the deadlines cover queue time, generation and
verification together. An admin Retry resets created_at, which restarts both clocks.
Deadlines
Section titled “Deadlines”| After | Condition | Action | Guard |
|---|---|---|---|
| 5 minutes | Job is queued, processing or delivering, no link sent, no notice yet |
Guest: Дорабатываем детали твоего набора ✨ Скоро пришлю! (“Finishing the details, will send soon”). Admin alert. Log DELAY_NOTICE |
db.claim_delay_notice sets delay_notice_sent_at only once |
| 15 minutes | Same, still no link | Job failed, error STICKERSET_DELIVERY_FAILED: …. Guest: Что-то пошло не так… (“Something went wrong… we will help manually”). Admin alert. The check task is cancelled |
db.fail_undelivered_job succeeds only once and only if link_sent_at is empty |
The sweeper handles the 15-minute case before the 5-minute case, so a job that sat through a long outage does not get
“still working” followed by a failure. The verification loop applies the same 15-minute rule itself; whichever
component gets there first wins the conditional UPDATE.
Failure branches
Section titled “Failure branches”| Situation | Result |
|---|---|
| All 50 candidate names are occupied | PackNameError; the job attempt fails and follows the job retry rules |
createNewStickerSet fails for another reason |
Job attempt fails. A later attempt checks name occupancy first, because the earlier call may have succeeded on Telegram’s side |
Bot API reports the pack but t.me does not |
Polling continues until the 15-minute deadline |
Bot restarts while a job is delivering |
Sweeper respawns the check within 20 s of start |
| Worker finishes generation after the deadline already failed the job | No pack is created (снят по сроку in the log — “removed by deadline”) |
Pack was created but the job was failed by the deadline before start_delivery |
The pack exists in Telegram, no link is sent. The name is in the log |
| The link message cannot be delivered after 3 attempts (guest blocked the bot, network) | The job stays done. The link is not re-sent automatically; it is visible in the admin panel |
Why packs are never recreated
Section titled “Why packs are never recreated”The module comment in telegram_stickers.py records the reason: Telegram clients cache the state of a pack name, and
a pack recreated under the same name could open as “Sticker set not found” for the guest. A new pack therefore always
gets a new number, and the stored pack_name is the only source for building the link.
Where to go next
Section titled “Where to go next”- Generation — what happens before the pack is created.
- State machines — job statuses and who may change them.
- Runbook: pack not delivered — operator steps for this flow.
- Monitoring and alerts — the alerts named here.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.