refactor: centralize WhatsApp target normalization
This commit is contained in:
@@ -48,12 +48,11 @@ import {
|
||||
import { registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import { parseTelegramTarget } from "../telegram/targets.js";
|
||||
import { resolveTelegramToken } from "../telegram/token.js";
|
||||
import { normalizeE164, truncateUtf16Safe } from "../utils.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeE164,
|
||||
normalizeWhatsAppTarget,
|
||||
truncateUtf16Safe,
|
||||
} from "../utils.js";
|
||||
} from "../whatsapp/normalize.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
export type RunCronAgentTurnResult = {
|
||||
@@ -209,21 +208,21 @@ function resolveDeliveryTarget(
|
||||
const sanitizedWhatsappTo = (() => {
|
||||
if (provider !== "whatsapp") return rawTo;
|
||||
if (rawTo && isWhatsAppGroupJid(rawTo)) {
|
||||
return normalizeWhatsAppTarget(rawTo) || rawTo;
|
||||
return normalizeWhatsAppTarget(rawTo) ?? rawTo;
|
||||
}
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
if (rawAllow.includes("*")) {
|
||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
||||
return rawTo ? (normalizeWhatsAppTarget(rawTo) ?? rawTo) : rawTo;
|
||||
}
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
if (allowFrom.length === 0) {
|
||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
||||
return rawTo ? (normalizeWhatsAppTarget(rawTo) ?? rawTo) : rawTo;
|
||||
}
|
||||
if (!rawTo) return allowFrom[0];
|
||||
const normalized = normalizeWhatsAppTarget(rawTo);
|
||||
if (allowFrom.includes(normalized)) return normalized;
|
||||
if (normalized && allowFrom.includes(normalized)) return normalized;
|
||||
return allowFrom[0];
|
||||
})();
|
||||
|
||||
@@ -555,7 +554,8 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
summary: "Delivery skipped (no WhatsApp recipient).",
|
||||
};
|
||||
}
|
||||
const to = normalizeWhatsAppTarget(resolvedDelivery.to);
|
||||
const rawTo = resolvedDelivery.to;
|
||||
const to = normalizeWhatsAppTarget(rawTo) ?? rawTo;
|
||||
try {
|
||||
await deliverPayloadsWithMedia({
|
||||
payloads,
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
isGatewayMessageProvider,
|
||||
normalizeMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeE164,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "../../utils.js";
|
||||
} from "../../whatsapp/normalize.js";
|
||||
import {
|
||||
type AgentWaitParams,
|
||||
ErrorCodes,
|
||||
@@ -227,16 +227,18 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
: undefined;
|
||||
if (explicit) {
|
||||
if (!resolvedTo) return resolvedTo;
|
||||
return normalizeWhatsAppTarget(resolvedTo) || resolvedTo;
|
||||
return normalizeWhatsAppTarget(resolvedTo) ?? resolvedTo;
|
||||
}
|
||||
if (resolvedTo && isWhatsAppGroupJid(resolvedTo)) {
|
||||
return normalizeWhatsAppTarget(resolvedTo) || resolvedTo;
|
||||
return normalizeWhatsAppTarget(resolvedTo) ?? resolvedTo;
|
||||
}
|
||||
|
||||
const cfg = cfgForAgent ?? loadConfig();
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
if (rawAllow.includes("*")) {
|
||||
return resolvedTo ? normalizeWhatsAppTarget(resolvedTo) : resolvedTo;
|
||||
return resolvedTo
|
||||
? (normalizeWhatsAppTarget(resolvedTo) ?? resolvedTo)
|
||||
: resolvedTo;
|
||||
}
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
|
||||
@@ -96,6 +96,21 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes explicit WhatsApp targets when allowFrom is '*'", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: { target: "whatsapp", to: "whatsapp:(555) 123" },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
};
|
||||
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({
|
||||
provider: "whatsapp",
|
||||
to: "+555123",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips when last route is webchat", () => {
|
||||
const cfg: ClawdbotConfig = {};
|
||||
const entry = {
|
||||
|
||||
@@ -3,39 +3,64 @@ import { describe, expect, it } from "vitest";
|
||||
import { resolveOutboundTarget } from "./targets.js";
|
||||
|
||||
describe("resolveOutboundTarget", () => {
|
||||
it("falls back to whatsapp allowFrom", () => {
|
||||
const res = resolveOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to: "",
|
||||
allowFrom: ["+1555"],
|
||||
});
|
||||
expect(res).toEqual({ ok: true, to: "+1555" });
|
||||
});
|
||||
|
||||
it("normalizes whatsapp allowFrom fallback targets", () => {
|
||||
const res = resolveOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to: "",
|
||||
allowFrom: ["whatsapp:(555) 123-4567"],
|
||||
});
|
||||
expect(res).toEqual({ ok: true, to: "+5551234567" });
|
||||
});
|
||||
|
||||
it("normalizes whatsapp target when provided", () => {
|
||||
const res = resolveOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to: " (555) 123-4567 ",
|
||||
});
|
||||
if (!res.ok) throw res.error;
|
||||
expect(res.to).toBe("+5551234567");
|
||||
});
|
||||
|
||||
it("keeps whatsapp group targets", () => {
|
||||
const res = resolveOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to: "120363401234567890@g.us",
|
||||
});
|
||||
expect(res).toEqual({ ok: true, to: "120363401234567890@g.us" });
|
||||
it.each([
|
||||
{
|
||||
name: "normalizes whatsapp target when provided",
|
||||
input: { provider: "whatsapp" as const, to: " (555) 123-4567 " },
|
||||
expected: { ok: true as const, to: "+5551234567" },
|
||||
},
|
||||
{
|
||||
name: "keeps whatsapp group targets",
|
||||
input: { provider: "whatsapp" as const, to: "120363401234567890@g.us" },
|
||||
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||
},
|
||||
{
|
||||
name: "normalizes prefixed/uppercase whatsapp group targets",
|
||||
input: {
|
||||
provider: "whatsapp" as const,
|
||||
to: " WhatsApp:Group:120363401234567890@G.US ",
|
||||
},
|
||||
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||
},
|
||||
{
|
||||
name: "falls back to whatsapp allowFrom",
|
||||
input: { provider: "whatsapp" as const, to: "", allowFrom: ["+1555"] },
|
||||
expected: { ok: true as const, to: "+1555" },
|
||||
},
|
||||
{
|
||||
name: "normalizes whatsapp allowFrom fallback targets",
|
||||
input: {
|
||||
provider: "whatsapp" as const,
|
||||
to: "",
|
||||
allowFrom: ["whatsapp:(555) 123-4567"],
|
||||
},
|
||||
expected: { ok: true as const, to: "+5551234567" },
|
||||
},
|
||||
{
|
||||
name: "rejects invalid whatsapp target",
|
||||
input: { provider: "whatsapp" as const, to: "wat" },
|
||||
expectedErrorIncludes: "WhatsApp",
|
||||
},
|
||||
{
|
||||
name: "rejects whatsapp without to when allowFrom missing",
|
||||
input: { provider: "whatsapp" as const, to: " " },
|
||||
expectedErrorIncludes: "WhatsApp",
|
||||
},
|
||||
{
|
||||
name: "rejects whatsapp allowFrom fallback when invalid",
|
||||
input: { provider: "whatsapp" as const, to: "", allowFrom: ["wat"] },
|
||||
expectedErrorIncludes: "WhatsApp",
|
||||
},
|
||||
])("$name", ({ input, expected, expectedErrorIncludes }) => {
|
||||
const res = resolveOutboundTarget(input);
|
||||
if (expected) {
|
||||
expect(res).toEqual(expected);
|
||||
return;
|
||||
}
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.error.message).toContain(expectedErrorIncludes);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects telegram with missing target", () => {
|
||||
|
||||
@@ -4,11 +4,11 @@ import type {
|
||||
DeliverableMessageProvider,
|
||||
GatewayMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeE164,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "../../utils.js";
|
||||
} from "../../whatsapp/normalize.js";
|
||||
|
||||
export type OutboundProvider = DeliverableMessageProvider | "none";
|
||||
|
||||
@@ -24,7 +24,7 @@ export type OutboundTargetResolution =
|
||||
| { ok: true; to: string }
|
||||
| { ok: false; error: Error };
|
||||
|
||||
export function resolveOutboundTarget(params: {
|
||||
export function normalizeOutboundTarget(params: {
|
||||
provider: GatewayMessageProvider;
|
||||
to?: string;
|
||||
allowFrom?: string[];
|
||||
@@ -129,6 +129,14 @@ export function resolveOutboundTarget(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveOutboundTarget(params: {
|
||||
provider: GatewayMessageProvider;
|
||||
to?: string;
|
||||
allowFrom?: string[];
|
||||
}): OutboundTargetResolution {
|
||||
return normalizeOutboundTarget(params);
|
||||
}
|
||||
|
||||
export function resolveHeartbeatDeliveryTarget(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
entry?: SessionEntry;
|
||||
@@ -194,14 +202,14 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
}
|
||||
|
||||
if (provider !== "whatsapp") {
|
||||
const resolved = resolveOutboundTarget({ provider, to });
|
||||
const resolved = normalizeOutboundTarget({ provider, to });
|
||||
return resolved.ok
|
||||
? { provider, to: resolved.to }
|
||||
: { provider: "none", reason: "no-target" };
|
||||
}
|
||||
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
const resolved = resolveOutboundTarget({
|
||||
const resolved = normalizeOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to,
|
||||
allowFrom: rawAllow,
|
||||
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
assertProvider,
|
||||
CONFIG_DIR,
|
||||
ensureDir,
|
||||
isWhatsAppGroupJid,
|
||||
jidToE164,
|
||||
normalizeE164,
|
||||
normalizePath,
|
||||
normalizeWhatsAppTarget,
|
||||
resolveJidToE164,
|
||||
resolveUserPath,
|
||||
sleep,
|
||||
@@ -86,55 +84,6 @@ describe("normalizeE164 & toWhatsappJid", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeWhatsAppTarget", () => {
|
||||
it("preserves group JIDs", () => {
|
||||
expect(normalizeWhatsAppTarget("120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("123456789-987654321@g.us")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(
|
||||
normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us"),
|
||||
).toBe("120363401234567890@g.us");
|
||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
expect(
|
||||
normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US "),
|
||||
).toBe("123456789-987654321@g.us");
|
||||
});
|
||||
|
||||
it("normalizes direct JIDs to E.164", () => {
|
||||
expect(normalizeWhatsAppTarget("1555123@s.whatsapp.net")).toBe("+1555123");
|
||||
});
|
||||
|
||||
it("rejects invalid targets", () => {
|
||||
expect(normalizeWhatsAppTarget("wat")).toBe("");
|
||||
expect(normalizeWhatsAppTarget("whatsapp:")).toBe("");
|
||||
expect(normalizeWhatsAppTarget("@g.us")).toBe("");
|
||||
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWhatsAppGroupJid", () => {
|
||||
it("detects group JIDs with or without prefixes", () => {
|
||||
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("+1555123")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("jidToE164", () => {
|
||||
it("maps @lid using reverse mapping file", () => {
|
||||
const mappingPath = path.join(
|
||||
|
||||
28
src/utils.ts
28
src/utils.ts
@@ -25,34 +25,6 @@ export function withWhatsAppPrefix(number: string): string {
|
||||
return number.startsWith("whatsapp:") ? number : `whatsapp:${number}`;
|
||||
}
|
||||
|
||||
function stripWhatsAppTargetPrefixes(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
return trimmed
|
||||
.replace(/^whatsapp:/i, "")
|
||||
.replace(/^group:/i, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isWhatsAppGroupJid(value: string): boolean {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
const lower = candidate.toLowerCase();
|
||||
if (!lower.endsWith("@g.us")) return false;
|
||||
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
||||
if (!localPart || localPart.includes("@")) return false;
|
||||
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppTarget(value: string): string {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
if (!candidate) return "";
|
||||
if (isWhatsAppGroupJid(candidate)) {
|
||||
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
||||
return `${localPart}@g.us`;
|
||||
}
|
||||
const normalized = normalizeE164(candidate);
|
||||
return normalized.length > 1 ? normalized : "";
|
||||
}
|
||||
|
||||
export function normalizeE164(number: string): string {
|
||||
const withoutPrefix = number.replace(/^whatsapp:/, "").trim();
|
||||
const digits = withoutPrefix.replace(/[^\d+]/g, "");
|
||||
|
||||
57
src/whatsapp/normalize.test.ts
Normal file
57
src/whatsapp/normalize.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
||||
|
||||
describe("normalizeWhatsAppTarget", () => {
|
||||
it("preserves group JIDs", () => {
|
||||
expect(normalizeWhatsAppTarget("120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("123456789-987654321@g.us")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(
|
||||
normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us"),
|
||||
).toBe("120363401234567890@g.us");
|
||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
expect(
|
||||
normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US "),
|
||||
).toBe("123456789-987654321@g.us");
|
||||
});
|
||||
|
||||
it("normalizes direct JIDs to E.164", () => {
|
||||
expect(normalizeWhatsAppTarget("1555123@s.whatsapp.net")).toBe("+1555123");
|
||||
});
|
||||
|
||||
it("rejects invalid targets", () => {
|
||||
expect(normalizeWhatsAppTarget("wat")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles repeated prefixes", () => {
|
||||
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
|
||||
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBe("120@g.us");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWhatsAppGroupJid", () => {
|
||||
it("detects group JIDs with or without prefixes", () => {
|
||||
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("+1555123")).toBe(false);
|
||||
});
|
||||
});
|
||||
33
src/whatsapp/normalize.ts
Normal file
33
src/whatsapp/normalize.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { normalizeE164 } from "../utils.js";
|
||||
|
||||
function stripWhatsAppTargetPrefixes(value: string): string {
|
||||
let candidate = value.trim();
|
||||
for (;;) {
|
||||
const before = candidate;
|
||||
candidate = candidate
|
||||
.replace(/^whatsapp:/i, "")
|
||||
.replace(/^group:/i, "")
|
||||
.trim();
|
||||
if (candidate === before) return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWhatsAppGroupJid(value: string): boolean {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
const lower = candidate.toLowerCase();
|
||||
if (!lower.endsWith("@g.us")) return false;
|
||||
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
||||
if (!localPart || localPart.includes("@")) return false;
|
||||
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppTarget(value: string): string | null {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
if (!candidate) return null;
|
||||
if (isWhatsAppGroupJid(candidate)) {
|
||||
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
||||
return `${localPart}@g.us`;
|
||||
}
|
||||
const normalized = normalizeE164(candidate);
|
||||
return normalized.length > 1 ? normalized : null;
|
||||
}
|
||||
Reference in New Issue
Block a user