Add /support with data deletion, Telegram API metrics, and a shared test container - #149
Merged
Merged
Conversation
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
This was
linked to
issues
Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
/supportrelays a message toSUPPORT_CHAT_IDwithout 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.Users.banned_until, theerase_user/ban_user/unban_userfunctions for the owner to call from a DB client, and a trigger enforcing the ban.erase_userdeletes every row a user owns but keeps theUsersrow — it carries the ban, and keeping it means none of the four foreign keys withoutON DELETE CASCADEis violated. The retained id is what stops the deleted data coming back.BAN_LIST_REFRESH_SECSand on SIGHUP, and — because that list can be stale — aBEFORE UPDATEtrigger onUsersthat is the actual guarantee.Details, including why the trigger sits on
Usersand notDicks, are in the wiki page.Observability
51c87a5— unique names for instrumented functions. The autometricsfunctionlabel and the#[tracing::instrument]span name are both the bare Rust function name, so sixcallback_handlers, fourcmd_handlers and threegets 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 theBOT_HTTP_*_TIMEOUT_SECSvalues had to be picked blindly. Addstelegram_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 — onApiError::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 throughUsername::escaped(), with a test covering<,&and>.3dba01a—get_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.Three things make it work, and each is load-bearing —
CLAUDE.mdrecords them along with two approaches that were tried and failed, so this doesn't get "simplified" back:staticruntime, because a#[tokio::test]runtime is torn down at the end of each test and a sqlx pool dies with it;task test:cleanis 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
SUPPORT_CHAT_ID(unset ⇒/supporthidden and inert) andBAN_LIST_REFRESH_SECS(default 900). Both still need adding to the server-configs repo —DickGrowerBot/docker-compose.ymland the value in.env.sops— or/supportstays hidden in production.51c87a5and the newtelegram_request_*metrics in4800220both touch the dashboard in server-configs.🤖 Generated with Claude Code