A community based topic aggregation platform built on atproto

feat(adminreports): alert an operator on Telegram when a report is filed master

SubmitReport wrote a row to admin_reports and stopped there. No email, no notification of any kind — so a CSAM report sat in a table nobody watches while the response clock ran, and a test report filed against production produced silence that was indistinguishable from a quiet week. Adds an alert channel behind a domain port: adminreports.Notifier what the service calls, channel-agnostic adminreports.MessageSender narrow transport seam (one plain-text blob) internal/notify/telegram Bot API adapter, imports nothing from the domain Telegram rather than email for the alerting path: no SPF/DKIM, no spam folder, no domain reputation to maintain, and it reaches a phone in seconds. Email remains the better channel for a durable record and can be added as a second Notifier without touching this package. Off unless TELEGRAM_ALERTS_ENABLED is set — a self-hosted Coves must not need a Telegram account to boot. Enabled-but-incomplete fails startup rather than degrading to "alerts off", since a silently disabled alerter is the exact fault this removes. Decisions that look like bugs from the outside: - Delivery is synchronous. A detached goroutine drops the alert whenever the process exits first, and losing a CSAM alert to a routine deploy is worse than making one reporter wait. Bounded by maxConcurrentAlerts, because the route's rate limiter keys on client IP (middleware.GetClientIP), so N source addresses buy N independent budgets and nothing upstream caps this. - The alert context is detached from the request's cancellation. The row is committed; a reporter closing the app must not also cancel the alert. - Nothing in the alert path can fail the submission — errors and panics alike. Answering an error would tell reporters their report failed when it did not, and invite a retry that files a duplicate. - The alert withholds the reporter DID and the explanation text. Both cross third-party infrastructure, and the explanation quotes the reported content. The report ID leads so an operator can retrieve them from Postgres. - disable_web_page_preview and the absent parse_mode are safety requirements, not cosmetics: the first stops Telegram fetching a reported URI server-side, the second stops a URI being reinterpreted as markup. - The bot token sits in the request path, so net/http embeds it in *url.Error. Client.scrub flattens errors to redact it rather than wrapping, because errors.As on the original hands back a URL that still holds the credential. Target URIs are truncated and the message clamped: atURIPattern is unbounded and only the explanation is length-checked, so an oversized but pattern-valid URI would push the alert past the channel's size limit — handing a reporter the ability to suppress the alert about their own report. Tests pin each claimed invariant at the boundary that enforces it, verified by mutation: dropping the status-code gate, renaming the disable_web_page_preview tag, and appending the explanation to the sent message each turn a test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>