From 186dc0363fb61cdc9048d4b3b9eae9e3d53015bb Mon Sep 17 00:00:00 2001 From: Marcus Castro Date: Thu, 12 Feb 2026 02:09:09 -0300 Subject: [PATCH] fix: default MIME type for WhatsApp voice messages when Baileys omits it (#14444) --- src/web/inbound/media.node.test.ts | 101 +++++++++++++++++++++++++++++ src/web/inbound/media.ts | 39 +++++++++-- 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 src/web/inbound/media.node.test.ts diff --git a/src/web/inbound/media.node.test.ts b/src/web/inbound/media.node.test.ts new file mode 100644 index 0000000000..5e9b7b991b --- /dev/null +++ b/src/web/inbound/media.node.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it, vi } from "vitest"; + +const { normalizeMessageContent, downloadMediaMessage } = vi.hoisted(() => ({ + normalizeMessageContent: vi.fn((msg: unknown) => msg), + downloadMediaMessage: vi.fn().mockResolvedValue(Buffer.from("fake-media-data")), +})); + +vi.mock("@whiskeysockets/baileys", () => ({ + normalizeMessageContent, + downloadMediaMessage, +})); + +import { downloadInboundMedia } from "./media.js"; + +const mockSock = { + updateMediaMessage: vi.fn(), + logger: { child: () => ({}) }, +} as never; + +describe("downloadInboundMedia", () => { + it("returns undefined for messages without media", async () => { + const msg = { message: { conversation: "hello" } } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeUndefined(); + }); + + it("uses explicit mimetype from audioMessage when present", async () => { + const msg = { + message: { audioMessage: { mimetype: "audio/mp4", ptt: true } }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("audio/mp4"); + }); + + it("defaults to audio/ogg for voice messages without explicit MIME", async () => { + const msg = { + message: { audioMessage: { ptt: true } }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("audio/ogg; codecs=opus"); + }); + + it("defaults to audio/ogg for audio messages without MIME or ptt flag", async () => { + const msg = { + message: { audioMessage: {} }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("audio/ogg; codecs=opus"); + }); + + it("uses explicit mimetype from imageMessage when present", async () => { + const msg = { + message: { imageMessage: { mimetype: "image/png" } }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("image/png"); + }); + + it("defaults to image/jpeg for images without explicit MIME", async () => { + const msg = { + message: { imageMessage: {} }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("image/jpeg"); + }); + + it("defaults to video/mp4 for video messages without explicit MIME", async () => { + const msg = { + message: { videoMessage: {} }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("video/mp4"); + }); + + it("defaults to image/webp for sticker messages without explicit MIME", async () => { + const msg = { + message: { stickerMessage: {} }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("image/webp"); + }); + + it("preserves fileName from document messages", async () => { + const msg = { + message: { + documentMessage: { mimetype: "application/pdf", fileName: "report.pdf" }, + }, + } as never; + const result = await downloadInboundMedia(msg, mockSock); + expect(result).toBeDefined(); + expect(result?.mimetype).toBe("application/pdf"); + expect(result?.fileName).toBe("report.pdf"); + }); +}); diff --git a/src/web/inbound/media.ts b/src/web/inbound/media.ts index 387eda9462..68650cde3d 100644 --- a/src/web/inbound/media.ts +++ b/src/web/inbound/media.ts @@ -8,6 +8,37 @@ function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | un return normalized; } +/** + * Resolve the MIME type for an inbound media message. + * Falls back to WhatsApp's standard formats when Baileys omits the MIME. + */ +function resolveMediaMimetype(message: proto.IMessage): string | undefined { + const explicit = + message.imageMessage?.mimetype ?? + message.videoMessage?.mimetype ?? + message.documentMessage?.mimetype ?? + message.audioMessage?.mimetype ?? + message.stickerMessage?.mimetype ?? + undefined; + if (explicit) { + return explicit; + } + // WhatsApp voice messages (PTT) and audio use OGG Opus by default + if (message.audioMessage) { + return "audio/ogg; codecs=opus"; + } + if (message.imageMessage) { + return "image/jpeg"; + } + if (message.videoMessage) { + return "video/mp4"; + } + if (message.stickerMessage) { + return "image/webp"; + } + return undefined; +} + export async function downloadInboundMedia( msg: proto.IWebMessageInfo, sock: Awaited>, @@ -16,13 +47,7 @@ export async function downloadInboundMedia( if (!message) { return undefined; } - const mimetype = - message.imageMessage?.mimetype ?? - message.videoMessage?.mimetype ?? - message.documentMessage?.mimetype ?? - message.audioMessage?.mimetype ?? - message.stickerMessage?.mimetype ?? - undefined; + const mimetype = resolveMediaMimetype(message); const fileName = message.documentMessage?.fileName ?? undefined; if ( !message.imageMessage &&