Skip to content

Post classified submissions to Slack, not just the CMS queue - #187

Merged
ssavutu merged 1 commit into
mainfrom
feat/slack-classified-notifications
Aug 5, 2026
Merged

Post classified submissions to Slack, not just the CMS queue#187
ssavutu merged 1 commit into
mainfrom
feat/slack-classified-notifications

Conversation

@ssavutu

@ssavutu ssavutu commented Aug 5, 2026

Copy link
Copy Markdown
Member

The Approve/Reject interactivity endpoint has been in place for a while, but nothing ever posted the message those buttons live on — the CMS was verifying clicks that could not arrive. This adds the outbound half of the moderation loop.

What's here

  • server/internal/slack — a Notifier that renders the Block Kit message (submitter, email, category, run-until date, the message itself, Approve/Reject buttons, and an optional link back to the CMS queue) and posts it to an incoming webhook. The button action_ids and the row id they carry are the contract with internal/handlers/slack.go, so both ends now share exported constants. The handler still accepts the past-tense spellings used by messages already sitting in channel history.
  • POST /v1/classifieds now notifies on success. The post runs in a goroutine on a context detached from the request — the response is written immediately after, which would otherwise cancel it mid-flight — and a failure logs rather than 500s. The row is already in the queue, and a reader must not see an error for a moderation channel they do not know exists.
  • Reader-supplied text is escaped and blockquoted so a submission cannot forge links or mimic the CMS's own formatting, and is truncated before Slack's 3000-character section limit, which would otherwise reject the whole message rather than just the tail.
  • Configuration is two halves, and half is worse than none. A webhook without a signing secret puts buttons in the channel that the callback refuses; a signing secret without a webhook posts nothing to click. Startup now names whichever variable is missing, and the moderation queue only promises Slack approvals when both are present (SlackModerationConfigured). A malformed webhook URL is fatal at boot rather than silently failing on every submission.

Config

SLACK_WEBHOOK_URL (new, required for notifications) and SLACK_CLASSIFIEDS_QUEUE_URL (new, optional link back to the CMS) added to both compose files and both env examples. Running with neither is still supported — submissions simply wait in the CMS queue.

Testing

go build, go vet, and go test ./... are clean; new tests cover webhook URL validation, the button contract, non-200 error surfacing, escaping, and truncation, plus a table test that SlackModerationConfigured requires both halves. Frontend tsc -b and eslint are clean. Not yet exercised against a real Slack workspace — that needs a webhook URL in the Delta env.

🤖 Generated with Claude Code

The Approve/Reject interactivity endpoint has been in place, but nothing
ever posted the message those buttons live on: the CMS verified clicks
that could not arrive. This adds the outbound half.

internal/slack builds the Block Kit notification and posts it to an
incoming webhook. The button action_ids and the row id they carry are
the contract with internal/handlers/slack.go, so both ends now share
exported constants; the handler still accepts the past-tense spellings
used by messages already in channel history.

The post is deliberately off the request's critical path. It runs in a
goroutine on a detached context -- the response is written immediately
after, which would otherwise cancel it mid-flight -- and a failure logs
rather than 500s. The row is already in the queue, and a reader must not
see an error for a moderation channel they do not know exists.

Reader-supplied text is escaped and blockquoted so a submission cannot
forge links or mimic the CMS's own formatting, and is truncated before
Slack's section limit, which would otherwise reject the whole message.

The two halves are configured separately and half is worse than none, so
startup names whichever variable is missing and the queue UI only
promises Slack approvals when both are set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ssavutu
ssavutu merged commit 77ddc28 into main Aug 5, 2026
6 checks passed
@ssavutu
ssavutu deleted the feat/slack-classified-notifications branch August 5, 2026 06:55
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.

1 participant