Skip to content

Pack delivery

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 createNewStickerSet call, 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: getStickerSet returns exactly 12 stickers and the public page t.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 UPDATE statements guarantee that a job sends either the link or the failure text, never both, and each at most once.
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
  1. Number. telegram_stickers.next_pack_number takes every pack name the guest ever had — jobs.pack_name, users.pack_name / pack_link, and names kept in audit_log after an admin reset — and returns the next number. Gaps are not filled.
  2. Title. worker.pack_title_for asks Telegram for the guest’s current username (getChat), falls back to the profile saved at /start, and builds AI стикер пак @<username> (“AI sticker pack”). Without a username it uses the first name; without a name just AI стикер пак. The title is cut to 64 UTF-16 units.
  3. Create. telegram_stickers.create_pack in telegram_stickers.py tries pack<N>, pack<N+1> … up to 50 names (MAX_NAME_ATTEMPTS). A name is skipped when getStickerSet finds it or when createNewStickerSet answers “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 from getMe.
  4. Hand-off. Back in worker._process_one_job the selfie file is deleted and db.start_delivery moves the job processing → delivering with pack_name, pack_created_at and provider_used. If the job was already failed by the deadline, the transition does not happen and no link is sent.
  5. Verify. delivery.verify_and_deliver in delivery.py loops over telegram_stickers.check_pack_ready:
    • (a) getStickerSet must return exactly stickers_per_pack (12) stickers;
    • (b) the page https://t.me/addstickers/<name> must contain og:title of the form Add sticker set <title> on Telegram. The page always answers HTTP 200; an absent pack has the title Add 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”.
  6. Send exactly once. db.mark_link_sent runs one transaction: delivering → done, link_sent_at, clears appearance_json, and upserts users.pack_name / pack_link. Only the caller that gets True sends Готово! 🎉 Вот твой персональный набор стикеров: (“Done! Here is your personal sticker pack”) with the link. The log line is STICKERSET_CONFIRMED.
  7. Sweeper. delivery.run_sweeper runs every 20 s (SWEEP_INTERVAL_SEC) in the bot process and enforces the deadlines below. It also restarts the check for any delivering job that has no live task, which is how delivery survives a bot restart.
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.

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.

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

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.


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