Reminders, digest and scheduled anonymisation
Who is this for
Section titled “Who is this for”Team members who receive the pings and the morning digest, and developers who change the schedule.
- One loop in the bot process publishes cards every 2 seconds and runs housekeeping once a minute.
- Reminder: a lead that stays
newfor more than 2 hours gets one ping, as a reply to its card. Pings go out only Monday to Friday, 09:00–21:00 Tashkent time. There is never a second ping. - Digest: every day at 10:00 Tashkent time, if there is anything to report. If the bot was down at 10:00 it catches up until 12:00; after that the day is skipped.
- Anonymisation: spam leads lose their personal data 30 days after the last status change; all other leads after 365 days.
- Each of the three steps is isolated: an error in one is logged and the others still run.
The loop
Section titled “The loop”sequenceDiagram
autonumber
participant L as Leads loop in the bot
participant D as SQLite
participant T as Leads group
loop every 2 s until the bot stops
L->>D: publish due cards, notify due repeats
opt 60 s since the last housekeeping
L->>D: new leads older than 2 h without a ping
opt weekday 09 to 21 Tashkent
L->>T: Reminder as a reply to the card
L->>D: reminded_at
end
L->>D: last digest day from settings
opt hour is 10 or 11 Tashkent and no digest today
L->>T: Digest, only if a number is not zero
L->>D: store today as last digest day
end
L->>D: leads past their retention period
L->>T: Rewrite each expired card to data erased
end
end
run_outbox in
leads_bot.py owns the loop.
HOUSEKEEP_SEC is 60. The first housekeeping pass runs right after the bot starts. Time rules are pure functions in
services/leads_schedule.py,
tested with frozen time in
tests/test_leads_schedule.py
and
tests/test_leads_housekeeping.py.
All times use a fixed UTC+5 offset (TASHKENT in services/leads.py); there is no daylight-saving change.
Reminder
Section titled “Reminder”| Rule | Value | Code |
|---|---|---|
| Lead is eligible | status new, not anonymised, card published, reminded_at empty, created at least 2 hours ago |
db.leads_for_reminder, REMIND_AFTER |
| Allowed time | Monday–Friday, hour from 9 up to but not including 21, Tashkent | is_work_time, WORK_DAYS, WORK_HOURS |
| Text | ⏰ #stickers_<id> без статуса уже <N> ч — кто возьмёт? (“no status for N h — who takes it?”) |
reminder_text |
| Where | Reply to the card (allow_sending_without_reply), or a plain message if the card is gone |
send_reminders |
| How many | One per lead, ever | db.lead_reminded |
Steps:
send_remindersreturns at once outside working hours. Nothing is queued; eligibility is recomputed each minute.- A lead whose 2 hours ran out at night is pinged at 09:00. One from Friday evening is pinged on Monday at 09:00.
- For each eligible lead the bot sends the ping and then sets
reminded_at. reminded_atis set even if sending failed. The failure is logged asLEAD_REMIND #<id> не отправлен(“not sent”) and the ping is not retried. This is deliberate: one failed ping must not repeat every minute.- The hours in the text are whole hours since
created_at, so a lead from Friday evening shows a large number on Monday.
Changing a lead’s status before the ping is due removes it from the eligible set.
Digest
Section titled “Digest”| Rule | Value | Code |
|---|---|---|
| Send window | Tashkent hour is 10 or 11 (DIGEST_AT 10, DIGEST_LATE_UNTIL 12) |
digest_day |
| Once per day | The last sent day is stored in settings under stickers_digest_last |
send_digest |
| Period covered | 10:00 yesterday to 10:00 today, Tashkent | digest_window |
| Days | Every day, weekends included | — |
| Empty digest | Not sent, but the day is still marked as done | digest_text returns None |
Content:
☀️ Стикеры за сутки (<dd.mm 10:00> — <dd.mm 10:00>)Новых заявок: <n>Без статуса: <n> — #stickers_<id>, #stickers_<id> и ещё <k>Паков сгенерировано: <n>(“Stickers in the last 24 h / New leads / Without a status / Packs generated”.)
- New leads are counted by
created_atinside the window. - Without a status is the current number of
new, non-anonymised leads, whatever their age. Up to 10 hashtags are listed, oldest first. - Packs generated counts jobs by
pack_created_atinside the window, excluding jobs of users listed inADMIN_USER_IDS(their test packs).
Failure behaviour: if sending the digest raises an error, the day is not marked, the error is logged as
LEADS: ошибка шага «дайджест» (“step error: digest”), and the step runs again a minute later. Retries stop at
12:00. A bot that starts at 11:30 still sends the digest; one that starts at 12:05 does not.
Scheduled anonymisation
Section titled “Scheduled anonymisation”| Lead status | Personal data is removed after | Clock starts at |
|---|---|---|
spam |
30 days (SPAM_KEEP) |
Last status change (status_at), or creation if the status never changed |
| Any status | 365 days (LEAD_KEEP) |
Same |
erase_expired selects the leads with db.leads_to_erase and calls erase_lead for each:
db.lead_erasesetsname,telegram,phone,commentandip_hashtoNULLand stampserased_at. The same personal fields are cleared inlead_repeats.- Status, language, UTM values, event date, guest count, timestamps and the status history stay, so statistics do not change.
- The card in the group is rewritten to
🗑 Заявка #stickers_<id> — данные удалены (<date>)(“data erased”). If the card cannot be edited the failure is only logged; the database is already clean. - Log line:
LEAD_ERASED #<id> по сроку (статус <status>)(“by retention period”).
A status change restarts the clock, because the rule uses the last change. Erasure on request is described in Data erasure.
Failure isolation
Section titled “Failure isolation”housekeepingwraps each step (пинги,дайджест,сроки хранения— pings, digest, retention) in its owntry. An exception is logged and the next step runs.run_outboxwraps the whole pass. The loop must never end: inmain.servethe end of any task stops the whole bot, including the guest flow.- The loop does not run when the module is disabled (
LEADS_ENABLED=0or noLEADS_CHAT_ID).
Where to go next
Section titled “Where to go next”- Lead submission — the publishing half of the same loop.
- Lead status change — how a lead leaves the
newstate. - Data erasure — erasure on request and what backups still hold.
- Privacy — the full retention picture.
- Lead process — the team’s response-time expectations.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.