From 1bef2fc68bc512c886c25ef1dfba67f58c90e811 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 16 Feb 2026 22:13:05 +0800 Subject: [PATCH] fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys . --- .../whatsapp/src/channel.send-options.test.ts | 56 +++++++++++++++++++ extensions/whatsapp/src/channel.ts | 5 +- src/web/active-listener.ts | 1 + src/web/inbound/monitor.ts | 2 +- src/web/inbound/send-api.ts | 13 ++++- src/web/outbound.ts | 9 ++- 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 extensions/whatsapp/src/channel.send-options.test.ts diff --git a/extensions/whatsapp/src/channel.send-options.test.ts b/extensions/whatsapp/src/channel.send-options.test.ts new file mode 100644 index 0000000000..c5df32163c --- /dev/null +++ b/extensions/whatsapp/src/channel.send-options.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { whatsappPlugin } from "./channel.js"; + +// Mock runtime +const mockSendMessageWhatsApp = vi.fn().mockResolvedValue({ messageId: "123", toJid: "123@s.whatsapp.net" }); + +vi.mock("./runtime.js", () => ({ + getWhatsAppRuntime: () => ({ + channel: { + text: { chunkText: (t: string) => [t] }, + whatsapp: { + sendMessageWhatsApp: mockSendMessageWhatsApp, + createLoginTool: vi.fn(), + }, + }, + logging: { shouldLogVerbose: () => false }, + }), +})); + +describe("whatsappPlugin.outbound.sendText", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("passes linkPreview option to sendMessageWhatsApp", async () => { + await whatsappPlugin.outbound.sendText({ + to: "1234567890", + text: "http://example.com", + // @ts-expect-error - injecting extra param as per runtime behavior + linkPreview: false, + }); + + expect(mockSendMessageWhatsApp).toHaveBeenCalledWith( + "1234567890", + "http://example.com", + expect.objectContaining({ + linkPreview: false, + }) + ); + }); + + it("passes linkPreview=undefined when omitted", async () => { + await whatsappPlugin.outbound.sendText({ + to: "1234567890", + text: "hello", + }); + + expect(mockSendMessageWhatsApp).toHaveBeenCalledWith( + "1234567890", + "hello", + expect.objectContaining({ + linkPreview: undefined, + }) + ); + }); +}); diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index f0248823ca..900ce0cde8 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -290,12 +290,15 @@ export const whatsappPlugin: ChannelPlugin = { pollMaxOptions: 12, resolveTarget: ({ to, allowFrom, mode }) => resolveWhatsAppOutboundTarget({ to, allowFrom, mode }), - sendText: async ({ to, text, accountId, deps, gifPlayback }) => { + sendText: async (params) => { + const { to, text, accountId, deps, gifPlayback } = params; + const linkPreview = (params as { linkPreview?: boolean }).linkPreview; const send = deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp; const result = await send(to, text, { verbose: false, accountId: accountId ?? undefined, gifPlayback, + linkPreview, }); return { channel: "whatsapp", ...result }; }, diff --git a/src/web/active-listener.ts b/src/web/active-listener.ts index 0cb48ab405..010ff7040c 100644 --- a/src/web/active-listener.ts +++ b/src/web/active-listener.ts @@ -6,6 +6,7 @@ export type ActiveWebSendOptions = { gifPlayback?: boolean; accountId?: string; fileName?: string; + linkPreview?: boolean; }; export type ActiveWebListener = { diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index b21813e6f0..abd1c2128e 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -366,7 +366,7 @@ export async function monitorWebInbox(options: { const sendApi = createWebSendApi({ sock: { - sendMessage: (jid: string, content: AnyMessageContent) => sock.sendMessage(jid, content), + sendMessage: (jid, content, options) => sock.sendMessage(jid, content, options), sendPresenceUpdate: (presence, jid?: string) => sock.sendPresenceUpdate(presence, jid), }, defaultAccountId: options.accountId, diff --git a/src/web/inbound/send-api.ts b/src/web/inbound/send-api.ts index 05824536c1..2200801941 100644 --- a/src/web/inbound/send-api.ts +++ b/src/web/inbound/send-api.ts @@ -1,4 +1,4 @@ -import type { AnyMessageContent, WAPresence } from "@whiskeysockets/baileys"; +import type { AnyMessageContent, MiscMessageGenerationOptions, WAPresence } from "@whiskeysockets/baileys"; import type { ActiveWebSendOptions } from "../active-listener.js"; import { recordChannelActivity } from "../../infra/channel-activity.js"; import { toWhatsappJid } from "../../utils.js"; @@ -19,7 +19,11 @@ function resolveOutboundMessageId(result: unknown): string { export function createWebSendApi(params: { sock: { - sendMessage: (jid: string, content: AnyMessageContent) => Promise; + sendMessage: ( + jid: string, + content: AnyMessageContent, + options?: MiscMessageGenerationOptions, + ) => Promise; sendPresenceUpdate: (presence: WAPresence, jid?: string) => Promise; }; defaultAccountId: string; @@ -63,7 +67,10 @@ export function createWebSendApi(params: { } else { payload = { text }; } - const result = await params.sock.sendMessage(jid, payload); + const miscOptions: MiscMessageGenerationOptions = { + linkPreview: sendOptions?.linkPreview === false ? null : undefined, + }; + const result = await params.sock.sendMessage(jid, payload, miscOptions); const accountId = sendOptions?.accountId ?? params.defaultAccountId; recordWhatsAppOutbound(accountId); const messageId = resolveOutboundMessageId(result); diff --git a/src/web/outbound.ts b/src/web/outbound.ts index 5d3e84ba40..c0163a9f37 100644 --- a/src/web/outbound.ts +++ b/src/web/outbound.ts @@ -21,6 +21,7 @@ export async function sendMessageWhatsApp( mediaLocalRoots?: readonly string[]; gifPlayback?: boolean; accountId?: string; + linkPreview?: boolean; }, ): Promise<{ messageId: string; toJid: string }> { let text = body; @@ -75,10 +76,16 @@ export async function sendMessageWhatsApp( const hasExplicitAccountId = Boolean(options.accountId?.trim()); const accountId = hasExplicitAccountId ? resolvedAccountId : undefined; const sendOptions: ActiveWebSendOptions | undefined = - options.gifPlayback || accountId || documentFileName + options.gifPlayback || + options.accountId || + options.linkPreview !== undefined || + documentFileName ? { ...(options.gifPlayback ? { gifPlayback: true } : {}), ...(documentFileName ? { fileName: documentFileName } : {}), + ...(options.linkPreview !== undefined + ? { linkPreview: options.linkPreview } + : {}), accountId, } : undefined;