mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
fix(whatsapp): preserve outbound document filenames (#15594)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 8e0d765d1d
Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Gateway: breaking default-behavior change - canvas IP-based auth fallback now only accepts machine-scoped addresses (RFC1918, link-local, ULA IPv6, CGNAT); public-source IP matches now require bearer token auth. (#14661) Thanks @sumleo.
|
||||
- Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow.
|
||||
- Security/WhatsApp: enforce `0o600` on `creds.json` and `creds.json.bak` on save/backup/restore paths to reduce credential file exposure. (#10529) Thanks @abdelsfane.
|
||||
- WhatsApp: preserve outbound document filenames for web-session document sends instead of always sending `"file"`. (#15594) Thanks @TsekaLuk.
|
||||
- Security/Gateway + ACP: block high-risk tools (`sessions_spawn`, `sessions_send`, `gateway`, `whatsapp_login`) from HTTP `/tools/invoke` by default with `gateway.tools.{allow,deny}` overrides, and harden ACP permission selection to fail closed when tool identity/options are ambiguous while supporting `allow_always`/`reject_always`. (#15390) Thanks @aether-ai-agent.
|
||||
- Gateway/Tools Invoke: sanitize `/tools/invoke` execution failures while preserving `400` for tool input errors and returning `500` for unexpected runtime failures, with regression coverage and docs updates. (#13185) Thanks @davidrudduck.
|
||||
- MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin.
|
||||
|
||||
@@ -19,15 +19,29 @@ import { attachGatewayWsMessageHandler } from "./ws-connection/message-handler.j
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
|
||||
const LOG_HEADER_MAX_LEN = 300;
|
||||
const LOG_HEADER_CONTROL_REGEX = /[\u0000-\u001f\u007f-\u009f]/g;
|
||||
const LOG_HEADER_FORMAT_REGEX = /\p{Cf}/gu;
|
||||
|
||||
function replaceControlChars(value: string): string {
|
||||
let cleaned = "";
|
||||
for (const char of value) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (
|
||||
codePoint !== undefined &&
|
||||
(codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f))
|
||||
) {
|
||||
cleaned += " ";
|
||||
continue;
|
||||
}
|
||||
cleaned += char;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
const sanitizeLogValue = (value: string | undefined): string | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = value
|
||||
.replace(LOG_HEADER_CONTROL_REGEX, " ")
|
||||
const cleaned = replaceControlChars(value)
|
||||
.replace(LOG_HEADER_FORMAT_REGEX, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
export type ActiveWebSendOptions = {
|
||||
gifPlayback?: boolean;
|
||||
accountId?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
export type ActiveWebListener = {
|
||||
|
||||
54
src/web/inbound/send-api.test.ts
Normal file
54
src/web/inbound/send-api.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const recordChannelActivity = vi.fn();
|
||||
vi.mock("../../infra/channel-activity.js", () => ({
|
||||
recordChannelActivity: (...args: unknown[]) => recordChannelActivity(...args),
|
||||
}));
|
||||
|
||||
import { createWebSendApi } from "./send-api.js";
|
||||
|
||||
describe("createWebSendApi", () => {
|
||||
const sendMessage = vi.fn(async () => ({ key: { id: "msg-1" } }));
|
||||
const sendPresenceUpdate = vi.fn(async () => {});
|
||||
const api = createWebSendApi({
|
||||
sock: { sendMessage, sendPresenceUpdate },
|
||||
defaultAccountId: "main",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses sendOptions fileName for outbound documents", async () => {
|
||||
const payload = Buffer.from("pdf");
|
||||
await api.sendMessage("+1555", "doc", payload, "application/pdf", { fileName: "invoice.pdf" });
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"1555@s.whatsapp.net",
|
||||
expect.objectContaining({
|
||||
document: payload,
|
||||
fileName: "invoice.pdf",
|
||||
caption: "doc",
|
||||
mimetype: "application/pdf",
|
||||
}),
|
||||
);
|
||||
expect(recordChannelActivity).toHaveBeenCalledWith({
|
||||
channel: "whatsapp",
|
||||
accountId: "main",
|
||||
direction: "outbound",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to default document filename when fileName is absent", async () => {
|
||||
const payload = Buffer.from("pdf");
|
||||
await api.sendMessage("+1555", "doc", payload, "application/pdf");
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"1555@s.whatsapp.net",
|
||||
expect.objectContaining({
|
||||
document: payload,
|
||||
fileName: "file",
|
||||
caption: "doc",
|
||||
mimetype: "application/pdf",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -38,9 +38,10 @@ export function createWebSendApi(params: {
|
||||
...(gifPlayback ? { gifPlayback: true } : {}),
|
||||
};
|
||||
} else {
|
||||
const fileName = sendOptions?.fileName?.trim() || "file";
|
||||
payload = {
|
||||
document: mediaBuffer,
|
||||
fileName: "file",
|
||||
fileName,
|
||||
caption: text || undefined,
|
||||
mimetype: mediaType,
|
||||
};
|
||||
|
||||
@@ -130,7 +130,9 @@ describe("web outbound", () => {
|
||||
verbose: false,
|
||||
mediaUrl: "/tmp/file.pdf",
|
||||
});
|
||||
expect(sendMessage).toHaveBeenLastCalledWith("+1555", "doc", buf, "application/pdf");
|
||||
expect(sendMessage).toHaveBeenLastCalledWith("+1555", "doc", buf, "application/pdf", {
|
||||
fileName: "file.pdf",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends polls via active listener", async () => {
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function sendMessageWhatsApp(
|
||||
const jid = toWhatsappJid(to);
|
||||
let mediaBuffer: Buffer | undefined;
|
||||
let mediaType: string | undefined;
|
||||
let documentFileName: string | undefined;
|
||||
if (options.mediaUrl) {
|
||||
const media = await loadWebMedia(options.mediaUrl);
|
||||
const caption = text || undefined;
|
||||
@@ -62,6 +63,7 @@ export async function sendMessageWhatsApp(
|
||||
text = caption ?? "";
|
||||
} else {
|
||||
text = caption ?? "";
|
||||
documentFileName = media.fileName;
|
||||
}
|
||||
}
|
||||
outboundLog.info(`Sending message -> ${jid}${options.mediaUrl ? " (media)" : ""}`);
|
||||
@@ -70,9 +72,10 @@ export async function sendMessageWhatsApp(
|
||||
const hasExplicitAccountId = Boolean(options.accountId?.trim());
|
||||
const accountId = hasExplicitAccountId ? resolvedAccountId : undefined;
|
||||
const sendOptions: ActiveWebSendOptions | undefined =
|
||||
options.gifPlayback || accountId
|
||||
options.gifPlayback || accountId || documentFileName
|
||||
? {
|
||||
...(options.gifPlayback ? { gifPlayback: true } : {}),
|
||||
...(documentFileName ? { fileName: documentFileName } : {}),
|
||||
accountId,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
Reference in New Issue
Block a user