refactor(whatsapp): reuse SDK normalization helpers (#105580)

This commit is contained in:
Marcus Castro
2026-07-12 20:04:18 -03:00
committed by GitHub
parent f494f1579a
commit 0ff416161f
6 changed files with 107 additions and 43 deletions
+10 -14
View File
@@ -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 | number>): string[] {
const seen = new Set<string>();
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 {
@@ -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: {},
},
},
},
+3 -18
View File
@@ -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<
@@ -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);
@@ -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",
+10 -11
View File
@@ -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,
};
}