diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f6ea8d10b..a1522443aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez. - Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov. - Telegram: recover proactive sends when stale topic thread IDs are used by retrying without `message_thread_id`. (#11620) - Telegram: render markdown spoilers with `` HTML tags. (#11543) Thanks @ezhikkk. diff --git a/src/web/inbound.media.test.ts b/src/web/inbound.media.test.ts index 91e37a5b4f3..eb23d887b02 100644 --- a/src/web/inbound.media.test.ts +++ b/src/web/inbound.media.test.ts @@ -88,6 +88,11 @@ vi.mock("./session.js", () => { import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js"; +async function waitForMessage(onMessage: ReturnType) { + await vi.waitFor(() => expect(onMessage).toHaveBeenCalledTimes(1)); + return onMessage.mock.calls[0][0]; +} + describe("web inbound media saves with extension", () => { beforeEach(() => { saveMediaBufferSpy.mockClear(); @@ -125,16 +130,7 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); - // Allow a brief window for the async handler to fire on slower hosts. - for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - expect(onMessage).toHaveBeenCalledTimes(1); - const msg = onMessage.mock.calls[0][0]; + const msg = await waitForMessage(onMessage); const mediaPath = msg.mediaPath; expect(mediaPath).toBeDefined(); expect(path.extname(mediaPath as string)).toBe(".jpg"); @@ -179,15 +175,7 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); - for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - expect(onMessage).toHaveBeenCalledTimes(1); - const msg = onMessage.mock.calls[0][0]; + const msg = await waitForMessage(onMessage); expect(msg.chatType).toBe("group"); expect(msg.mentionedJids).toEqual(["999@s.whatsapp.net"]); @@ -221,18 +209,44 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); - for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - expect(onMessage).toHaveBeenCalledTimes(1); + await waitForMessage(onMessage); expect(saveMediaBufferSpy).toHaveBeenCalled(); const lastCall = saveMediaBufferSpy.mock.calls.at(-1); expect(lastCall?.[3]).toBe(1 * 1024 * 1024); await listener.close(); }); + + it("passes document filenames to saveMediaBuffer", async () => { + const onMessage = vi.fn(); + const listener = await monitorWebInbox({ verbose: false, onMessage }); + const { createWaSocket } = await import("./session.js"); + const realSock = await ( + createWaSocket as unknown as () => Promise<{ + ev: import("node:events").EventEmitter; + }> + )(); + + const fileName = "invoice.pdf"; + const upsert = { + type: "notify", + messages: [ + { + key: { id: "doc1", fromMe: false, remoteJid: "333@s.whatsapp.net" }, + message: { documentMessage: { mimetype: "application/pdf", fileName } }, + messageTimestamp: 1_700_000_004, + }, + ], + }; + + realSock.ev.emit("messages.upsert", upsert); + + const msg = await waitForMessage(onMessage); + expect(msg.mediaFileName).toBe(fileName); + expect(saveMediaBufferSpy).toHaveBeenCalled(); + const lastCall = saveMediaBufferSpy.mock.calls.at(-1); + expect(lastCall?.[4]).toBe(fileName); + + await listener.close(); + }); }); diff --git a/src/web/inbound/media.ts b/src/web/inbound/media.ts index b99721ffb2d..387eda9462d 100644 --- a/src/web/inbound/media.ts +++ b/src/web/inbound/media.ts @@ -11,7 +11,7 @@ function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | un export async function downloadInboundMedia( msg: proto.IWebMessageInfo, sock: Awaited>, -): Promise<{ buffer: Buffer; mimetype?: string } | undefined> { +): Promise<{ buffer: Buffer; mimetype?: string; fileName?: string } | undefined> { const message = unwrapMessage(msg.message as proto.IMessage | undefined); if (!message) { return undefined; @@ -23,6 +23,7 @@ export async function downloadInboundMedia( message.audioMessage?.mimetype ?? message.stickerMessage?.mimetype ?? undefined; + const fileName = message.documentMessage?.fileName ?? undefined; if ( !message.imageMessage && !message.videoMessage && @@ -42,7 +43,7 @@ export async function downloadInboundMedia( logger: sock.logger, }, ); - return { buffer, mimetype }; + return { buffer, mimetype, fileName }; } catch (err) { logVerbose(`downloadMediaMessage failed: ${String(err)}`); return undefined; diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index c7cfabeba33..b21813e6f06 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -253,6 +253,7 @@ export async function monitorWebInbox(options: { let mediaPath: string | undefined; let mediaType: string | undefined; + let mediaFileName: string | undefined; try { const inboundMedia = await downloadInboundMedia(msg as proto.IWebMessageInfo, sock); if (inboundMedia) { @@ -266,9 +267,11 @@ export async function monitorWebInbox(options: { inboundMedia.mimetype, "inbound", maxBytes, + inboundMedia.fileName, ); mediaPath = saved.path; mediaType = inboundMedia.mimetype; + mediaFileName = inboundMedia.fileName; } } catch (err) { logVerbose(`Inbound media download failed: ${String(err)}`); @@ -293,7 +296,7 @@ export async function monitorWebInbox(options: { const senderName = msg.pushName ?? undefined; inboundLogger.info( - { from, to: selfE164 ?? "me", body, mediaPath, mediaType, timestamp }, + { from, to: selfE164 ?? "me", body, mediaPath, mediaType, mediaFileName, timestamp }, "inbound message", ); const inboundMessage: WebInboundMessage = { @@ -326,6 +329,7 @@ export async function monitorWebInbox(options: { sendMedia, mediaPath, mediaType, + mediaFileName, }; try { const task = Promise.resolve(debouncer.enqueue(inboundMessage)); diff --git a/src/web/inbound/types.ts b/src/web/inbound/types.ts index 5f861fcc8c0..dfac5a27c50 100644 --- a/src/web/inbound/types.ts +++ b/src/web/inbound/types.ts @@ -37,6 +37,7 @@ export type WebInboundMessage = { sendMedia: (payload: AnyMessageContent) => Promise; mediaPath?: string; mediaType?: string; + mediaFileName?: string; mediaUrl?: string; wasMentioned?: boolean; };