fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing)
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
- Models/Auth: add OpenCode Zen (multi-model proxy) onboarding. (#623) — thanks @magimetal
|
||||
- WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete
|
||||
- WhatsApp: include phone numbers when multiple contacts are shared. (#625) — thanks @mahmoudashraf93
|
||||
- WhatsApp: preserve group JIDs when normalizing delivery targets. (#631) — thanks @imfing
|
||||
- Agents: warn on small context windows (<32k) and block unusable ones (<16k). — thanks @steipete
|
||||
- Pairing: cap pending DM pairing requests at 3 per provider and avoid pairing replies for outbound DMs. — thanks @steipete
|
||||
- macOS: replace relay smoke test with version check in packaging script. (#615) — thanks @YuriNachos
|
||||
|
||||
@@ -48,7 +48,12 @@ 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";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
export type RunCronAgentTurnResult = {
|
||||
@@ -203,14 +208,21 @@ function resolveDeliveryTarget(
|
||||
|
||||
const sanitizedWhatsappTo = (() => {
|
||||
if (provider !== "whatsapp") return rawTo;
|
||||
if (rawTo && isWhatsAppGroupJid(rawTo)) {
|
||||
return normalizeWhatsAppTarget(rawTo) || rawTo;
|
||||
}
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
if (rawAllow.includes("*")) return rawTo;
|
||||
if (rawAllow.includes("*")) {
|
||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
||||
}
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
if (allowFrom.length === 0) return rawTo;
|
||||
if (allowFrom.length === 0) {
|
||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
||||
}
|
||||
if (!rawTo) return allowFrom[0];
|
||||
const normalized = normalizeE164(rawTo);
|
||||
const normalized = normalizeWhatsAppTarget(rawTo);
|
||||
if (allowFrom.includes(normalized)) return normalized;
|
||||
return allowFrom[0];
|
||||
})();
|
||||
@@ -543,7 +555,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
summary: "Delivery skipped (no WhatsApp recipient).",
|
||||
};
|
||||
}
|
||||
const to = normalizeE164(resolvedDelivery.to);
|
||||
const to = normalizeWhatsAppTarget(resolvedDelivery.to);
|
||||
try {
|
||||
await deliverPayloadsWithMedia({
|
||||
payloads,
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
isGatewayMessageProvider,
|
||||
normalizeMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeE164,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "../../utils.js";
|
||||
import {
|
||||
type AgentWaitParams,
|
||||
ErrorCodes,
|
||||
@@ -221,11 +225,19 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
typeof request.to === "string" && request.to.trim()
|
||||
? request.to.trim()
|
||||
: undefined;
|
||||
if (explicit) return resolvedTo;
|
||||
if (explicit) {
|
||||
if (!resolvedTo) return resolvedTo;
|
||||
return normalizeWhatsAppTarget(resolvedTo) || resolvedTo;
|
||||
}
|
||||
if (resolvedTo && isWhatsAppGroupJid(resolvedTo)) {
|
||||
return normalizeWhatsAppTarget(resolvedTo) || resolvedTo;
|
||||
}
|
||||
|
||||
const cfg = cfgForAgent ?? loadConfig();
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
if (rawAllow.includes("*")) return resolvedTo;
|
||||
if (rawAllow.includes("*")) {
|
||||
return resolvedTo ? normalizeWhatsAppTarget(resolvedTo) : resolvedTo;
|
||||
}
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
@@ -233,7 +245,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const normalizedLast =
|
||||
typeof resolvedTo === "string" && resolvedTo.trim()
|
||||
? normalizeE164(resolvedTo)
|
||||
? normalizeWhatsAppTarget(resolvedTo)
|
||||
: undefined;
|
||||
if (normalizedLast && allowFrom.includes(normalizedLast)) {
|
||||
return normalizedLast;
|
||||
|
||||
@@ -126,6 +126,36 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps WhatsApp group targets even with allowFrom set", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
whatsapp: { allowFrom: ["+1555"] },
|
||||
};
|
||||
const entry = {
|
||||
...baseEntry,
|
||||
lastProvider: "whatsapp" as const,
|
||||
lastTo: "120363401234567890@g.us",
|
||||
};
|
||||
expect(resolveHeartbeatDeliveryTarget({ cfg, entry })).toEqual({
|
||||
provider: "whatsapp",
|
||||
to: "120363401234567890@g.us",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes prefixed WhatsApp group targets for heartbeat delivery", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
whatsapp: { allowFrom: ["+1555"] },
|
||||
};
|
||||
const entry = {
|
||||
...baseEntry,
|
||||
lastProvider: "whatsapp" as const,
|
||||
lastTo: "whatsapp:group:120363401234567890@G.US",
|
||||
};
|
||||
expect(resolveHeartbeatDeliveryTarget({ cfg, entry })).toEqual({
|
||||
provider: "whatsapp",
|
||||
to: "120363401234567890@g.us",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit telegram targets", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } },
|
||||
|
||||
@@ -12,6 +12,15 @@ describe("resolveOutboundTarget", () => {
|
||||
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",
|
||||
@@ -21,6 +30,14 @@ describe("resolveOutboundTarget", () => {
|
||||
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("rejects telegram with missing target", () => {
|
||||
const res = resolveOutboundTarget({ provider: "telegram", to: " " });
|
||||
expect(res.ok).toBe(false);
|
||||
|
||||
@@ -4,7 +4,11 @@ import type {
|
||||
DeliverableMessageProvider,
|
||||
GatewayMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeE164,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "../../utils.js";
|
||||
|
||||
export type OutboundProvider = DeliverableMessageProvider | "none";
|
||||
|
||||
@@ -28,16 +32,28 @@ export function resolveOutboundTarget(params: {
|
||||
const trimmed = params.to?.trim() || "";
|
||||
if (params.provider === "whatsapp") {
|
||||
if (trimmed) {
|
||||
return { ok: true, to: normalizeE164(trimmed) };
|
||||
const normalized = normalizeWhatsAppTarget(trimmed);
|
||||
if (!normalized) {
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(
|
||||
"Delivering to WhatsApp requires --to <E.164|group JID> or whatsapp.allowFrom[0]",
|
||||
),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: normalized };
|
||||
}
|
||||
const fallback = params.allowFrom?.[0]?.trim();
|
||||
if (fallback) {
|
||||
return { ok: true, to: fallback };
|
||||
const normalized = normalizeWhatsAppTarget(fallback);
|
||||
if (normalized) {
|
||||
return { ok: true, to: normalized };
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(
|
||||
"Delivering to WhatsApp requires --to <E.164> or whatsapp.allowFrom[0]",
|
||||
"Delivering to WhatsApp requires --to <E.164|group JID> or whatsapp.allowFrom[0]",
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -194,6 +210,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
return { provider: "none", reason: "no-target" };
|
||||
}
|
||||
if (rawAllow.includes("*")) return { provider, to: resolved.to };
|
||||
if (isWhatsAppGroupJid(resolved.to)) return { provider, to: resolved.to };
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
assertProvider,
|
||||
CONFIG_DIR,
|
||||
ensureDir,
|
||||
isWhatsAppGroupJid,
|
||||
jidToE164,
|
||||
normalizeE164,
|
||||
normalizePath,
|
||||
normalizeWhatsAppTarget,
|
||||
resolveJidToE164,
|
||||
resolveUserPath,
|
||||
sleep,
|
||||
@@ -84,6 +86,55 @@ 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,6 +25,34 @@ 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, "");
|
||||
|
||||
Reference in New Issue
Block a user