Skip to content

Commit 8500000

Browse files
committed
fix: add WhatsApp document fallback extensions
1 parent 94ed68b commit 8500000

7 files changed

Lines changed: 98 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
77
### Fixes
88

99
- WhatsApp: honor forced document delivery for outbound image, GIF, and video media so `forceDocument`/`asDocument` sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.
10+
- WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as `file.pdf` and `file.csv` instead of an extensionless `file`. Thanks @mcaxtr.
1011

1112
## 2026.5.17
1213

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
2+
3+
const WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME = "file";
4+
5+
export function resolveWhatsAppDefaultDocumentFileName(mimetype?: string): string {
6+
const extension = extensionForMime(mimetype);
7+
return extension
8+
? `${WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME}${extension}`
9+
: WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME;
10+
}
11+
12+
export function resolveWhatsAppDocumentFileName(params: {
13+
fileName?: string;
14+
mimetype?: string;
15+
}): string {
16+
return params.fileName?.trim() || resolveWhatsAppDefaultDocumentFileName(params.mimetype);
17+
}

extensions/whatsapp/src/inbound/send-api.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,42 @@ describe("createWebSendApi", () => {
111111
});
112112
});
113113

114-
it("falls back to default document filename when fileName is absent", async () => {
114+
it("falls back to a MIME-aware document filename when fileName is absent", async () => {
115115
const payload = Buffer.from("pdf");
116116
await api.sendMessage("+1555", "doc", payload, "application/pdf");
117117
expectFirstSendJid("1555@s.whatsapp.net");
118118
expectSendContentFields(0, {
119119
document: payload,
120-
fileName: "file",
120+
fileName: "file.pdf",
121121
caption: "doc",
122122
mimetype: "application/pdf",
123123
});
124124
});
125125

126+
it("uses MIME mappings for text document filename fallbacks", async () => {
127+
const payload = Buffer.from("a,b\n1,2\n");
128+
await api.sendMessage("+1555", "doc", payload, "text/csv");
129+
130+
expectSendContentFields(0, {
131+
document: payload,
132+
fileName: "file.csv",
133+
caption: "doc",
134+
mimetype: "text/csv",
135+
});
136+
});
137+
138+
it("keeps the plain default document filename when MIME has no extension mapping", async () => {
139+
const payload = Buffer.from("unknown");
140+
await api.sendMessage("+1555", "doc", payload, "application/x-custom");
141+
142+
expectSendContentFields(0, {
143+
document: payload,
144+
fileName: "file",
145+
caption: "doc",
146+
mimetype: "application/x-custom",
147+
});
148+
});
149+
126150
it("sends visual media as document when sendOptions.asDocument is true", async () => {
127151
const payload = Buffer.from("img");
128152
await api.sendMessage("+1555", "promo", payload, "image/png", {
@@ -140,6 +164,23 @@ describe("createWebSendApi", () => {
140164
);
141165
});
142166

167+
it("uses MIME-aware filename fallback for forced visual documents", async () => {
168+
const payload = Buffer.from("img");
169+
await api.sendMessage("+1555", "promo", payload, "image/png", {
170+
asDocument: true,
171+
});
172+
173+
expect(sendMessage).toHaveBeenCalledWith(
174+
"1555@s.whatsapp.net",
175+
expect.objectContaining({
176+
document: payload,
177+
fileName: "file.png",
178+
caption: "promo",
179+
mimetype: "image/png",
180+
}),
181+
);
182+
});
183+
143184
it("does not force audio media onto the document branch", async () => {
144185
const payload = Buffer.from("aud");
145186
await api.sendMessage("+1555", "voice", payload, "audio/ogg", {

extensions/whatsapp/src/inbound/send-api.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
WAPresence,
66
} from "baileys";
77
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
8+
import { resolveWhatsAppDocumentFileName } from "../document-filename.js";
89
import { isWhatsAppNewsletterJid } from "../normalize.js";
910
import { buildQuotedMessageOptions } from "../quoted-message.js";
1011
import { toWhatsappJid, toWhatsappJidWithLid } from "../text-runtime.js";
@@ -84,7 +85,10 @@ export function createWebSendApi(params: {
8485
: await resolveMentions(jid, text);
8586
if (mediaBuffer && mediaType) {
8687
if (sendOptions?.asDocument === true && supportsForcedDocumentMediaType(mediaType)) {
87-
const fileName = sendOptions?.fileName?.trim() || "file";
88+
const fileName = resolveWhatsAppDocumentFileName({
89+
fileName: sendOptions?.fileName,
90+
mimetype: mediaType,
91+
});
8892
payload = {
8993
document: mediaBuffer,
9094
fileName,
@@ -108,7 +112,10 @@ export function createWebSendApi(params: {
108112
...(gifPlayback ? { gifPlayback: true } : {}),
109113
};
110114
} else {
111-
const fileName = sendOptions?.fileName?.trim() || "file";
115+
const fileName = resolveWhatsAppDocumentFileName({
116+
fileName: sendOptions?.fileName,
117+
mimetype: mediaType,
118+
});
112119
payload = {
113120
document: mediaBuffer,
114121
fileName,

extensions/whatsapp/src/outbound-media-contract.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin
33
import { sanitizeForPlainText } from "openclaw/plugin-sdk/outbound-runtime";
44
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
55
import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
6+
import { resolveWhatsAppDocumentFileName } from "./document-filename.js";
67
import { formatError } from "./session-errors.js";
78
import {
89
sanitizeAssistantVisibleText,
@@ -123,7 +124,10 @@ function normalizeWhatsAppLoadedMedia(
123124
: (media.contentType ?? "application/octet-stream");
124125
const fileName =
125126
kind === "document"
126-
? (media.fileName ?? deriveWhatsAppDocumentFileName(mediaUrl) ?? "file")
127+
? resolveWhatsAppDocumentFileName({
128+
fileName: media.fileName ?? deriveWhatsAppDocumentFileName(mediaUrl),
129+
mimetype,
130+
})
127131
: media.fileName;
128132
return {
129133
buffer: media.buffer,

extensions/whatsapp/src/send.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,23 @@ describe("web outbound", () => {
452452
});
453453
});
454454

455+
it("maps documents without fileName to MIME-aware default filename", async () => {
456+
const buf = Buffer.from("pdf");
457+
loadWebMediaMock.mockResolvedValueOnce({
458+
buffer: buf,
459+
contentType: "application/pdf",
460+
kind: "document",
461+
});
462+
await sendMessageWhatsApp("+1555", "doc", {
463+
verbose: false,
464+
cfg: WHATSAPP_TEST_CFG,
465+
mediaUrl: "media://generated",
466+
});
467+
expect(sendMessage).toHaveBeenLastCalledWith("+1555", "doc", buf, "application/pdf", {
468+
fileName: "file.pdf",
469+
});
470+
});
471+
455472
it("forces document branch when forceDocument is true with image media", async () => {
456473
const buf = Buffer.from("img");
457474
loadWebMediaMock.mockResolvedValueOnce({
@@ -511,7 +528,7 @@ describe("web outbound", () => {
511528
});
512529
expect(sendMessage).toHaveBeenLastCalledWith("+1555", "promo", buf, "image/png", {
513530
asDocument: true,
514-
fileName: "file",
531+
fileName: "file.png",
515532
});
516533
});
517534

extensions/whatsapp/src/send.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
resolveWhatsAppMediaMaxBytes,
1616
} from "./accounts.js";
1717
import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
18+
import { resolveWhatsAppDocumentFileName } from "./document-filename.js";
1819
import type { ActiveWebListener, ActiveWebSendOptions } from "./inbound/types.js";
1920
import { isWhatsAppNewsletterJid } from "./normalize.js";
2021
import {
@@ -151,7 +152,10 @@ export async function sendMessageWhatsApp(
151152
text = caption ?? "";
152153
}
153154
if (forceDocumentDelivery) {
154-
documentFileName ??= media.fileName ?? "file";
155+
documentFileName ??= resolveWhatsAppDocumentFileName({
156+
fileName: media.fileName,
157+
mimetype: media.mimetype,
158+
});
155159
}
156160
}
157161
outboundLog.info(`Sending message -> ${redactedJid}${primaryMediaUrl ? " (media)" : ""}`);

0 commit comments

Comments
 (0)