Skip to content

Support requests and data deletion

Leonid Kozarin edited this page Aug 3, 2026 · 1 revision

Support requests and data deletion

How a player reaches the bot's owner, and how the owner answers a request to delete personal data.

/support

/support lets a user write to the owner without exposing an email address or a personal account: the bot relays the message to the chat set in SUPPORT_CHAT_ID. A group works best — you can reply from it and add other people later. Unset that variable and the command is hidden from the menu and does nothing.

The message carries the sender's id, name, language and a tg://user?id=… link, so you can open their profile in one tap and answer in a private chat.

/support works in private chats only, and there is one message per user per minute.

The privacy policy points at /support as the way to ask for a copy of one's data or for its deletion.

Answering a deletion request

There is deliberately no self-service delete button, and there should not be one: nothing in the GDPR requires the erasure to be self-service, and a button is an easy way to wipe a leaderboard by accident. A request is answered by hand, with the SQL functions of migrations/35_erase-user-function.sql:

SELECT erase_user(123456789);      -- delete everything the user owns, block them for 90 days
SELECT erase_user(123456789, 30);  -- the same, but for 30 days
SELECT ban_user(123456789, 7);     -- block only, keep the data
SELECT unban_user(123456789);      -- let the user play again

All three raise an exception on an unknown id, so a typo fails loudly instead of doing nothing.

What erase_user leaves behind

It deletes every row the user owns — Dicks, Battle_Stats, Loans, Dick_of_Day, Promo_Code_Activations, Stale_Dick_Shrinks, Imports — and keeps the Users row with an empty name, a reset created_at and a banned_until in the future.

The row has to stay for two reasons:

  1. it carries the ban, and the retained id is what stops the deleted data from coming back — without it the next /grow would simply recreate the user;
  2. it keeps the foreign keys of Loans, Dick_of_Day, Promo_Code_Activations and Stale_Dick_Shrinks valid, none of which have ON DELETE CASCADE.

Resetting created_at drops the original registration date and gives a returning user a fresh grace period.

Keeping only the id is a suppression list, which is a recognised practice: it stores the minimum needed to honour the request, and it is not the processing the person objected to. The privacy policy says out loud that the id is kept and for how long.

Why the ban is 90 days and not forever

A permanent ban would attach a lasting penalty to the exercise of a legal right, which works against the fairness principle of Art. 5(1)(a) GDPR. A bounded ban still removes the incentive to use the erasure as a "reset my bad score" trick — waiting three months is a worse deal than living with the score.

How the ban is enforced

Two layers, and only the second one is a guarantee.

In memory. bans::BanList keeps the whole (tiny) list of active bans and re-reads it every BAN_LIST_REFRESH_SECS (900 — 15 minutes — by default), because a ban is written straight into the database and nothing else tells the bot about it. On Linux, SIGHUP applies a ban at once (docker kill -s HUP dickgrowerbot, the same signal that reloads the announcements). A DB error keeps the previous list rather than locking everybody out.

checks::reject_banned_users() sits in the middle of the dispatcher tree in main.rs. The order there is load-bearing: nothing above the gate may write a row for its sender. Only /help, /privacy and /support qualify — a banned user must still be able to read the policy and reach the owner. /start does not qualify (a promo deeplink activates a code) and neither does /language (it writes to user-service).

In the database. The list above is a convenience, not the guard. migrations/36_forbid-updates-of-banned-users.sql adds a BEFORE UPDATE trigger on Users that refuses any statement touching a banned user's row, unless that statement changes banned_until itself — which is exactly what erase_user, ban_user and unban_user do, and what create_or_update does not. So even with a stale list the database will not take a deleted user back; the user just gets the ban notice a little later.

The trigger sits on Users rather than Dicks because the check is free there (banned_until is on the very row being updated, so there is nothing to look up), Users has exactly one production write and nothing bulk, and a trigger on Dicks would fire during the chat merge's bulk insert and abort the whole merge.

The exception carries SQLSTATE GD3E1 with the ban's end date as its message; both call sites of create_or_update turn it back into the normal "you can't play until …" notice.

What none of this can stop

A new Telegram account has a different id, and to Telegram — and therefore to us — that is a different person. No database rule sees through it, and it is not worth trying to.