-
Notifications
You must be signed in to change notification settings - Fork 46
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 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.
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 againAll three raise an exception on an unknown id, so a typo fails loudly instead of doing nothing.
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:
- it carries the ban, and the retained id is what stops the deleted data from coming back — without
it the next
/growwould simply recreate the user; - it keeps the foreign keys of
Loans,Dick_of_Day,Promo_Code_ActivationsandStale_Dick_Shrinksvalid, none of which haveON 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.
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.
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.
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.