Skip to content

support sticker payload#127

Open
clairton wants to merge 1 commit intomainfrom
feature/sticker
Open

support sticker payload#127
clairton wants to merge 1 commit intomainfrom
feature/sticker

Conversation

@clairton
Copy link
Owner

@clairton clairton commented Feb 8, 2026

Summary by CodeRabbit

  • New Features
    • Added support for sticker media in message processing.
    • Added on-device sticker conversion to WebP with optional animation, resizing to sticker dimensions, and enforcement of a maximum sticker size.
  • Chores
    • Adjusted media processing flow to handle stickers end-to-end during download and saving.

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Adds sticker handling across media processing: sticker is routed through the existing transformer media branch, a new convertToWebpSticker utility (with size limit and animated support) is added, and incoming media download flow converts eligible stickers to WebP before saving.

Changes

Cohort / File(s) Summary
Transformer — media handling
src/services/transformer.ts
Adds 'sticker' to the media case in toBaileysMessageContent, treating stickers like other media (extract link, mime, filename, caption).
Sticker conversion utility
src/utils/sticker_convert.ts
New module exporting StickerConvertOptions, MAX_STICKER_BYTES, and convertToWebpSticker(input: Buffer, opts?) using sharp to resize to ≤512×512 and emit WebP (lossless for static, lossy for animated).
Incoming job — download & convert
src/jobs/incoming.ts
When downloading media, if payload.type === 'sticker' and size ≤ MAX_STICKER_BYTES, convert buffer to WebP (animated if original was GIF) via convertToWebpSticker and set mimetype to image/webp before saving/updating payload.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant IncomingJob
  participant Transformer
  participant StickerUtil
  participant Sharp
  Client->>IncomingJob: Upload/send media (type: sticker)
  IncomingJob->>IncomingJob: fetch media buffer & determine mimetype/size
  alt size ≤ MAX_STICKER_BYTES and type is sticker
    IncomingJob->>StickerUtil: convertToWebpSticker(buffer, {animated?})
    StickerUtil->>Sharp: resize & encode WebP
    Sharp-->>StickerUtil: WebP buffer
    StickerUtil-->>IncomingJob: WebP buffer
    IncomingJob->>IncomingJob: set mimetype = image/webp, replace buffer
  end
  IncomingJob->>Transformer: pass media payload (sticker as media)
  Transformer->>Client: produce Baileys message content with sticker media
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I munch a PNG, give it a twirl,
Turn pixels to stickers — watch them whirl,
Static or dancing, snug and small,
WebP delights — I hop and call! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'support sticker payload' directly and clearly describes the main change: adding support for sticker payloads across the transformer, utilities, and job processing layers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/sticker

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.

Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/transformer.ts (1)

467-487: ⚠️ Potential issue | 🟠 Major

Pre-existing fallthrough to contacts case when link is falsy.

Biome correctly flags this: when payload[type].link is falsy (Line 469), the break on Line 484 is inside the if block, so execution falls through into case 'contacts':. This silently produces a malformed contacts response instead of throwing an error. This affects all media types including the newly added sticker.

While this is pre-existing, sticker payloads may be more likely to hit this path (e.g., if a sticker is sent as base64 data rather than a link).

Proposed fix
         response[type] = { url: link }
         break
       }
+      break

     case 'contacts':

Alternatively, throw an error for missing links:

         response[type] = { url: link }
-        break
       }
+      else {
+        throw new Error(`Missing link for ${type} message`)
+      }
+      break

     case 'contacts':

Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/transformer.ts (1)

467-485: ⚠️ Potential issue | 🟡 Minor

Pre-existing fallthrough when link is falsy applies to stickers too.

The static analyzer flags that if payload[type].link is falsy, execution silently falls through into the 'contacts' case (line 487). This isn't new to your change — it affects all media types — but it now also applies to 'sticker'. A sticker payload without a link would be (mis)handled as a contact.

Consider adding an explicit break or throw after the if (link) block so invalid media payloads fail fast rather than silently falling through.

Proposed fix
         response[type] = { url: link }
         break
       }
+      throw new Error(`Missing link for media type ${type}`)
 
     case 'contacts':
🧹 Nitpick comments (1)
src/utils/sticker_convert.ts (1)

7-14: MAX_STICKER_BYTES is defined but the output is never validated against it.

WhatsApp enforces sticker size limits. The constant suggests the intent to guard against oversized output, but convertToWebpSticker doesn't check the result. Lossless WebP for high-res static images can easily exceed 8 MB.

Consider adding a post-conversion size check or a retry with lossy encoding:

Sketch
 export const convertToWebpSticker = async (input: Buffer, opts: StickerConvertOptions = {}) => {
   const image = sharp(input, { animated: !!opts.animated })
-  return image
+  let buf = await image
     .resize(512, 512, { fit: 'inside', withoutEnlargement: true })
     .webp({ lossless: !opts.animated, quality: 80, effort: 4 })
     .toBuffer()
+  if (buf.byteLength > MAX_STICKER_BYTES) {
+    buf = await sharp(input, { animated: !!opts.animated })
+      .resize(512, 512, { fit: 'inside', withoutEnlargement: true })
+      .webp({ lossless: false, quality: 60, effort: 4 })
+      .toBuffer()
+  }
+  if (buf.byteLength > MAX_STICKER_BYTES) {
+    throw new Error(`Sticker exceeds maximum size of ${MAX_STICKER_BYTES} bytes`)
+  }
+  return buf
 }

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/transformer.ts (1)

463-485: ⚠️ Potential issue | 🟡 Minor

Pre-existing fallthrough: when link is falsy, media cases fall through to contacts.

The static analysis tool correctly flags that if link is undefined/empty, the break on line 484 is skipped and execution falls through to the contacts case. This isn't new to this PR (it affects all media types), but adding sticker extends the surface area. A sticker payload without a link would silently fall into contact handling.

Consider adding an explicit break or throw after the if (link) block to prevent this for all media types.

💡 Suggested fix
         response[type] = { url: link }
         break
       }
+      break

     case 'contacts':
🤖 Fix all issues with AI agents
In `@src/jobs/incoming.ts`:
- Around line 69-79: The file is saved using fileName computed from the original
mimetype, so after converting stickers to webp the saved file keeps the wrong
extension; update the logic in the incoming handler (where getMimetype,
mime.extension and fileName are used) to recompute the extension and fileName
after convertToWebpSticker when payload.type === 'sticker' (set mimetype =
'image/webp' already done — ensure you also set a new finalFileName using
mime.extension(mimetype) before calling mediaStore.saveMediaBuffer), and make
sure any downstream reference such as messagePayload uses that finalFileName
instead of the original fileName.
🧹 Nitpick comments (1)
src/jobs/incoming.ts (1)

74-78: No error handling around convertToWebpSticker.

If sharp throws (e.g. corrupt input, unsupported format), the entire job will fail unhandled. Consider wrapping the conversion in a try/catch and falling back to the original buffer so sticker delivery isn't blocked by a conversion failure.

💡 Suggested resilient fallback
         if (payload.type === 'sticker' && buffer.byteLength <= MAX_STICKER_BYTES) {
-          const animated = extension == 'gif'
-          buffer = await convertToWebpSticker(buffer, { animated })
-          mimetype = 'image/webp'
+          try {
+            const animated = extension == 'gif'
+            buffer = await convertToWebpSticker(buffer, { animated })
+            mimetype = 'image/webp'
+          } catch (e) {
+            logger.warn('Failed to convert sticker to webp, using original', e)
+          }
         }

Comment on lines +69 to 79
let mimetype = getMimetype(payload)
const extension = mime.extension(mimetype)
const fileName = `${mediaKey}.${extension}`
const response: Response = await fetch(link, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), method: 'GET' })
const buffer = toBuffer(await response.arrayBuffer())
let buffer: Buffer = toBuffer(await response.arrayBuffer())
if (payload.type === 'sticker' && buffer.byteLength <= MAX_STICKER_BYTES) {
const animated = extension == 'gif'
buffer = await convertToWebpSticker(buffer, { animated })
mimetype = 'image/webp'
}
await mediaStore.saveMediaBuffer(fileName, buffer)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: file saved with original extension after webp conversion.

fileName is computed on line 71 using the original mimetype's extension (e.g. .gif, .png), but after the sticker-to-webp conversion the buffer contains webp data. The file is then saved at line 79 with the mismatched extension, which will cause issues when the media is later served or consumed.

Recompute fileName after conversion so the extension reflects the actual content.

🐛 Proposed fix
         let buffer: Buffer = toBuffer(await response.arrayBuffer())
         if (payload.type === 'sticker' && buffer.byteLength <= MAX_STICKER_BYTES) {
           const animated = extension == 'gif'
           buffer = await convertToWebpSticker(buffer, { animated })
           mimetype = 'image/webp'
         }
-        await mediaStore.saveMediaBuffer(fileName, buffer)
+        const finalExtension = mime.extension(mimetype) || extension
+        const finalFileName = `${mediaKey}.${finalExtension}`
+        await mediaStore.saveMediaBuffer(finalFileName, buffer)

Also update messagePayload to use finalFileName if the filename is referenced downstream.

📝 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
let mimetype = getMimetype(payload)
const extension = mime.extension(mimetype)
const fileName = `${mediaKey}.${extension}`
const response: Response = await fetch(link, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), method: 'GET' })
const buffer = toBuffer(await response.arrayBuffer())
let buffer: Buffer = toBuffer(await response.arrayBuffer())
if (payload.type === 'sticker' && buffer.byteLength <= MAX_STICKER_BYTES) {
const animated = extension == 'gif'
buffer = await convertToWebpSticker(buffer, { animated })
mimetype = 'image/webp'
}
await mediaStore.saveMediaBuffer(fileName, buffer)
let mimetype = getMimetype(payload)
const extension = mime.extension(mimetype)
const fileName = `${mediaKey}.${extension}`
const response: Response = await fetch(link, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), method: 'GET' })
let buffer: Buffer = toBuffer(await response.arrayBuffer())
if (payload.type === 'sticker' && buffer.byteLength <= MAX_STICKER_BYTES) {
const animated = extension == 'gif'
buffer = await convertToWebpSticker(buffer, { animated })
mimetype = 'image/webp'
}
const finalExtension = mime.extension(mimetype) || extension
const finalFileName = `${mediaKey}.${finalExtension}`
await mediaStore.saveMediaBuffer(finalFileName, buffer)
🤖 Prompt for AI Agents
In `@src/jobs/incoming.ts` around lines 69 - 79, The file is saved using fileName
computed from the original mimetype, so after converting stickers to webp the
saved file keeps the wrong extension; update the logic in the incoming handler
(where getMimetype, mime.extension and fileName are used) to recompute the
extension and fileName after convertToWebpSticker when payload.type ===
'sticker' (set mimetype = 'image/webp' already done — ensure you also set a new
finalFileName using mime.extension(mimetype) before calling
mediaStore.saveMediaBuffer), and make sure any downstream reference such as
messagePayload uses that finalFileName instead of the original fileName.

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