Skip to content

Add /support with data deletion, Telegram API metrics, and a shared test container - #148

Closed
kozalosev wants to merge 6 commits into
mainfrom
feature/support-and-bans
Closed

Add /support with data deletion, Telegram API metrics, and a shared test container#148
kozalosev wants to merge 6 commits into
mainfrom
feature/support-and-bans

Conversation

@kozalosev

Copy link
Copy Markdown
Owner

Six independent changes that accumulated on one branch: one user-facing feature, two observability improvements, two fixes and a test-infrastructure change.

/support, and erasing a user's data (7b19ec1)

#19 asks for a way to leave a chat's rating, with a GDPR argument attached. An erasure option is a real obligation, but nothing requires it to be self-service — so the bot gets a contact channel and the owner answers a request by hand. This does not close #19: the per-chat opt-out it actually asks for is still open, and needs its own anti-abuse rule.

  • /support relays a message to SUPPORT_CHAT_ID without exposing an email or a personal account. Two-step dialogue like /promo, private chats only, one message per user per minute, hidden from the menu when the variable is unset.
  • Migrations 34–36: Users.banned_until, the erase_user / ban_user / unban_user functions for the owner to call from a DB client, and a trigger enforcing the ban.
  • erase_user deletes every row a user owns but keeps the Users row — it carries the ban, and keeping it means none of the four foreign keys without ON DELETE CASCADE is violated. The retained id is what stops the deleted data coming back.
  • The ban is 90 days rather than permanent, so exercising the right to erasure carries no lasting penalty while still removing the incentive to use it as a progress reset.
  • Two layers enforce it: an in-memory list refreshed every BAN_LIST_REFRESH_SECS and on SIGHUP, and — because that list can be stale — a BEFORE UPDATE trigger on Users that is the actual guarantee.

Details, including why the trigger sits on Users and not Dicks, are in the wiki page.

Observability

51c87a5 — unique names for instrumented functions. The autometrics function label and the #[tracing::instrument] span name are both the bare Rust function name, so six callback_handlers, four cmd_handlers and three gets were indistinguishable in the Grafana latency panel and in traces. autometrics 3.0 offers no way to override the label, so the names themselves changed. Old series stop and new ones start from zero — the panels will show a break at the deploy.

4800220 — measure the Telegram API calls. Failed requests were counted but never timed, so a Telegram or DPI slowdown was invisible until it became an outright failure, and the BOT_HTTP_*_TIMEOUT_SECS values had to be picked blindly. Adds telegram_request_duration_seconds{method,outcome} (failures included, so a timeout is in the histogram rather than missing from it), a client span per call, and — on ApiError::Unknown — the serialized request body, which is the only way to learn which entity or over-long text was rejected.

Fixes

d11be57#92: can't parse entities: Unsupported start tag. The Dick of the Day "already chosen" message interpolated the winner's name straight from the SQL exception without escaping it, so a name containing < broke the whole message. Now escaped through Username::escaped(), with a test covering <, & and >.

3dba01aget_me() was requested twice at startup.

Tests

2470af5 — one shared Postgres container. Every test used to start a throwaway container: 132 per run, and most of the suite's time. Now the binary starts one and each test takes a database out of it.

before  ~75s
after   ~35s   (132 passing, cold and warm)

Three things make it work, and each is load-bearing — CLAUDE.md records them along with two approaches that were tried and failed, so this doesn't get "simplified" back:

  • the container and its maintenance pool live on a static runtime, because a #[tokio::test] runtime is torn down at the end of each test and a sqlx pool dies with it;
  • migrations run once into a template database that each test copies;
  • the container is marked reusable and outlives the run, so the next run finds it. task test:clean is now the only thing that removes it.

On CI nothing matches on a fresh runner, so it starts one and carries on — the cold path, still ~2x faster than before.

Deploying

  • Migrations 34–36 apply automatically at startup. After that they must not be edited.
  • New environment variablesSUPPORT_CHAT_ID (unset ⇒ /support hidden and inert) and BAN_LIST_REFRESH_SECS (default 900). Both still need adding to the server-configs repo — DickGrowerBot/docker-compose.yml and the value in .env.sops — or /support stays hidden in production.
  • Grafana — the renamed functions in 51c87a5 and the new telegram_request_* metrics in 4800220 both touch the dashboard in server-configs.

🤖 Generated with Claude Code

kozalosev and others added 6 commits August 3, 2026 05:20
The `function` label of autometrics and the span name of `#[tracing::instrument]`
are both the bare Rust function name, so six `callback_handler`s, four
`cmd_handler`s and three `get`s were indistinguishable in the Grafana handler
latency panel and in the traces. The macro of autometrics 3.0 takes only
`track_concurrency`, `ok_if`, `error_if` and `objective` — there is no way to
override the label — so the names themselves had to change.

The handlers get their module as a prefix, the way `dick_cmd_handler` and its
neighbours already do; the repositories name the entity they work with, next to
the `get_chat` and `get_active_loan` that were there before.

The old series stop and the new ones start from zero, so the panels show a break
at the deploy; the history stays queryable under the old names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gY171uE8o7opVNsYb7rHr
Issue #19 asks for a way to remove oneself from a chat's rating, with a
GDPR argument attached. An erasure option is a real obligation, but
nothing requires it to be self-service — so the bot gets a contact
channel, and the owner answers a request by hand.

/support relays a message to the chat set in SUPPORT_CHAT_ID, without
exposing an email or a personal account. It reuses the two-step dialogue
of /promo, works in private chats only, and is hidden from the menu when
the variable is unset.

Migration 34 adds Users.banned_until; migration 35 adds erase_user,
ban_user and unban_user for the owner to call from a DB client. The Users
row always survives an erasure: it carries the ban, and keeping it means
no foreign key from Loans, Dick_of_Day, Promo_Code_Activations or
Stale_Dick_Shrinks is ever violated. The retained id is what stops the
deleted data from coming back.

Two layers enforce the ban. bans::BanList keeps the (tiny) list in memory
and re-reads it every BAN_LIST_REFRESH_SECS and on SIGHUP; the gate in
main.rs sits above every branch that could write a row for the sender,
which is why /start and /language moved below it and only /help,
/privacy and /support stay above. Because that list can be stale,
migration 36 adds the actual guarantee: a trigger on Users refusing any
statement that touches a banned user's row without changing banned_until.

The ban is 90 days rather than permanent, so that exercising the right to
erasure carries no lasting penalty, while still removing the incentive to
use it as a progress reset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3qG7U7xVh84ahKs3egJiU
The bot counted the requests that failed but never measured how long one
took, so a Telegram (or DPI) slowdown was invisible unless it turned into
an outright failure — and the `BOT_HTTP_*_TIMEOUT_SECS` values had to be
picked blindly. A rejected request was just as opaque: `ApiError::Unknown`
says the payload was disliked, never which part of it, and the one place
that logged the text was a helper wired into two inline call sites by
hand, with a TODO asking for something general.

Both come from the same missing hook, so both are answered by one:
`TelegramObserver` implements the `RequestObserver` our teloxide fork
adds to `Bot`, and every request now yields a
`telegram_request_duration_seconds{method,outcome}` sample, a client span
of its own in the trace, and — when the API rejects it — an error record
carrying the body that was sent. Failures are measured too, so a timeout
lands in the histogram instead of going missing from it. The outcome
label comes from the same `classify` the error counters use, so the two
metrics cannot disagree.

The observer sits below the adaptors and sees every call site, which is
what lets the inline helper and its TODO go.

Switching the dependency to the fork branch also moves us onto TBA 9.2,
hence the new field in the chat fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every test used to start a throwaway Postgres of its own — 132 containers
per run, and most of the suite's time. Now the binary starts one and each
test takes a database out of it: ~35s instead of ~75s.

Three things make it work, and each is load-bearing.

A runtime of its own. A #[tokio::test] builds a runtime and tears it down
when the test ends, and a sqlx pool dies with the runtime that created
it, so a shared pool breaks with "a Tokio 1.x context was found, but it
is being shutdown" as soon as the first test finishes. The container and
its maintenance pool therefore live on a static RUNTIME, reached with
spawn — block_on would panic, being called from inside a runtime already.
The per-test pool is still built on the test's own runtime and dies with
it, which is what we want.

A template database. The migrations run once per run into test_template
and every test's database is a copy of it. Replaying them per test made
CREATE DATABASE the new bottleneck and left the suite slower than before.

A reused container. It outlives the run on purpose, so the next run finds
it instead of paying the startup again; `task test:clean` is now the only
thing that removes it. Its databases are named test_run<pid>_<n> and the
ones left by earlier runs are dropped at startup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3qG7U7xVh84ahKs3egJiU
@kozalosev kozalosev changed the title C:/Program Files/Git/support and data deletion, Telegram API metrics, unique instrumented names, and a shared test container Add /support with data deletion, Telegram API metrics, and a shared test container Aug 3, 2026
@kozalosev kozalosev closed this Aug 3, 2026
@kozalosev
kozalosev deleted the feature/support-and-bans branch August 3, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability for user to remove himself from specific chat rating

1 participant