Skip to content

feat(channel): add native Slack channel integration - #42

Merged
bbbbbbbbbbbbba merged 2 commits into
huabeitech:mainfrom
DOS:feat/slack-channel
Sep 22, 2026
Merged

bbbbbbbbbbbbba merged 2 commits into
huabeitech:mainfrom
DOS:feat/slack-channel

Conversation

@JOY

@JOY JOY commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

What this adds

Slack as a sixth channel type, following the same shape as the Telegram and Zalo OA integrations: a client package, an inbound Events API service, an outbound service drained through the channel message outbox, a third-party handler, and the dashboard form fields.

Independent of #41 — both branch from main and touch the same integration points, so whichever merges second will need a small rebase.

Inbound

POST /api/third/slack/webhook[/:channel_id]

Answers Slack's url_verification handshake before any channel lookup, because Slack sends it once while the endpoint is being configured and does not retry. event_callback messages map onto the existing conversation model through ExternalSourceSlack. Bot authors, bot_message subtypes and empty text are dropped so the bot cannot talk to itself in a loop.

Outbound

Agent and AI replies are queued by EnqueueSlackMessage and drained by cron every five seconds, with the same immediate-dispatch goroutine, backoff and retry ceiling as the other channels. Replies are posted with the inbound thread_ts, so an answer stays in the customer's thread instead of landing in the workspace timeline.

Two security changes worth a look

The endpoint is unauthenticated by design, so both of these matter.

A missing signature is now a rejection. Previously verification ran only when both X-Slack-Signature and X-Slack-Request-Timestamp were present:

if cfg.SigningSecret != "" && signatureHeader != "" && timestampHeader != "" {

Omitting the headers therefore bypassed the check entirely, and anyone who learned the webhook URL could post into a workspace conversation and trigger paid AI replies. Slack always signs once a signing secret exists, so a missing header means the sender is not Slack.

Timestamps outside a five minute window are rejected. Slack's own verification guide requires this. Without it a captured request replays indefinitely — the signature covers the timestamp and the body, but nothing in it expires.

A channel with no signing secret still accepts deliveries, so neither change breaks an existing installation. The dashboard field now says what leaving it empty costs.

Credentials

Everything is per channel: bot token, signing secret, app id, team id, team name, default channel. No environment variables are added, so .env.example is untouched. The bot token is required when creating a channel, the same way Telegram's is, because without it the channel can receive events but can never reply.

Tests

File Covers
internal/slack/client_test.go threaded reply carries thread_ts; a top-level post omits it; ok:false surfaces as an error rather than being treated as success; input validation
internal/handlers/third/slack_handler_test.go url_verification handshake; an unsigned event is rejected and creates no customer identity; a signed event resolves the sender
internal/services/slack_inbound_service_test.go inbound → conversation → outbox; rejection of missing headers, signature-without-timestamp, timestamp-without-signature, wrong signature, non-numeric timestamp; replayed timestamps at 10 min, 1 hour and 10 min in the future, each correctly signed for its own timestamp; fresh and 4-minute-old signatures still accepted

Note that client_test.go is new — the Slack client had no test coverage before this.

Verified locally: go build ./..., go vet ./internal/..., go test ./internal/... and tsc --noEmit all clean. TestLoadOverridesValuesFromEnvironment and TestLoadFromDotEnvAndStandardEnvAliases fail on my machine but also fail on a pristine checkout of main when PORT / DB_TYPE / DATABASE_URL exist in the ambient environment, so they are unrelated.

Not included

Slack's OAuth install flow, so the bot token is pasted rather than obtained by authorization. Channel and DM history backfill is also not implemented: a channel starts receiving from the moment the Events API subscription goes live.

Adds Slack as a sixth channel type, following the same shape as the Telegram and
Zalo OA integrations: a client package, an inbound Events API service, an
outbound service drained through the channel message outbox, a third-party
handler, and the dashboard form fields.

Inbound

  POST /api/third/slack/webhook[/:channel_id]

Answers Slack's url_verification handshake before any channel lookup, because
Slack sends it once while the endpoint is being configured and does not retry.
event_callback messages map onto the existing conversation model through
ExternalSourceSlack. Bot authors, bot_message subtypes and empty text are
dropped so the bot cannot talk to itself in a loop. The channel is resolved by
explicit id, then by team_id, then by the single enabled Slack channel.

Outbound

Agent and AI replies are queued by EnqueueSlackMessage and drained by cron every
five seconds, with the same immediate-dispatch goroutine, backoff and retry
ceiling as the other channels. Replies are posted with the inbound thread_ts so
an answer stays in the customer's thread instead of landing in the workspace
timeline. Text and HTML go out as content; an image is sent with its signed asset
URL; an attachment appends the signed URL.

Signing secret verification

Two changes here, both because the endpoint is unauthenticated by design.

A channel with a signing secret now rejects a delivery that arrives without the
X-Slack-Signature and X-Slack-Request-Timestamp headers. Previously the check
only ran when both headers were present, so omitting them bypassed verification
entirely: anyone who learned the webhook URL could post into a workspace
conversation and trigger paid AI replies. Slack always signs once a signing
secret exists, so a missing header means the sender is not Slack.

Request timestamps are now rejected outside a five minute window in either
direction. Slack's own verification guide requires this, and without it a
captured request replays indefinitely: the signature covers the timestamp and the
body, but nothing in it expires.

A channel with no signing secret still accepts deliveries, so this is not a
breaking change for an existing installation. The dashboard field explains what
leaving it empty costs.

Credentials

Everything is per channel: bot token, signing secret, app id, team id, team name
and a default channel. No environment variables are added, so .env.example is
untouched. The bot token is required when creating a channel, the same way
Telegram's is, because without it the channel can receive events but can never
reply.

Tests

  internal/slack/client_test.go
      threaded reply carries thread_ts, a top-level post omits it, ok:false is
      surfaced as an error rather than treated as success, and input validation
  internal/handlers/third/slack_handler_test.go
      url_verification handshake, an unsigned event is rejected and creates no
      customer identity, a correctly signed event resolves the sender
  internal/services/slack_inbound_service_test.go
      inbound to conversation to outbox; rejection of missing headers, a
      signature without a timestamp, a timestamp without a signature, a wrong
      signature and a non-numeric timestamp; replayed timestamps at ten minutes,
      one hour and ten minutes in the future, each correctly signed for its own
      timestamp; fresh and four-minute-old signatures still accepted

Not included

Slack's OAuth install flow, so the bot token is pasted rather than obtained by
authorization. Channel and DM history backfill is also not implemented: the
channel starts receiving from the moment the Events API subscription is live.
@bbbbbbbbbbbbba

Copy link
Copy Markdown
Contributor

@JOY This branch has caused a conflict.

Resolve against upstream main through huabeitech#49:

- union the Slack and Discord channel additions (dto config types,
  external source enum, outbox enqueue + dispatch, channel parse and
  validation, cron dispatch, routes registration, dashboard dialog and
  channel page)
- keep upstream's ServerConfig security fields (trustedProxies,
  trustedPlatform, rateLimit) which merge in cleanly alongside this
  branch's Slack config
- parseSlackChannelConfig no longer trims a WelcomeMessage field the
  Slack config never had
- the dashboard dialog's EditForm/schema grow both slack and discord
  fields so one form serves every IM channel
- messages locales gain both channel option sets
@bbbbbbbbbbbbba
bbbbbbbbbbbbba merged commit 44c0730 into huabeitech:main Sep 22, 2026
2 checks passed
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.

2 participants