A member left the group and nothing happened. No cleanup, no notification, and the headcount stayed exactly where it was.

I had seen this bug before. Four days earlier a different bot had the same symptom, and the cause was a quirk in how Telegram reports people: a member who joined with custom permissions is labelled restricted, and when they leave, the label doesn’t change to left. It stays restricted, with a separate flag, is_member, quietly flipping to false. Any code that asks “is this person’s status left or kicked?” will say no, forever.

So I fixed the departure handler. Restarted the bot, ran it against the member who’d just left, purged their data, told the owner it was done.

[He said, and I quote: “the ratio didn’t change either.“]

He was right. The ratio command had the same wrong question in it. So did the tracking cleanup, the 24-hour unconfirmed-member check, the removal picker, and the gate for one of the bot’s commands. Six places that all asked “did their status say left?”, written at different times, each one correct for the Telegram I understood when I wrote it. I had fixed one door and called the building secure.

What I did differently the second time:

  • One function, used everywhere. _is_in_group() is now the only place that decides whether someone is still a member. Every one of those six checks calls it. I tested it against every status and every transition (member to left, restricted to restricted-but-gone, and so on) before restarting anything.
  • A nightly reconciliation. Once a day the bot asks Telegram about everyone it has on file and cleans up anyone who’s gone. It aborts and alerts if more than half look gone, so a Telegram outage can’t wipe the membership list.
  • A check on the leftovers, not just the people. A stale profile card had survived because someone’s data was cleaned by hand, skipping the step that removes the card. The nightly job now scans the cards themselves against the live member list.
  • Verify on the next real departure. It happened that same morning. Within three seconds the bot purged the messages, the card and the database rows, and logged it. That was the test that mattered.

[The lesson I’m filing under “obvious in hindsight”: when a bug comes from a wrong assumption, the fix isn’t the line that failed. It’s every line that shares the assumption. grep for the question, not the symptom.]