Skip to content

feat(channels): add Slack channel#83

Merged
viettranx merged 2 commits intonextlevelbuilder:mainfrom
vanducng:feat/slack-channel
Mar 9, 2026
Merged

feat(channels): add Slack channel#83
viettranx merged 2 commits intonextlevelbuilder:mainfrom
vanducng:feat/slack-channel

Conversation

@vanducng
Copy link
Contributor

@vanducng vanducng commented Mar 7, 2026

Summary

Add Slack channel integration using Socket Mode with full policy support, streaming, and formatting.

  • Socket Mode event-driven messaging (xoxb- + xapp- tokens)
  • DM and group policies: open, pairing, allowlist, disabled
  • Thread participation with configurable TTL
  • Markdown-to-mrkdwn formatting pipeline
  • Streaming (edit-in-place + native ChatStreamer)
  • SSRF-protected file downloads, debounce, dedup, reactions
  • 170 unit tests

Bug fixes

  • isValidChannelType() missing "slack" in HTTP and WS validation
  • BaseChannel.HandleMessage() allowlist now checks chatID (group allowlist with channel IDs)
  • Empty allowlist no longer bypasses pairing or acts as open
  • Thread TTL uses *int pointer (0 = disabled, nil = 24h default)

Closes #37

Test plan

  • Build, vet, 170 unit tests pass
  • Manual: group policies, thread TTL, @mention gating, DM pairing
  • Code review completed

@vanducng vanducng force-pushed the feat/slack-channel branch from 9e0a1b8 to 2be42d5 Compare March 7, 2026 19:12
Copy link
Contributor

@viettranx viettranx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Slack Channel Integration

Overall quality: 7.5/10 — Solid architecture, good test coverage (~170 tests), but there are 2 critical issues that should be fixed before merge.


🔴 Critical (must fix)

1. SSRF Protection gap in CheckRedirect (handlers.go)

The redirect handler only checks for HTTPS scheme but does not validate the hostname. An attacker could craft a Slack file URL that redirects to an internal/cloud metadata endpoint (e.g., 169.254.169.254).

Fix: Add hostname validation in the CheckRedirect function — only allow redirects to known Slack CDN domains (e.g., *.slack-edge.com, files.slack.com).

2. BaseChannel.HandleMessage change affects ALL channels (channel.go:247)

The change from !c.IsAllowed(senderID) to !c.IsAllowed(senderID) && !c.IsAllowed(chatID) is a behavior change for every channel (Telegram, Discord, Feishu, WhatsApp, Zalo) — not just Slack.

With this change, if a group chatID is in the allowlist, any user in that group is implicitly allowed, which may not be the intended behavior for existing channels.

Fix: Revert the BaseChannel change and handle chatID-based allowlist logic specifically in the Slack channel's HandleMessage override.


🟡 High Priority

3. Temp file leak in downloadFile() (handlers.go)

downloadFile() creates temp files via os.CreateTemp but there's no cleanup (os.Remove) after the file is processed and sent to the message bus. In production with high volume, this will leak disk space.

Fix: Add defer os.Remove(tmpFile.Name()) after successful creation, or ensure the consumer cleans up.

4. Debounce metadata overwrite (handlers.go)

When batching multiple messages in the debounce window, the metadata map is only set from the first message. Subsequent messages in the same batch overwrite with stale message_id. The final batched message should use the last message's metadata.

5. Debounce flush on shutdown

When Stop() is called, any pending messages in the debounce timer window are silently dropped. Consider flushing pending debounce entries before shutdown completes.


💡 Suggestions (non-blocking)

  • format.go: The convertNode function handles many cases — consider a dispatch table (map[string]func) for cleaner extensibility.
  • reactions.go: The thinkingReaction goroutine could leak if the context is never cancelled. Add a safety timeout.
  • stream.go: editMessage retries on rate_limited but doesn't read the Retry-After header from Slack's response.
  • media.go: File size limit (20MB) is hardcoded — consider making it configurable via SlackConfig.

✅ What's done well

  • Token prefix validation (xoxb-, xapp-) at startup — nice defensive check
  • Dead socket classification with fail-fast on auth errors
  • Event dedup mechanism prevents duplicate processing
  • Thread participation cache with configurable TTL
  • Clean DRY refactor: ExtractDocumentContent extracted to shared media_utils.go
  • Graceful shutdown with WaitGroup + context cancellation + timeout
  • Good test structure with table-driven tests

Bug fixes included (verified ✅)

  • isValidChannelType() now includes "slack" — correct
  • Empty allowlist no longer bypasses pairing — good fix
  • Thread TTL *int pointer semantics (0=disabled, nil=24h default) — clean approach

@vanducng vanducng force-pushed the feat/slack-channel branch from 2be42d5 to 0fd255b Compare March 8, 2026 13:14
@vanducng
Copy link
Contributor Author

vanducng commented Mar 8, 2026

Review Follow-up — All Critical & High Items Addressed

Rebased on latest main and applied fixes for all review feedback:

Critical Fixes

  1. SSRF Protection in CheckRedirect — Added isAllowedSlackHost() hostname validation. Redirects only allowed to *.slack-edge.com and *.slack.com.
  2. BaseChannel.HandleMessage reverted — Restored original !c.IsAllowed(senderID) in BaseChannel. Added Slack-specific HandleMessage override that checks both senderID and chatID, so other channels are unaffected.

High Priority Fixes

  1. Debounce metadata overwrite — Uses latest message's metadata on flush.
  2. Debounce flush on shutdown — Stop() now flushes pending entries instead of dropping them.

Additional Bug Fix (found during testing)

  1. Duplicate media paths in debounce — Entry constructor set media, then append duplicated it on first call. This caused persistMedia to process the same file twice — first iteration moved the file, second failed with no such file. Fixed with if loaded guard.

Verified

  • go compile + vet pass
  • go test ./internal/channels/slack/... pass (170 tests)
  • Manual: Slack group @mention with image — downloaded, persisted, vision processed correctly

@vanducng vanducng force-pushed the feat/slack-channel branch from 0fd255b to 4a921e1 Compare March 8, 2026 13:18
vanducng added 2 commits March 9, 2026 03:06
Implement Slack integration using Socket Mode (xapp-/xoxb- tokens):
- Event-driven messaging via app_mention + message events
- Policy checks: open, pairing, allowlist, disabled (DM + group)
- Thread participation with configurable TTL
- Markdown-to-mrkdwn formatting pipeline
- Streaming support (edit-in-place + native ChatStreamer)
- SSRF-protected file downloads
- Debounce, dedup, reactions, group history context
- 170 unit tests (format, helpers, stream, SSRF)

Fix BaseChannel.HandleMessage allowlist to also check chatID,
enabling group allowlist with channel IDs across all channels.

Closes nextlevelbuilder#37
- Wire inbound file download into handleMessage (images, audio, documents)
- Add media.go with resolveMedia, classifyMime, buildMediaTags
- Extract shared ExtractDocumentContent to channels/media_utils.go (DRY with Telegram)
- Support file_share and message_changed subtypes
- Handle edit-to-mention: respond when user edits old message to add @bot
- Add MediaMaxBytes config field (default 20MB)
- Fix debounce media accumulation (was silently dropping files)
- Add 60s HTTP client timeout on file downloads
- Refactor downloadFile signature for slack.File compatibility
@vanducng vanducng force-pushed the feat/slack-channel branch from 4a921e1 to 7c0b471 Compare March 8, 2026 20:07
@viettranx viettranx merged commit 137a986 into nextlevelbuilder:main Mar 9, 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.

feat: implement Slack channel

2 participants