From 0ff416161f6cfbd3580be99a77dfb225e871897a Mon Sep 17 00:00:00 2001 From: Marcus Castro <7562095+mcaxtr@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:04:18 -0300 Subject: [PATCH] refactor(whatsapp): reuse SDK normalization helpers (#105580) --- extensions/whatsapp/src/normalize-target.ts | 24 ++++------ extensions/whatsapp/src/outbound-base.test.ts | 48 +++++++++++++++++++ extensions/whatsapp/src/outbound-base.ts | 21 ++------ .../whatsapp/src/resolve-target.test.ts | 15 ++++++ extensions/whatsapp/src/socket-timing.test.ts | 21 ++++++++ extensions/whatsapp/src/socket-timing.ts | 21 ++++---- 6 files changed, 107 insertions(+), 43 deletions(-) diff --git a/extensions/whatsapp/src/normalize-target.ts b/extensions/whatsapp/src/normalize-target.ts index 1aa031a4cfd7..b475cfda41b2 100644 --- a/extensions/whatsapp/src/normalize-target.ts +++ b/extensions/whatsapp/src/normalize-target.ts @@ -1,6 +1,10 @@ // Whatsapp helper module supports normalize target behavior. import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeStringEntries, + uniqueStrings, +} from "openclaw/plugin-sdk/string-coerce-runtime"; const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i; const WHATSAPP_LEGACY_USER_JID_RE = /^(\d+)@c\.us$/i; @@ -111,19 +115,11 @@ export function normalizeWhatsAppMessagingTarget(raw: string): string | undefine } export function normalizeWhatsAppAllowFromEntries(allowFrom: Array): string[] { - const seen = new Set(); - const normalized = allowFrom - .map((entry) => String(entry).trim()) - .filter((entry): entry is string => Boolean(entry)) - .map(normalizeWhatsAppAllowFromEntry) - .filter((entry): entry is string => Boolean(entry)); - return normalized.filter((entry) => { - if (seen.has(entry)) { - return false; - } - seen.add(entry); - return true; - }); + return uniqueStrings( + normalizeStringEntries(allowFrom) + .map(normalizeWhatsAppAllowFromEntry) + .filter((entry): entry is string => Boolean(entry)), + ); } export function normalizeWhatsAppAllowFromEntry(entry: string): string | null { diff --git a/extensions/whatsapp/src/outbound-base.test.ts b/extensions/whatsapp/src/outbound-base.test.ts index fc92557f4880..418fa3c5fde3 100644 --- a/extensions/whatsapp/src/outbound-base.test.ts +++ b/extensions/whatsapp/src/outbound-base.test.ts @@ -162,6 +162,7 @@ describe("createWhatsAppOutboundBase", () => { cfg: { channels: { whatsapp: { + authDir: "/tmp/whatsapp-default", defaultAccount: "work", accounts: { work: {}, @@ -185,6 +186,50 @@ describe("createWhatsAppOutboundBase", () => { }); }); + it("uses the implicit authDir default account for quote metadata lookup", async () => { + cacheInboundMessageMeta("default", "15551234567@s.whatsapp.net", "reply-auth-dir", { + participant: "444@s.whatsapp.net", + body: "implicit default body", + }); + const sendMessageWhatsApp = vi.fn(async () => ({ + messageId: "msg-auth-dir", + toJid: "15551234567@s.whatsapp.net", + })); + const outbound = createWhatsAppOutboundBase({ + chunker: (text) => [text], + sendMessageWhatsApp, + sendPollWhatsApp: vi.fn(), + shouldLogVerbose: () => false, + resolveTarget: ({ to }) => ({ ok: true as const, to: to ?? "" }), + }); + + await outbound.sendText!({ + cfg: { + channels: { + whatsapp: { + authDir: "/tmp/whatsapp-default", + accounts: { + work: {}, + }, + }, + }, + } as never, + to: "whatsapp:+15551234567", + text: "reply", + deps: { sendWhatsApp: sendMessageWhatsApp }, + replyToId: "reply-auth-dir", + }); + + const options = sendMessageOptionsAt(sendMessageWhatsApp, 0, "whatsapp:+15551234567", "reply"); + expect(options.quotedMessageKey).toEqual({ + id: "reply-auth-dir", + remoteJid: "15551234567@s.whatsapp.net", + fromMe: false, + participant: "444@s.whatsapp.net", + messageText: "implicit default body", + }); + }); + it("normalizes mixed-case defaultAccount before quote metadata lookup", async () => { cacheInboundMessageMeta("work", "15551234567@s.whatsapp.net", "reply-case", { participant: "333@s.whatsapp.net", @@ -345,8 +390,11 @@ describe("createWhatsAppOutboundBase", () => { cfg: { channels: { whatsapp: { + authDir: "/tmp/whatsapp-default", + defaultAccount: "other", accounts: { work: {}, + other: {}, }, }, }, diff --git a/extensions/whatsapp/src/outbound-base.ts b/extensions/whatsapp/src/outbound-base.ts index 7a0000cb10c8..badf0a12a240 100644 --- a/extensions/whatsapp/src/outbound-base.ts +++ b/extensions/whatsapp/src/outbound-base.ts @@ -1,10 +1,5 @@ // Whatsapp plugin module implements outbound base behavior. -import { - DEFAULT_ACCOUNT_ID, - listCombinedAccountIds, - normalizeOptionalAccountId, - resolveListedDefaultAccountId, -} from "openclaw/plugin-sdk/account-core"; +import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-core"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-outbound"; import { attachChannelToResult, @@ -13,6 +8,7 @@ import { } from "openclaw/plugin-sdk/channel-send-result"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload"; +import { resolveDefaultWhatsAppAccountId } from "./account-ids.js"; import { normalizeWhatsAppOutboundPayload, normalizeWhatsAppPayloadText, @@ -73,18 +69,7 @@ function resolveQuoteLookupAccountId(cfg?: OpenClawConfig, accountId?: string | if (explicitAccountId) { return explicitAccountId; } - const channelCfg = cfg?.channels?.whatsapp; - const configuredIds = listCombinedAccountIds({ - configuredAccountIds: - channelCfg?.accounts && typeof channelCfg.accounts === "object" - ? Object.keys(channelCfg.accounts).filter(Boolean) - : [], - fallbackAccountIdWhenEmpty: DEFAULT_ACCOUNT_ID, - }); - return resolveListedDefaultAccountId({ - accountIds: configuredIds, - configuredDefaultAccountId: normalizeOptionalAccountId(channelCfg?.defaultAccount), - }); + return resolveDefaultWhatsAppAccountId(cfg ?? {}); } type WhatsAppOutboundBaseCore = Pick< diff --git a/extensions/whatsapp/src/resolve-target.test.ts b/extensions/whatsapp/src/resolve-target.test.ts index 112c6ae8dbd4..162449c2d5d4 100644 --- a/extensions/whatsapp/src/resolve-target.test.ts +++ b/extensions/whatsapp/src/resolve-target.test.ts @@ -5,6 +5,7 @@ import { isWhatsAppNewsletterJid, looksLikeWhatsAppTargetId, isWhatsAppUserTarget, + normalizeWhatsAppAllowFromEntries, normalizeWhatsAppMessagingTarget, normalizeWhatsAppTarget, } from "./normalize-target.js"; @@ -120,6 +121,20 @@ describe("normalizeWhatsAppMessagingTarget", () => { }); }); +describe("normalizeWhatsAppAllowFromEntries", () => { + it("deduplicates entries after WhatsApp target normalization", () => { + expect( + normalizeWhatsAppAllowFromEntries([ + " +1 (555) 123-4567 ", + "15551234567@s.whatsapp.net", + 15551234567, + " ", + "invalid", + ]), + ).toEqual(["15551234567"]); + }); +}); + describe("looksLikeWhatsAppTargetId", () => { it("detects common WhatsApp target forms", () => { expect(looksLikeWhatsAppTargetId("whatsapp:+15555550123")).toBe(true); diff --git a/extensions/whatsapp/src/socket-timing.test.ts b/extensions/whatsapp/src/socket-timing.test.ts index 599a4505bc9a..365337d77cd0 100644 --- a/extensions/whatsapp/src/socket-timing.test.ts +++ b/extensions/whatsapp/src/socket-timing.test.ts @@ -58,6 +58,27 @@ describe("resolveWhatsAppSocketTiming", () => { }); }); + it("rejects invalid numeric timing values", () => { + expect( + resolveWhatsAppSocketTiming( + { + web: { + whatsapp: { + keepAliveIntervalMs: 0, + connectTimeoutMs: Number.NaN, + defaultQueryTimeoutMs: 1.5, + }, + }, + }, + { + keepAliveIntervalMs: -1, + connectTimeoutMs: Number.POSITIVE_INFINITY, + defaultQueryTimeoutMs: Number.MAX_SAFE_INTEGER + 1, + }, + ), + ).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING); + }); + it("marks operation timeout errors as unknown delivery state", () => { const error = new WhatsAppSocketOperationTimeoutError( "sendMessage", diff --git a/extensions/whatsapp/src/socket-timing.ts b/extensions/whatsapp/src/socket-timing.ts index 620ec490af70..3520746bee4e 100644 --- a/extensions/whatsapp/src/socket-timing.ts +++ b/extensions/whatsapp/src/socket-timing.ts @@ -6,7 +6,10 @@ import type { WAPresence, } from "baileys"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; +import { + parseStrictPositiveInteger, + resolveTimerTimeoutMs, +} from "openclaw/plugin-sdk/number-runtime"; export type WhatsAppSocketTimingOptions = { keepAliveIntervalMs?: number; @@ -47,10 +50,6 @@ export class WhatsAppSocketOperationTimeoutError extends Error { } } -function positiveInteger(value: number | undefined): number | undefined { - return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; -} - export function resolveWhatsAppSocketTiming( cfg: OpenClawConfig, overrides?: WhatsAppSocketTimingOptions, @@ -58,16 +57,16 @@ export function resolveWhatsAppSocketTiming( const configured = cfg.web?.whatsapp; return { keepAliveIntervalMs: - positiveInteger(overrides?.keepAliveIntervalMs) ?? - positiveInteger(configured?.keepAliveIntervalMs) ?? + parseStrictPositiveInteger(overrides?.keepAliveIntervalMs) ?? + parseStrictPositiveInteger(configured?.keepAliveIntervalMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs, connectTimeoutMs: - positiveInteger(overrides?.connectTimeoutMs) ?? - positiveInteger(configured?.connectTimeoutMs) ?? + parseStrictPositiveInteger(overrides?.connectTimeoutMs) ?? + parseStrictPositiveInteger(configured?.connectTimeoutMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs, defaultQueryTimeoutMs: - positiveInteger(overrides?.defaultQueryTimeoutMs) ?? - positiveInteger(configured?.defaultQueryTimeoutMs) ?? + parseStrictPositiveInteger(overrides?.defaultQueryTimeoutMs) ?? + parseStrictPositiveInteger(configured?.defaultQueryTimeoutMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs, }; }