Skip to content

feat(slack): inbound + outbound media attachments#176

Merged
williamwa merged 11 commits into
mainfrom
feat/slack-attachments
May 29, 2026
Merged

feat(slack): inbound + outbound media attachments#176
williamwa merged 11 commits into
mainfrom
feat/slack-attachments

Conversation

@williamwa
Copy link
Copy Markdown
Collaborator

@williamwa williamwa commented May 29, 2026

Third channel in the attachment-support rollout (after #174 WhatsApp, #175 Discord).

Slack was fully text-only and even dropped file_share messages (the subtype guard returned early). Wires the channel to the already-complete core attachment infrastructure both directions. No core/protocol/gateway changes.

Inbound (Slack → agent)

  • file_share events are now accepted. Each file is fetched from url_private with bot-token auth and uploaded as a session attachment — images become vision input automatically.
  • Audio files are transcribed via client.transcribeAudio and appended as [Transcribed voice message] text.
  • Media-only messages now trigger the agent instead of being dropped.
  • Files over the 25 MiB cap are skipped (logged).

Outbound (agent → Slack)

  • The agent's attachment_send deliveries (SSE attachment event) are uploaded via files.uploadV2, into the originating thread when applicable.

Notes

  • New bot scopes required: files:read (download inbound) + files:write (upload outbound).
  • Slack is a private package bundled into the openhermit CLI, so this ships with the next CLI release. Version bumped 0.2.0 → 0.3.0.
  • Also corrects the manual, which previously claimed "File attachments supported" — that was aspirational; it's now actually true.
  • Adds the first unit tests for this package (isProcessableMessage gating).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Slack/Discord/WhatsApp adapters now support media: inbound files are downloaded and forwarded (images → vision, audio → STT); media-only messages are processable; agents can send attachments back to threads; files >25 MiB are skipped; WhatsApp supports TTS voice replies when appropriate.
  • Documentation

    • Channel docs updated with media behavior and required scopes/notes.
  • Chores

    • Channel package versions bumped to 0.3.0.
  • Tests

    • Added tests covering message filtering, attachment mapping, file download caps, and media send behavior.

Review Change Stack

williamwa and others added 3 commits May 29, 2026 18:01
WhatsApp was text-only: inbound media was dropped (only captions kept)
and the agent couldn't send files. Wire the channel to the existing
attachment infrastructure in both directions.

Inbound: images/video/documents are downloaded via Baileys
downloadMediaMessage and uploaded as session attachments (images become
vision input); captions ride along as text. Voice/audio notes are
transcribed via the agent's STT, and replies to a voice note are spoken
back as a WhatsApp voice note when TTS is configured. Media-only messages
now trigger the agent instead of being dropped. Media over the 25 MiB cap
is skipped.

Outbound: the agent's attachment_send deliveries (SSE 'attachment' event)
are routed to the matching Baileys send — image/video/document, and
ogg/opus audio as a native push-to-talk voice note.

No core/protocol/gateway changes — uses SDK uploadAttachment,
postMessage({attachments}), downloadAttachmentBytes, transcribeAudio,
synthesizeAudio. Bumps 0.2.0 -> 0.3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Discord was fully text-only: inbound attachments were ignored and the
agent couldn't send files. Wire the channel to the existing attachment
infrastructure both directions.

Inbound: message attachments (guild messages + DM gateway dispatch) are
fetched from the Discord CDN and uploaded as session attachments (images
become vision input); audio attachments are transcribed via the agent's
STT and appended as text. Media-only messages now trigger the agent.
Attachments over the 25 MiB cap are skipped.

Outbound: the agent's attachment_send deliveries (SSE 'attachment' event)
are sent back as Discord file uploads via AttachmentBuilder, with any
caption as the message content.

Discord is bundled into the CLI (private package), so this ships with the
next openhermit release rather than a standalone publish. Bumps 0.2.0 ->
0.3.0 for changelog clarity. Adds the first unit tests for this package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slack was fully text-only and even dropped file_share messages (the
subtype guard returned early). Wire the channel to the existing
attachment infrastructure both directions.

Inbound: file_share uploads are now accepted; each file is fetched from
url_private with bot-token auth and uploaded as a session attachment
(images become vision input). Audio files are transcribed via STT and
appended as text. Media-only messages now trigger the agent. Files over
the 25 MiB cap are skipped.

Outbound: the agent's attachment_send deliveries (SSE 'attachment' event)
are uploaded back via files.uploadV2, into the originating thread when
applicable.

Needs files:read (download) and files:write (upload) bot scopes. Slack is
bundled into the CLI (private package), so this ships with the next
openhermit release. Bumps 0.2.0 -> 0.3.0 and adds the first unit tests
(isProcessableMessage gating). Also corrects the manual, which previously
claimed file support that did not exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds media support across Slack, Discord, and WhatsApp adapters: file/download/upload APIs, inbound resolution (audio STT, non-audio session attachments), outbound agent attachment delivery via SSE, media-aware message filtering, tests, docs, and package bumps.

Changes

Channel media handling across adapters

Layer / File(s) Summary
API contracts and file-transfer methods
apps/channels/*/src/*-api.ts, apps/channels/slack/test/slack-api.test.ts, apps/channels/whatsapp/test/whatsapp-api.test.ts
Introduce channel-specific attachment types and APIs: Slack SlackFile + SlackApi.downloadFile/uploadFile (authenticated streaming with maxBytes cap), Discord DiscordIncomingAttachment + DiscordApi.sendFile, WhatsApp WhatsAppOutboundMedia + WhatsAppApi.sendMedia/downloadMedia. Tests validate streaming, caps, and send routing.
Message filtering and bot ingestion
apps/channels/slack/src/bot.ts, apps/channels/discord/src/bot.ts, apps/channels/whatsapp/src/bot.ts, apps/channels/*/test/*
Centralized filtering allows media-only messages and rejects bot/no-user/unsupported-subtype events. Bots now treat text as optional and include attachments/media where present. Unit tests added for Slack and Discord mapping and WhatsApp media extraction.
Inbound resolution in bridges
apps/channels/*/src/bridge.ts
Bridges add MAX_MEDIA_BYTES and ResolvedInbound. resolveInbound downloads files (with timeout/cap), transcribes audio into message text when available, uploads non-audio files as durable session attachments, and returns composed text and attachment refs. Posting to agent uses resolved payload and short-circuits if nothing usable.
Outbound attachment delivery and SSE handling
apps/channels/*/src/bridge.ts
Bridges add deliverAttachment to validate session/attachment ids, fetch attachment bytes from the agent-local API, and upload to the channel (Slack files.uploadV2, Discord sendFile, WhatsApp sendMedia). SSE consumer recognizes attachment frames, invokes delivery, logs failures, and continues.
WhatsApp media helpers and voice routing
apps/channels/whatsapp/src/*, apps/channels/whatsapp/test/*
Add extractMedia, WhatsAppIncomingMedia type, STT for inbound audio, TTS gating and voice-reply logic, and WhatsApp API download/send wiring preserving captions and handling ptt for OGG/OPUS. Tests cover media parsing and sendMedia routing.
Tests, docs, and manifests
apps/channels/*/README.md, docs/*, apps/*/package.json, apps/gateway/package.json
Update READMEs and docs to document media behavior and required Slack scopes (files:read,files:write). Bump channel package versions to 0.3.0 and update gateway optionalDependencies. Add/extend tests for Slack download streaming, Slack bot filtering, Discord mapping, and WhatsApp sendMedia.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐇 I nibbled bytes and fetched each file,
Hopped through transcripts, and stacked them with style,
Threads got uploads, small hops not a slog,
Twenty-five megs pause — the hill, not the frog,
Media now dances from user to bot with a smile.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(slack): inbound + outbound media attachments' accurately summarizes the main change: enabling media attachment support (both receiving and sending) for the Slack channel adapter.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/slack-attachments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/channels/slack/src/bot.ts (1)

18-24: ⚡ Quick win

Trim text before processability check to avoid forwarding whitespace-only events.

Boolean(event.text) treats ' ' as processable, but downstream bridge trims and drops it. Aligning this check avoids unnecessary dedupe/work in handleMessageEvent.

Proposed change
 export function isProcessableMessage(event: SlackMessageEvent): boolean {
   if (event.subtype && event.subtype !== 'file_share') return false;
   if (event.bot_id) return false;
   if (!event.user) return false;
   const hasFiles = Array.isArray(event.files) && event.files.length > 0;
-  return Boolean(event.text) || hasFiles;
+  return Boolean(event.text?.trim()) || hasFiles;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/channels/slack/src/bot.ts` around lines 18 - 24, The
isProcessableMessage function currently treats whitespace-only event.text (e.g.,
"   ") as processable; update the check to trim the text before evaluating
processability so whitespace-only messages are considered non-processable. In
isProcessableMessage, replace the Boolean(event.text) check with a trimmed-text
check (e.g., event.text?.trim().length > 0) while preserving the existing
file/sharing and bot/user filters and the hasFiles condition so messages with
actual text or files remain processable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/channels/slack/package.json`:
- Line 4: The repository bumped `@openhermit/channel-slack` to 0.3.0 in
apps/channels/slack/package.json but downstream pins/lockfiles still reference
0.2.0; update any references to `@openhermit/channel-slack` (e.g., in
apps/gateway/package.json and package-lock.json) to 0.3.0 and regenerate the
lockfile (run npm install or npm ci/npm install --package-lock-only as
appropriate) so the CLI path and adapter consumers pull the new media behavior.

In `@apps/channels/slack/src/bridge.ts`:
- Around line 127-134: The current check uses file.size but may still download
oversized content; after calling this.slack.downloadFile(url) in the block that
assigns bytes, immediately validate bytes.length against MAX_MEDIA_BYTES and if
it exceeds the cap, log a skipping message (including file.name or file.id and
the actual bytes.length), discard/free the bytes and continue the loop; ensure
this path mirrors the existing oversized-file handling so oversized downloads
are not kept in memory.

---

Nitpick comments:
In `@apps/channels/slack/src/bot.ts`:
- Around line 18-24: The isProcessableMessage function currently treats
whitespace-only event.text (e.g., "   ") as processable; update the check to
trim the text before evaluating processability so whitespace-only messages are
considered non-processable. In isProcessableMessage, replace the
Boolean(event.text) check with a trimmed-text check (e.g.,
event.text?.trim().length > 0) while preserving the existing file/sharing and
bot/user filters and the hasFiles condition so messages with actual text or
files remain processable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3bf6c5c-2587-4e0f-8681-07f858609717

📥 Commits

Reviewing files that changed from the base of the PR and between 38db5d9 and 2eca2e7.

📒 Files selected for processing (8)
  • apps/channels/slack/README.md
  • apps/channels/slack/package.json
  • apps/channels/slack/src/bot.ts
  • apps/channels/slack/src/bridge.ts
  • apps/channels/slack/src/slack-api.ts
  • apps/channels/slack/test/bot.test.ts
  • docs/channel-adapter.md
  • docs/manual/17-channels.md

Comment thread apps/channels/slack/package.json
Comment thread apps/channels/slack/src/bridge.ts
Three fixes from PR review:

1. Version pin/lockfile: gateway optionalDependencies still pinned
   @openhermit/channel-slack@0.2.0 while the workspace moved to 0.3.0, so a
   fresh install wouldn't link the updated adapter. Bump the gateway pin and
   the lockfile to 0.3.0.

2. 25 MiB cap was only enforced against Slack's reported file.size, which can
   be missing or wrong — downloadFile() would then read the whole body into
   memory. Move the cap into SlackApi.downloadFile(url, maxBytes): reject an
   oversized content-length up front, then stream and abort the moment the
   body crosses the limit so an oversized/mislabeled file never fully lands in
   memory. The bridge passes MAX_MEDIA_BYTES (file.size stays as a cheap
   early-skip).

3. isProcessableMessage() now trims text, so a whitespace-only event with no
   files is no longer treated as processable.

Adds tests: whitespace-only not processable; downloadFile rejects on
content-length and aborts mid-stream over the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamwa
Copy link
Copy Markdown
Collaborator Author

Addressed all three points in 9b591fc:

  1. Version pin / lockfile — bumped apps/gateway/package.json optionalDependency @openhermit/channel-slack 0.2.0 → 0.3.0 and updated package-lock.json to match (scoped to the slack refs).
  2. Download cap moved into the transportSlackApi.downloadFile(url, maxBytes) now rejects an oversized content-length up front and otherwise streams the body, aborting the moment it crosses the limit, so an oversized or mislabeled file never fully lands in memory. The bridge passes MAX_MEDIA_BYTES; the file.size check stays as a cheap early-skip.
  3. isProcessableMessage trimsBoolean(event.text?.trim()) || hasFiles, so whitespace-only events are no longer processable.

Added tests: whitespace-only not processable, and downloadFile rejecting both on content-length and mid-stream over the 25 MiB cap. npm test -w @openhermit/channel-slack (8/8) + build + typecheck green.

Note on scope: the gateway pin gap (#2 in the original review) also affects the bundled Discord (#175) and Telegram (#177) adapters — their gateway pins are still 0.2.0. I can fix those in their respective PRs, or fold all three gateway pin bumps + the stale cli/whatsapp lockfile sync into one small follow-up — your call.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/channels/slack/src/slack-api.ts`:
- Around line 52-55: downloadFile currently calls fetch(urlPrivate, ...) with no
timeout; add an AbortController and pass its signal into fetch
(fetch(urlPrivate, { headers: {...}, signal })) and start a timer (e.g.
configurable default like 10-15s) that calls controller.abort() on timeout;
clear the timer on success/final error, catch the abort error and rethrow a
clear timeout error (e.g. "downloadFile timed out") so callers fail fast, and
ensure existing usage of this.botToken and maxBytes inside downloadFile remains
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2fe24ef8-3b7e-4a13-a84c-bef710afa063

📥 Commits

Reviewing files that changed from the base of the PR and between 2eca2e7 and 9b591fc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • apps/channels/slack/src/bot.ts
  • apps/channels/slack/src/bridge.ts
  • apps/channels/slack/src/slack-api.ts
  • apps/channels/slack/test/bot.test.ts
  • apps/channels/slack/test/slack-api.test.ts
  • apps/gateway/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/channels/slack/src/bot.ts
  • apps/channels/slack/test/bot.test.ts
  • apps/channels/slack/src/bridge.ts

Comment thread apps/channels/slack/src/slack-api.ts
williamwa and others added 4 commits May 29, 2026 18:59
The gateway optionalDependencies still pinned @openhermit/channel-discord
at 0.2.0 while the workspace moved to 0.3.0, so a fresh install wouldn't
link the updated adapter and the media changes wouldn't take effect in the
bundled CLI. Bump the gateway pin and lockfile to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three review fixes:
- package.json description no longer says "Text-only v1." (stale npm
  metadata after the 0.3.0 media/voice rollout).
- On STT failure, log the detail but show the user a generic message
  instead of forwarding the raw error text into the chat.
- Normalize the audio MIME (strip params like `; codecs=opus`) before
  push-to-talk detection, so `audio/ogg; codecs=opus` still sends as a
  voice note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A stalled Discord CDN connection during inbound attachment download could
block the per-channel message queue indefinitely. Add a 15s
AbortSignal.timeout() to the fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A stalled url_private connection during inbound file download could block
the channel queue indefinitely. Add a 15s AbortSignal.timeout() to the
downloadFile fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamwa
Copy link
Copy Markdown
Collaborator Author

Added the remaining nitpick: a 15s AbortSignal.timeout() on the downloadFile fetch so a stalled url_private connection can't block the channel queue. All earlier review items (gateway pin/lockfile, download cap moved into the transport, text trim) were already addressed. Build + typecheck + tests (8/8) green.

williamwa added 3 commits May 29, 2026 19:50
…eat/slack-attachments

# Conflicts:
#	apps/gateway/package.json
#	docs/channel-adapter.md
#	package-lock.json
# Conflicts:
#	apps/gateway/package.json
#	docs/channel-adapter.md
#	package-lock.json
@williamwa williamwa merged commit 446d899 into main May 29, 2026
1 check was pending
@williamwa williamwa deleted the feat/slack-attachments branch May 29, 2026 11:57
williamwa added a commit that referenced this pull request May 29, 2026
Ship the bundled channel media support — Discord/Slack/Telegram
attachments (#175, #176, #177) and their gateway pins (now all 0.3.0) —
in the published CLI. Also syncs the stale lockfile cli entry (0.9.0 ->
0.9.2) left by the earlier 0.9.1 bump.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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