You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Once the core ban plumbing lands (#150, #151, WXYC/Backend-Service#1261, WXYC/wxyc-ios-64#351), banning a fingerprint still requires an operator to (a) find the offender's fingerprint somewhere, then (b) curl the admin API. That's awkward, undiscoverable, and pushes ops work out of the surface DJs are actually looking at: Slack.
DJs see the abusive request in Slack, where the post is already rendered with the listener's message and metadata. The natural place to act is right there.
This ticket replaces (supersedes) #149, which was drafted against device_id identity from a legacy BS auth system since confirmed dead. The ban-target is now the iOS-generated fingerprint UUID, attached to the Slack post as message metadata.
End state
A full Slack app for request-o-matic that:
Replaces the incoming webhook for posting requests. Posts go through chat.postMessage with a bot token instead of requests.post(SLACK_WEBHOOK_URL, ...). Same visual output as today, but now interactive.
Attaches a "Ban requester" button to each post that originated from an authenticated client (i.e. where request-o-matic has a fingerprint for the requester). Posts without an associated fingerprint get no button. The fingerprint is stored in the message's metadata.event_payload (Slack-native private metadata) — not visible to listeners, only retrievable by the bot on button-click.
On button click: opens a Slack modal asking for a reason. On submit, request-o-matic:
Verifies the Slack request signature (v0= HMAC-SHA256 against SLACK_SIGNING_SECRET)
Posts an ephemeral confirmation to the acting user, and edits the original message to show "banned by @ — "
Authorization: only specific Slack users can ban. v1 mechanism: hardcoded allowlist in env (SLACK_BAN_AUTHORIZED_USERS=U01ABC,U02DEF,...). Reject (and log + Sentry-breadcrumb) all other users. Allowlist can graduate to a user-group or channel-membership check later.
Files / changes
services/slack.py — switch from incoming-webhook POST to chat.postMessage with SLACK_BOT_TOKEN; attach fingerprint as Slack message metadata; add the "Ban requester" button block when fingerprint is known
routers/slack_interactivity.py (new) — handles POST /slack/interactivity (the URL Slack calls when buttons are clicked or modals submitted)
config/settings.py — new env vars: SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_BAN_AUTHORIZED_USERS, SLACK_CHANNEL_ID. Deprecate SLACK_WEBHOOK_URL (keep as fallback for one release, then remove).
Slack app setup outside the repo: register a new Slack app, request scopes (chat:write, chat:write.public if posting to public channels), install to the workspace, register the request URL for interactivity, capture the bot token.
Update CLAUDE.md, README.md with the new Slack-app setup runbook.
Constraints
Don't fork the ban codepath. The Slack handler must call services/ban_service.py.ban(...) (created in Admin HTTP API for managing request-line fingerprint bans #151), passing actor=slack_user_id. The HTTP admin API and the Slack action are two frontends on one mechanism. Sketch:
This keeps audit trails consistent (actor field gets logged whether the source is a curl or a Slack click) and ensures the two paths can't drift.
Don't bypass signature verification. Every Slack callback must pass HMAC verification before any other processing. This is non-negotiable — an unverified callback is forgeable.
Don't post the "Ban requester" button on posts with no fingerprint. Unauthenticated v3.1 callers and degraded-mode posts have nothing to ban. Showing a button that 500s on click is worse than showing no button.
Keep chat.postMessage channel ID configurable. With bot-token posting, channel ID is explicit (env var SLACK_CHANNEL_ID). Default to the same channel as the current webhook.
Don't break current Slack posting during migration. Ship the Slack-app post path behind a feature flag (SLACK_USE_BOT_TOKEN) and migrate gradually, OR stage as two PRs (migration first, interactivity second — see "Suggested approach").
Don't expose the fingerprint as visible text in the message. It's a stable per-user identifier — putting it in user-visible content is a deanon vector. Use Slack's metadata.event_payload (private metadata) or the button value field (visible only to button handlers).
Suggested approach
Land as two PRs:
PR 1 — Slack-app migration. Replace incoming webhook with bot-token chat.postMessage. No UX change. Pure plumbing. Behind a feature flag (SLACK_USE_BOT_TOKEN) for one release so we can roll back if chat.postMessage behaves differently than the webhook.
PR 2 — Interactive ban action. Add the button + modal + interactivity endpoint + signature verification + authorization. Requires PR 1 as a base.
If the implementer prefers one bigger PR, that's fine — but the rollback story is cleaner with the split.
Open questions
Edit-the-message-on-ban vs ephemeral-confirmation-only. Editing the original post to "banned by @dj — reason" gives a public audit trail for everyone in the channel; ephemeral confirmation is private to the DJ. Recommend: both — edit the message (small footer) AND post an ephemeral ack.
Authorization graduation path. Hardcoded allowlist is v1. Future: Slack user-group membership (@wxyc-dj-staff via usergroups.users.list), or channel-membership in a specific ops channel.
Acceptance
Slack app registered, bot token + signing secret stored as env vars on staging and prod
chat.postMessage replaces incoming webhook (behind feature flag during transition)
"Ban requester" button appears on posts with a fingerprint, not on posts without
Problem
Once the core ban plumbing lands (#150, #151, WXYC/Backend-Service#1261, WXYC/wxyc-ios-64#351), banning a fingerprint still requires an operator to (a) find the offender's fingerprint somewhere, then (b)
curlthe admin API. That's awkward, undiscoverable, and pushes ops work out of the surface DJs are actually looking at: Slack.DJs see the abusive request in Slack, where the post is already rendered with the listener's message and metadata. The natural place to act is right there.
This ticket replaces (supersedes) #149, which was drafted against
device_ididentity from a legacy BS auth system since confirmed dead. The ban-target is now the iOS-generated fingerprint UUID, attached to the Slack post as message metadata.End state
A full Slack app for request-o-matic that:
chat.postMessagewith a bot token instead ofrequests.post(SLACK_WEBHOOK_URL, ...). Same visual output as today, but now interactive.fingerprintfor the requester). Posts without an associated fingerprint get no button. The fingerprint is stored in the message'smetadata.event_payload(Slack-native private metadata) — not visible to listeners, only retrievable by the bot on button-click.v0=HMAC-SHA256 againstSLACK_SIGNING_SECRET)services/ban_service.py.ban(fingerprint, reason, actor=slack_user_id)— the same function Admin HTTP API for managing request-line fingerprint bans #151 uses. Not a parallel codepath.SLACK_BAN_AUTHORIZED_USERS=U01ABC,U02DEF,...). Reject (and log + Sentry-breadcrumb) all other users. Allowlist can graduate to a user-group or channel-membership check later.Files / changes
services/slack.py— switch from incoming-webhook POST tochat.postMessagewithSLACK_BOT_TOKEN; attachfingerprintas Slack message metadata; add the "Ban requester" button block when fingerprint is knownrouters/slack_interactivity.py(new) — handlesPOST /slack/interactivity(the URL Slack calls when buttons are clicked or modals submitted)services/slack_signature.py(new) — request-signature verification middlewareservices/slack_authorization.py(new) — Slack user-ID allowlist checkservices/ban_service.py— already created by Admin HTTP API for managing request-line fingerprint bans #151; this ticket just calls into itconfig/settings.py— new env vars:SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET,SLACK_BAN_AUTHORIZED_USERS,SLACK_CHANNEL_ID. DeprecateSLACK_WEBHOOK_URL(keep as fallback for one release, then remove).chat:write,chat:write.publicif posting to public channels), install to the workspace, register the request URL for interactivity, capture the bot token.Constraints
services/ban_service.py.ban(...)(created in Admin HTTP API for managing request-line fingerprint bans #151), passingactor=slack_user_id. The HTTP admin API and the Slack action are two frontends on one mechanism. Sketch:actorfield gets logged whether the source is a curl or a Slack click) and ensures the two paths can't drift.chat.postMessagechannel ID configurable. With bot-token posting, channel ID is explicit (env varSLACK_CHANNEL_ID). Default to the same channel as the current webhook.SLACK_USE_BOT_TOKEN) and migrate gradually, OR stage as two PRs (migration first, interactivity second — see "Suggested approach").metadata.event_payload(private metadata) or the buttonvaluefield (visible only to button handlers).Suggested approach
Land as two PRs:
chat.postMessage. No UX change. Pure plumbing. Behind a feature flag (SLACK_USE_BOT_TOKEN) for one release so we can roll back ifchat.postMessagebehaves differently than the webhook.If the implementer prefers one bigger PR, that's fine — but the rollback story is cleaner with the split.
Open questions
@wxyc-dj-staffviausergroups.users.list), or channel-membership in a specific ops channel.Acceptance
chat.postMessagereplaces incoming webhook (behind feature flag during transition)services/ban_service.py.ban(...)→ posts ephemeral ack + edits original messageservices/slack.pystill pass under the new posting pathRelated
services/ban_service.pyexisting; can land in parallel if that ticket creates the service stub first).services/ban_service.pyultimately calls).