Skip to content

feat: Slack media attachments for content pipeline#381

Merged
sweetmantech merged 15 commits intotestfrom
feature/slack-media-attachments
Mar 31, 2026
Merged

feat: Slack media attachments for content pipeline#381
sweetmantech merged 15 commits intotestfrom
feature/slack-media-attachments

Conversation

@recoup-coding-agent
Copy link
Copy Markdown
Collaborator

@recoup-coding-agent recoup-coding-agent commented Mar 31, 2026

Summary

  • Extract audio and image attachments from Slack messages that trigger the Content Agent
  • Upload attachments to Vercel Blob storage and pass public URLs to the content creation pipeline
  • Audio attachments replace the song selection from Git repos
  • Image attachments replace the face-guide.png from the artist's repo
  • Also adds attached_audio_url and attached_image_url to the REST API endpoint for parity

Changes

  • New: extractMessageAttachments.ts — extracts first audio/image from Slack message, uploads to Vercel Blob
  • New: extractMessageAttachments.test.ts — 9 unit tests
  • Modified: registerOnNewMention.ts — calls extraction, passes URLs in payload, notes in ack message
  • Modified: registerOnNewMention.test.ts — 4 new tests for attachment pass-through
  • Modified: triggerCreateContent.ts — adds attachedAudioUrl/attachedImageUrl to payload interface
  • Modified: validateCreateContentBody.ts — accepts attached_audio_url/attached_image_url via REST API
  • Modified: createContentHandler.ts — passes attachment URLs through to trigger

Test plan

  • 9 tests for extractMessageAttachments (all pass)
  • 13 tests for registerOnNewMention including 4 new attachment tests (all pass)
  • Full test suite: 1657 tests pass (8 pre-existing failures in slack/oauth unrelated)
  • E2E: Send Slack message with audio attachment → verify audio used in video
  • E2E: Send Slack message with image attachment → verify image used as face guide

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Detects first audio and first image attachments in incoming messages and uploads them to public storage.
    • Public attachment URLs are included in acknowledgement/thread messages.
    • Content-creation flow accepts optional image URLs (face guides) when present.
    • Songs may be provided as either existing identifiers or public song URLs.

When users attach audio or image files to their Slack message that triggers
the Recoup Content Agent, those attachments are now extracted, uploaded to
Vercel Blob storage, and passed through to the content creation pipeline.

- Audio attachments replace the song selection from Git
- Image attachments replace the face-guide from the artist's repo

New files:
- extractMessageAttachments.ts — extracts and uploads Slack attachments
- extractMessageAttachments.test.ts — 9 tests for attachment extraction

Modified files:
- registerOnNewMention.ts — calls extractMessageAttachments, passes URLs
- triggerCreateContent.ts — adds attachedAudioUrl/attachedImageUrl to payload
- validateCreateContentBody.ts — accepts attached_audio_url/attached_image_url
- createContentHandler.ts — passes attachment URLs through to trigger

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Extracts the first audio and first image from a Slack message's attachments, uploads resolved files to Vercel Blob to obtain public URLs, and passes those URLs (songUrl, imageUrl) into validation and the create-content trigger payload (images optional; songs may be URLs).

Changes

Cohort / File(s) Summary
Attachment extraction
lib/agents/content/extractMessageAttachments.ts
New module exporting extractMessageAttachments and ExtractedAttachments. Locates first audio/image attachments, resolves bytes via direct download (with SLACK_CONTENT_BOT_TOKEN) or attachment data, uploads to Vercel Blob at content-attachments/{prefix}/{Date.now()}-{filename} with access: "public", and returns { songUrl, imageUrl }. Per-attachment errors are caught and logged.
Mention handler
lib/agents/content/handlers/registerOnNewMention.ts
Now calls and awaits extractMessageAttachments, annotates ack/thread text when songUrl/imageUrl exist, merges songUrl into allSongs, and conditionally adds images: [imageUrl] to the payload sent to triggerCreateContent.
Validation & types
lib/content/validateCreateContentBody.ts, lib/trigger/triggerCreateContent.ts
Added optional images?: string[] to the create-content schema/type and TriggerCreateContentPayload. songs?: string[] semantics updated to permit public URLs in addition to song slugs.
Handler payload
lib/content/createContentHandler.ts
createContentHandler now conditionally spreads images into the payload only when validated.images exists and is non-empty; JSDoc param updated.

Sequence Diagram

sequenceDiagram
    participant Slack as Slack Message
    participant Handler as registerOnNewMention
    participant Extractor as extractMessageAttachments
    participant Blob as Vercel Blob
    participant Validator as validateCreateContentBody
    participant Trigger as triggerCreateContent
    participant Task as Content Creation Task

    Slack->>Handler: onNewMention(message with attachments)
    Handler->>Extractor: extractMessageAttachments(message)
    Extractor->>Blob: upload audio (if present)
    Blob-->>Extractor: audio URL
    Extractor->>Blob: upload image (if present)
    Blob-->>Extractor: image URL
    Extractor-->>Handler: { songUrl, imageUrl }
    Handler->>Validator: validateCreateContentBody(payload with songUrl/imageUrl)
    Validator-->>Handler: validated payload
    Handler->>Trigger: triggerCreateContent(payload with songs/images)
    Trigger->>Task: tasks.trigger(CREATE_CONTENT_TASK_ID, payload)
    Task-->>Trigger: triggered
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

A snippet pulled from Slack’s small chest,
Bottled to a blob and set to rest.
Song and face in tidy rows,
Validated, merged, then off it goes. 🎵✨

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request violates SOLID principles and clean code practices. Critical Blob handling error on line 103 fails at runtime; magic strings, code duplication, and Single Responsibility violations compromise maintainability. Fix Blob handling by awaiting arrayBuffer(); extract magic strings and regex to constants; refactor resolveAttachmentUrl into separate concern-focused functions; create utility for repeated conditional spreads; use proper logging.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/slack-media-attachments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Mar 31, 2026 10:48pm

Request Review

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

🧹 Nitpick comments (3)
lib/agents/content/extractMessageAttachments.ts (2)

62-63: Filename collisions may overwrite existing attachments.

Using only attachment.name (or the fallback "attachment") for the blob path could cause collisions when multiple users upload files with the same name. Consider adding a unique identifier (e.g., timestamp, UUID) to prevent overwrites.

♻️ Proposed fix to add uniqueness
+import { randomUUID } from "crypto";
+
 async function uploadAttachment(attachment: Attachment, prefix: string): Promise<string> {
   const data = attachment.fetchData ? await attachment.fetchData() : (attachment.data as Buffer);

   const filename = attachment.name ?? "attachment";
-  const blobPath = `content-attachments/${prefix}/${filename}`;
+  const uniqueId = randomUUID().slice(0, 8);
+  const blobPath = `content-attachments/${prefix}/${uniqueId}-${filename}`;

   const blob = await put(blobPath, data, { access: "public" });
   return blob.url;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` around lines 62 - 63, The
blob path uses a non-unique filename (const filename and blobPath) which can
cause collisions; modify extractMessageAttachments to append a unique identifier
(e.g., timestamp or UUID) to the filename before building blobPath (for example
derive a safe base name from attachment.name and add `-${uuid}` or
`-${Date.now()}`) so blobPath becomes unique per upload and prevents overwriting
existing attachments.

26-51: Consider handling partial upload failures gracefully.

If the audio upload succeeds but the image upload fails, the function throws without returning the successful audio URL. Depending on requirements, you may want to return partial results or handle each upload independently.

♻️ Optional: Independent error handling for each upload
 export async function extractMessageAttachments(
   message: MessageWithAttachments,
 ): Promise<ExtractedAttachments> {
   const result: ExtractedAttachments = {
     attachedAudioUrl: null,
     attachedImageUrl: null,
   };

   const attachments = message.attachments;
   if (!attachments || attachments.length === 0) {
     return result;
   }

   const audioAttachment = attachments.find(a => a.type === "audio");
   const imageAttachment = attachments.find(a => a.type === "image");

   if (audioAttachment) {
-    result.attachedAudioUrl = await uploadAttachment(audioAttachment, "audio");
+    try {
+      result.attachedAudioUrl = await uploadAttachment(audioAttachment, "audio");
+    } catch (error) {
+      console.error("[extractMessageAttachments] Failed to upload audio:", error);
+    }
   }

   if (imageAttachment) {
-    result.attachedImageUrl = await uploadAttachment(imageAttachment, "image");
+    try {
+      result.attachedImageUrl = await uploadAttachment(imageAttachment, "image");
+    } catch (error) {
+      console.error("[extractMessageAttachments] Failed to upload image:", error);
+    }
   }

   return result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` around lines 26 - 51, The
extractMessageAttachments function currently lets one failing upload prevent
returning any successful uploads; modify extractMessageAttachments so each call
to uploadAttachment (for audio and image) is wrapped in its own try/catch, set
result.attachedAudioUrl and result.attachedImageUrl only on success, and log or
collect the individual upload errors instead of rethrowing so the function can
return partial results; keep the existing return shape (ExtractedAttachments)
and ensure you reference uploadAttachment, attachedAudioUrl, attachedImageUrl,
and the extractMessageAttachments function when making the change.
lib/agents/content/handlers/registerOnNewMention.ts (1)

18-135: Consider extracting sub-handlers to reduce function length.

The onNewMention callback is approximately 115 lines, exceeding the 50-line guideline. While the logic is well-organized with clear sections, extracting some responsibilities (e.g., resolveGitHubRepo, buildAcknowledgmentText, triggerAndPoll) into separate helper functions would improve maintainability and testability.

This is an optional refactor that could be addressed in a follow-up PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handlers/registerOnNewMention.ts` around lines 18 - 135,
The onNewMention callback in registerOnNewMention is too long; extract
responsibilities into helpers to reduce length: implement a resolveGitHubRepo
helper used in the block that calls
getArtistContentReadiness/selectAccountSnapshots to return githubRepo (keep
current error handling but move it), a buildAcknowledgmentText helper that
accepts {artistSlug, template, batch, lipsync, songs, attachedAudioUrl,
attachedImageUrl} and returns the posted string, and a triggerAndPoll helper
that accepts the payload and thread (or runIds + callbackThreadId) to run
Promise.allSettled, collect runIds, set thread state, and call
triggerPollContentRun with proper error handling; then replace the corresponding
inline sections in registerOnNewMention/onNewMention with calls to these helpers
to shrink the callback and improve testability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Around line 59-67: In uploadAttachment, validate that attachment.fetchData()
or attachment.data yields non-null Buffer before calling put(): check
(attachment.fetchData ? await attachment.fetchData() : attachment.data) is
defined and of expected type and throw or return a clear error if not; also wrap
the put(blobPath, data, { access: "public" }) call in a try/catch to surface a
helpful error (including the attachment.name and blobPath) or perform
cleanup/rollback as appropriate so upload failures don’t leave partial state;
reference symbols: uploadAttachment, attachment.fetchData, attachment.data, put,
blobPath, blob.url.

---

Nitpick comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Around line 62-63: The blob path uses a non-unique filename (const filename
and blobPath) which can cause collisions; modify extractMessageAttachments to
append a unique identifier (e.g., timestamp or UUID) to the filename before
building blobPath (for example derive a safe base name from attachment.name and
add `-${uuid}` or `-${Date.now()}`) so blobPath becomes unique per upload and
prevents overwriting existing attachments.
- Around line 26-51: The extractMessageAttachments function currently lets one
failing upload prevent returning any successful uploads; modify
extractMessageAttachments so each call to uploadAttachment (for audio and image)
is wrapped in its own try/catch, set result.attachedAudioUrl and
result.attachedImageUrl only on success, and log or collect the individual
upload errors instead of rethrowing so the function can return partial results;
keep the existing return shape (ExtractedAttachments) and ensure you reference
uploadAttachment, attachedAudioUrl, attachedImageUrl, and the
extractMessageAttachments function when making the change.

In `@lib/agents/content/handlers/registerOnNewMention.ts`:
- Around line 18-135: The onNewMention callback in registerOnNewMention is too
long; extract responsibilities into helpers to reduce length: implement a
resolveGitHubRepo helper used in the block that calls
getArtistContentReadiness/selectAccountSnapshots to return githubRepo (keep
current error handling but move it), a buildAcknowledgmentText helper that
accepts {artistSlug, template, batch, lipsync, songs, attachedAudioUrl,
attachedImageUrl} and returns the posted string, and a triggerAndPoll helper
that accepts the payload and thread (or runIds + callbackThreadId) to run
Promise.allSettled, collect runIds, set thread state, and call
triggerPollContentRun with proper error handling; then replace the corresponding
inline sections in registerOnNewMention/onNewMention with calls to these helpers
to shrink the callback and improve testability.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff05264a-d281-4c40-a313-2f007b81795f

📥 Commits

Reviewing files that changed from the base of the PR and between 4afc4cf and d0acd04.

⛔ Files ignored due to path filters (2)
  • lib/agents/content/__tests__/extractMessageAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/registerOnNewMention.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/agents/content/extractMessageAttachments.ts
  • lib/agents/content/handlers/registerOnNewMention.ts
  • lib/content/createContentHandler.ts
  • lib/content/validateCreateContentBody.ts
  • lib/trigger/triggerCreateContent.ts

sweetmantech added a commit to recoupable/docs that referenced this pull request Mar 31, 2026
Document the new optional attachment parameters for POST /api/content/create.
These allow callers to provide custom audio and face-guide images that override
the defaults from the artist's GitHub repo.

Companion to recoupable/api#381 and recoupable/tasks#116.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…geAttachments

Address CodeRabbit review feedback:
- Guard against undefined attachment data
- Gracefully handle upload failures without crashing the handler
- Add timestamp prefix to blob paths to prevent filename collisions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sweetmantech added a commit to recoupable/docs that referenced this pull request Mar 31, 2026
* docs: add attached_audio_url and attached_image_url to content/create

Document the new optional attachment parameters for POST /api/content/create.
These allow callers to provide custom audio and face-guide images that override
the defaults from the artist's GitHub repo.

Companion to recoupable/api#381 and recoupable/tasks#116.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: implement design feedback — songs accepts URLs, images array replaces attached_image_url

Address sweetmantech's review:
- Extend songs param to accept both slugs and public URLs (no separate attached_audio_url)
- Replace attached_image_url with images array to match songs pattern (KISS)
- Add Create Content to Guides navbar above Create Sandbox

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…places attachedAudioUrl/attachedImageUrl

- extractMessageAttachments returns songUrl/imageUrl (not attachedAudioUrl/attachedImageUrl)
- registerOnNewMention merges songUrl into songs array, imageUrl into images array
- validateCreateContentBody accepts images array, removes attached_audio_url/attached_image_url
- triggerCreateContent payload uses songs/images (matches tasks schema)
- createContentHandler passes images array

TDD: 1659/1659 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

🧹 Nitpick comments (1)
lib/agents/content/handlers/registerOnNewMention.ts (1)

82-84: Deduplicate merged songs to avoid duplicate downstream requests.

If a prompt already includes the same URL as songUrl, allSongs can contain duplicates.

♻️ Proposed fix
-      const allSongs = [...(songs ?? []), ...(songUrl ? [songUrl] : [])];
+      const allSongs = Array.from(new Set([...(songs ?? []), ...(songUrl ? [songUrl] : [])]));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handlers/registerOnNewMention.ts` around lines 82 - 84,
The merged songs array (allSongs) can contain duplicates when songs already
includes songUrl; update the assignment in registerOnNewMention to deduplicate
the combined list: build the merged array from songs and songUrl (using the
existing variables songs and songUrl) and then remove duplicates (e.g., via
Array.from(new Set(...)) or a filter by indexOf) so downstream processing only
receives unique URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/handlers/registerOnNewMention.ts`:
- Around line 29-31: In registerOnNewMention, avoid performing external uploads
via extractMessageAttachments before validating required prerequisites
(artist/repo checks) and make uploads optional: move the call to
extractMessageAttachments to after the validation logic that checks artist and
repository existence, and wrap the call in a try/catch so any errors from
extractMessageAttachments do not throw but instead log a warning and set
songUrl/imageUrl to undefined (soft-fail) so the handler can continue; reference
extractMessageAttachments and the registerOnNewMention handler when applying
this change.

---

Nitpick comments:
In `@lib/agents/content/handlers/registerOnNewMention.ts`:
- Around line 82-84: The merged songs array (allSongs) can contain duplicates
when songs already includes songUrl; update the assignment in
registerOnNewMention to deduplicate the combined list: build the merged array
from songs and songUrl (using the existing variables songs and songUrl) and then
remove duplicates (e.g., via Array.from(new Set(...)) or a filter by indexOf) so
downstream processing only receives unique URLs.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7221b450-35b4-4fe9-9c95-8aaf9d435912

📥 Commits

Reviewing files that changed from the base of the PR and between c324aff and 2aa251a.

⛔ Files ignored due to path filters (2)
  • lib/agents/content/__tests__/extractMessageAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/registerOnNewMention.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/agents/content/extractMessageAttachments.ts
  • lib/agents/content/handlers/registerOnNewMention.ts
  • lib/content/createContentHandler.ts
  • lib/content/validateCreateContentBody.ts
  • lib/trigger/triggerCreateContent.ts
✅ Files skipped from review due to trivial changes (1)
  • lib/content/createContentHandler.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/trigger/triggerCreateContent.ts
  • lib/content/validateCreateContentBody.ts
  • lib/agents/content/extractMessageAttachments.ts

Comment on lines +29 to +31
// Extract audio/image attachments from the Slack message
const { songUrl, imageUrl } = await extractMessageAttachments(message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move attachment extraction after prerequisite validation and soft-fail on upload errors.

Line 30 triggers external upload before artist/repo checks. If validation fails later, the handler exits but files may already be publicly uploaded. Also, any extraction/upload error currently aborts the whole request, even though attachments are optional.

💡 Proposed fix
-      // Extract audio/image attachments from the Slack message
-      const { songUrl, imageUrl } = await extractMessageAttachments(message);
+      let songUrl: string | undefined;
+      let imageUrl: string | undefined;
...
       // Resolve GitHub repo
       let githubRepo: string;
       try {
         const readiness = await getArtistContentReadiness({
           accountId,
           artistAccountId,
           artistSlug,
         });
         githubRepo = readiness.githubRepo;
       } catch {
         const snapshots = await selectAccountSnapshots(artistAccountId);
         const repo = snapshots?.[0]?.github_repo;
         if (!repo) {
           await thread.post(
             `No GitHub repository found for artist \`${artistSlug}\`. Content creation requires a configured repo.`,
           );
           return;
         }
         githubRepo = repo;
       }
+
+      // Extract audio/image attachments only after request prerequisites pass.
+      try {
+        ({ songUrl, imageUrl } = await extractMessageAttachments(message));
+      } catch (attachmentError) {
+        console.warn("[content-agent] attachment extraction failed:", attachmentError);
+      }
As per coding guidelines, `lib/**/*.ts`: For domain functions, ensure "Proper error handling" and "Avoid side effects".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handlers/registerOnNewMention.ts` around lines 29 - 31, In
registerOnNewMention, avoid performing external uploads via
extractMessageAttachments before validating required prerequisites (artist/repo
checks) and make uploads optional: move the call to extractMessageAttachments to
after the validation logic that checks artist and repository existence, and wrap
the call in a try/catch so any errors from extractMessageAttachments do not
throw but instead log a warning and set songUrl/imageUrl to undefined
(soft-fail) so the handler can continue; reference extractMessageAttachments and
the registerOnNewMention handler when applying this change.

Slack classifies uploaded images/audio as type: "file", not "image"/"audio".
Check mimeType (e.g. "image/jpeg", "audio/mpeg") to correctly detect media
from Slack file uploads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log attachment metadata (type, name, mimeType, url, fetchData presence),
fetched data size, and uploaded Blob URL to trace why files are corrupt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blob upload was corrupting both audio and image files (empty content).
Now uses attachment.url directly when available, skipping the
download+reupload round-trip. Falls back to Blob only when no URL exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vercel Blob was serving files with wrong content type when contentType
wasn't explicitly set. Now passes attachment.mimeType (or sensible
default) to put(). Slack URLs are private so we must keep the Blob
upload path but with correct content type headers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

🧹 Nitpick comments (1)
lib/agents/content/extractMessageAttachments.ts (1)

46-60: Deduplicate audio/image resolution branches.

The two try/catch blocks are structurally identical and should be consolidated to reduce branching duplication and maintenance overhead.

♻️ DRY refactor sketch
-  if (audioAttachment) {
-    try {
-      result.songUrl = await resolveAttachmentUrl(audioAttachment, "audio");
-    } catch (error) {
-      console.error("[content-agent] Failed to resolve audio attachment:", error);
-    }
-  }
-
-  if (imageAttachment) {
-    try {
-      result.imageUrl = await resolveAttachmentUrl(imageAttachment, "image");
-    } catch (error) {
-      console.error("[content-agent] Failed to resolve image attachment:", error);
-    }
-  }
+  const resolutions: Array<{
+    attachment: Attachment | undefined;
+    prefix: "audio" | "image";
+    key: keyof ExtractedAttachments;
+  }> = [
+    { attachment: audioAttachment, prefix: "audio", key: "songUrl" },
+    { attachment: imageAttachment, prefix: "image", key: "imageUrl" },
+  ];
+
+  for (const { attachment, prefix, key } of resolutions) {
+    if (!attachment) continue;
+    try {
+      result[key] = await resolveAttachmentUrl(attachment, prefix);
+    } catch (error) {
+      console.error(`[content-agent] Failed to resolve ${prefix} attachment:`, error);
+    }
+  }

As per coding guidelines, "Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` around lines 46 - 60, The
audio and image resolution try/catch blocks are duplicated; refactor by
extracting the shared logic into a small helper (e.g.,
resolveAndAssignAttachment) that accepts an attachment object, a type string
("audio" | "image"), and a target key (e.g., "songUrl" or "imageUrl"), calls
resolveAttachmentUrl(attachment, type) inside a single try/catch, and assigns
the result to result[targetKey], logging errors the same way; then replace the
two blocks that reference audioAttachment/imageAttachment with calls to this
helper to remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Around line 73-87: The logs in extractMessageAttachments.ts currently print
full URLs (attachment.url and blob.url) which can leak signed query params;
replace those console.log calls in the branch that returns attachment.url and
after put(...) so they do not emit the full URLs—log only non-sensitive metadata
(e.g., attachment.name, filename, blobPath, or a redacted indicator) or a
success message, and if necessary implement a redactUrl helper to strip query
params before logging; update references to attachment.url and blob.url
accordingly so raw URLs are never written to logs.

---

Nitpick comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Around line 46-60: The audio and image resolution try/catch blocks are
duplicated; refactor by extracting the shared logic into a small helper (e.g.,
resolveAndAssignAttachment) that accepts an attachment object, a type string
("audio" | "image"), and a target key (e.g., "songUrl" or "imageUrl"), calls
resolveAttachmentUrl(attachment, type) inside a single try/catch, and assigns
the result to result[targetKey], logging errors the same way; then replace the
two blocks that reference audioAttachment/imageAttachment with calls to this
helper to remove duplication.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 375b5cd6-6fd5-4d37-922e-24cf0ca013df

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa251a and 637388b.

⛔ Files ignored due to path filters (1)
  • lib/agents/content/__tests__/extractMessageAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/agents/content/extractMessageAttachments.ts

Chat SDK fetchData() returns Slack HTML login page instead of file content.
Now downloads directly from attachment.url using SLACK_CONTENT_BOT_TOKEN
Bearer auth, then uploads to Blob with correct contentType.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slack attachment.url is a files-tmb (thumbnail) URL that returns HTML.
Convert to files-pri/download URL format with bot token Bearer auth
to get actual file content. Verify response isn't HTML before uploading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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)
lib/agents/content/extractMessageAttachments.ts (1)

113-113: Default content-type assumptions may be incorrect for certain file formats.

Defaulting to audio/mpeg for audio and image/png for images may cause issues when the actual file is a .wav, .ogg, .webp, or .gif. Consider deriving the content type from the filename extension as a fallback, or logging a warning when falling back to defaults.

♻️ Proposed enhancement with extension-based fallback
+function inferContentType(filename: string, prefix: string): string {
+  const ext = filename.split(".").pop()?.toLowerCase();
+  const mimeMap: Record<string, string> = {
+    mp3: "audio/mpeg",
+    wav: "audio/wav",
+    ogg: "audio/ogg",
+    m4a: "audio/mp4",
+    png: "image/png",
+    jpg: "image/jpeg",
+    jpeg: "image/jpeg",
+    gif: "image/gif",
+    webp: "image/webp",
+  };
+  return mimeMap[ext ?? ""] ?? (prefix === "audio" ? "audio/mpeg" : "image/png");
+}
+
   const filename = attachment.name ?? "attachment";
   const blobPath = `content-attachments/${prefix}/${Date.now()}-${filename}`;
-  const contentType = attachment.mimeType ?? (prefix === "audio" ? "audio/mpeg" : "image/png");
+  const contentType = attachment.mimeType ?? inferContentType(filename, prefix);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` at line 113, The current
fallback for contentType in extractMessageAttachments.ts uses a fixed audio/mpeg
or image/png which mislabels .wav/.ogg/.webp/.gif files; update the logic in the
extractMessageAttachments function where contentType is set (currently using
attachment.mimeType ?? (prefix === "audio" ? "audio/mpeg" : "image/png")) to
instead attempt to derive the MIME type from attachment.filename extension
(e.g., using a small extension-to-mime map or a mime lookup utility) before
falling back to the generic audio/image defaults, and add a processLogger.warn
or similar when you must use the generic fallback so unexpected types are
visible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Around line 101-104: The current logic in extractMessageAttachments that sets
`data` from `raw` (using `attachment.fetchData` or `attachment.data`)
incorrectly casts a `Blob` to an `ArrayBuffer`; update the branch that
constructs `data` so it detects a Blob and calls `await raw.arrayBuffer()`
before converting to a Buffer: i.e., if `raw` is already a Buffer keep it, if it
is a Blob await raw.arrayBuffer() then Buffer.from(...) , otherwise handle other
ArrayBuffer-like types via Buffer.from; adjust the code paths around
`attachment.fetchData`, `attachment.data`, and the `data` variable so Blob
inputs are correctly awaited and converted.
- Line 1: Run Prettier to fix the formatting issue in
lib/agents/content/extractMessageAttachments.ts; specifically reformat the file
(including the import line "import { put } from \"@vercel/blob\";") by executing
prettier --write so the file conforms to project style and clears the CI
Prettier warning.

---

Nitpick comments:
In `@lib/agents/content/extractMessageAttachments.ts`:
- Line 113: The current fallback for contentType in extractMessageAttachments.ts
uses a fixed audio/mpeg or image/png which mislabels .wav/.ogg/.webp/.gif files;
update the logic in the extractMessageAttachments function where contentType is
set (currently using attachment.mimeType ?? (prefix === "audio" ? "audio/mpeg" :
"image/png")) to instead attempt to derive the MIME type from
attachment.filename extension (e.g., using a small extension-to-mime map or a
mime lookup utility) before falling back to the generic audio/image defaults,
and add a processLogger.warn or similar when you must use the generic fallback
so unexpected types are visible.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8eac377f-4009-427c-8346-73b97303d9c0

📥 Commits

Reviewing files that changed from the base of the PR and between 637388b and 1a3ff32.

⛔ Files ignored due to path filters (1)
  • lib/agents/content/__tests__/extractMessageAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/agents/content/extractMessageAttachments.ts

@@ -0,0 +1,141 @@
import { put } from "@vercel/blob";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address Prettier formatting warning.

The CI pipeline flagged a formatting issue on this file. Run prettier --write to fix code style inconsistencies before merging.

🧰 Tools
🪛 GitHub Actions: Format Check

[warning] 1-1: Prettier formatting check ([warn]) reported code style issues in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` at line 1, Run Prettier to
fix the formatting issue in lib/agents/content/extractMessageAttachments.ts;
specifically reformat the file (including the import line "import { put } from
\"@vercel/blob\";") by executing prettier --write so the file conforms to
project style and clears the CI Prettier warning.

Comment on lines +101 to +104
if (!data) {
const raw = attachment.fetchData ? await attachment.fetchData() : attachment.data;
if (raw) data = Buffer.isBuffer(raw) ? raw : Buffer.from(raw as unknown as ArrayBuffer);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect handling when attachment.data is a Blob.

The interface declares data?: Buffer | Blob, but if raw is a Blob, the cast raw as unknown as ArrayBuffer won't produce valid data—Blob is not directly convertible to ArrayBuffer without calling await blob.arrayBuffer() first.

🔧 Proposed fix to handle Blob correctly
   // Fallback to fetchData / data if Slack download didn't work
   if (!data) {
     const raw = attachment.fetchData ? await attachment.fetchData() : attachment.data;
-    if (raw) data = Buffer.isBuffer(raw) ? raw : Buffer.from(raw as unknown as ArrayBuffer);
+    if (raw) {
+      if (Buffer.isBuffer(raw)) {
+        data = raw;
+      } else if (raw instanceof Blob) {
+        data = Buffer.from(await raw.arrayBuffer());
+      }
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!data) {
const raw = attachment.fetchData ? await attachment.fetchData() : attachment.data;
if (raw) data = Buffer.isBuffer(raw) ? raw : Buffer.from(raw as unknown as ArrayBuffer);
}
if (!data) {
const raw = attachment.fetchData ? await attachment.fetchData() : attachment.data;
if (raw) {
if (Buffer.isBuffer(raw)) {
data = raw;
} else if (raw instanceof Blob) {
data = Buffer.from(await raw.arrayBuffer());
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/extractMessageAttachments.ts` around lines 101 - 104, The
current logic in extractMessageAttachments that sets `data` from `raw` (using
`attachment.fetchData` or `attachment.data`) incorrectly casts a `Blob` to an
`ArrayBuffer`; update the branch that constructs `data` so it detects a Blob and
calls `await raw.arrayBuffer()` before converting to a Buffer: i.e., if `raw` is
already a Buffer keep it, if it is a Blob await raw.arrayBuffer() then
Buffer.from(...) , otherwise handle other ArrayBuffer-like types via
Buffer.from; adjust the code paths around `attachment.fetchData`,
`attachment.data`, and the `data` variable so Blob inputs are correctly awaited
and converted.

…oad/

The /download/ suffix caused 404s. Slack files-pri URLs work with just
the team-fileID path + Bearer token auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cess

The attachment.url from Chat SDK is a thumbnail URL (files-tmb) that
doesn't serve actual file content. Now extracts the Slack file ID,
calls files.info to get url_private_download, then downloads with
Bearer token auth. This is the official Slack file download flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* then downloads with Bearer token auth. The attachment.url from the
* Chat SDK is a thumbnail URL that doesn't serve actual file content.
*/
async function resolveAttachmentUrl(attachment: Attachment, prefix: string): Promise<string | null> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SRP - new lib file for resolveAttachmentUrl and any function with a name different from the file name.

* Extracts the Slack file ID (e.g. F0APMKTKG9M) from a Slack file URL.
* URL format: files-tmb/TEAMID-FILEID-HASH/name or files-pri/TEAMID-FILEID/name
*/
function extractSlackFileId(url: string): string | null {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SRP - new lib file for extractSlackFileId and any function with a name different from the file name.

upscale: validated.upscale,
githubRepo,
songs: validated.songs,
...(validated.images && validated.images.length > 0 && { images: validated.images }),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

KISS - Why is images logic more complex than the songs logic?

…actSlackFileId

Address sweetmantech review:
- SRP: each function in its own file
- KISS: images passed through same as songs in createContentHandler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* @param token
*/
export async function downloadSlackFile(fileId: string, token: string): Promise<Buffer | null> {
console.log(`[content-agent] Fetching file info for ${fileId}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove dev logging.

const contentType = attachment.mimeType ?? (prefix === "audio" ? "audio/mpeg" : "image/png");

const blob = await put(blobPath, data, { access: "public", contentType });
console.log(`[content-agent] Uploaded to Blob: ${blob.url}, size=${data.byteLength}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove dev logging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slack utilities belong with other Slack helpers, not in agents/content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech merged commit 54667b3 into test Mar 31, 2026
4 of 5 checks passed
@sweetmantech sweetmantech mentioned this pull request Mar 31, 2026
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