Merge pull request #631 from imfing/fix-normalize
fix(whatsapp): normalize targets for groups and E.164
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- Models/Auth: add OpenCode Zen (multi-model proxy) onboarding. (#623) — thanks @magimetal
|
- 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: 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: 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
|
- 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
|
- 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
|
- 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 { registerAgentRunContext } from "../infra/agent-events.js";
|
||||||
import { parseTelegramTarget } from "../telegram/targets.js";
|
import { parseTelegramTarget } from "../telegram/targets.js";
|
||||||
import { resolveTelegramToken } from "../telegram/token.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";
|
import type { CronJob } from "./types.js";
|
||||||
|
|
||||||
export type RunCronAgentTurnResult = {
|
export type RunCronAgentTurnResult = {
|
||||||
@@ -203,14 +208,21 @@ function resolveDeliveryTarget(
|
|||||||
|
|
||||||
const sanitizedWhatsappTo = (() => {
|
const sanitizedWhatsappTo = (() => {
|
||||||
if (provider !== "whatsapp") return rawTo;
|
if (provider !== "whatsapp") return rawTo;
|
||||||
|
if (rawTo && isWhatsAppGroupJid(rawTo)) {
|
||||||
|
return normalizeWhatsAppTarget(rawTo) || rawTo;
|
||||||
|
}
|
||||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||||
if (rawAllow.includes("*")) return rawTo;
|
if (rawAllow.includes("*")) {
|
||||||
|
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
||||||
|
}
|
||||||
const allowFrom = rawAllow
|
const allowFrom = rawAllow
|
||||||
.map((val) => normalizeE164(val))
|
.map((val) => normalizeE164(val))
|
||||||
.filter((val) => val.length > 1);
|
.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];
|
if (!rawTo) return allowFrom[0];
|
||||||
const normalized = normalizeE164(rawTo);
|
const normalized = normalizeWhatsAppTarget(rawTo);
|
||||||
if (allowFrom.includes(normalized)) return normalized;
|
if (allowFrom.includes(normalized)) return normalized;
|
||||||
return allowFrom[0];
|
return allowFrom[0];
|
||||||
})();
|
})();
|
||||||
@@ -543,7 +555,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
summary: "Delivery skipped (no WhatsApp recipient).",
|
summary: "Delivery skipped (no WhatsApp recipient).",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const to = normalizeE164(resolvedDelivery.to);
|
const to = normalizeWhatsAppTarget(resolvedDelivery.to);
|
||||||
try {
|
try {
|
||||||
await deliverPayloadsWithMedia({
|
await deliverPayloadsWithMedia({
|
||||||
payloads,
|
payloads,
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import {
|
|||||||
isGatewayMessageProvider,
|
isGatewayMessageProvider,
|
||||||
normalizeMessageProvider,
|
normalizeMessageProvider,
|
||||||
} from "../../utils/message-provider.js";
|
} from "../../utils/message-provider.js";
|
||||||
import { normalizeE164 } from "../../utils.js";
|
import {
|
||||||
|
isWhatsAppGroupJid,
|
||||||
|
normalizeE164,
|
||||||
|
normalizeWhatsAppTarget,
|
||||||
|
} from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
type AgentWaitParams,
|
type AgentWaitParams,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
@@ -221,11 +225,19 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
typeof request.to === "string" && request.to.trim()
|
typeof request.to === "string" && request.to.trim()
|
||||||
? request.to.trim()
|
? request.to.trim()
|
||||||
: undefined;
|
: 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 cfg = cfgForAgent ?? loadConfig();
|
||||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||||
if (rawAllow.includes("*")) return resolvedTo;
|
if (rawAllow.includes("*")) {
|
||||||
|
return resolvedTo ? normalizeWhatsAppTarget(resolvedTo) : resolvedTo;
|
||||||
|
}
|
||||||
const allowFrom = rawAllow
|
const allowFrom = rawAllow
|
||||||
.map((val) => normalizeE164(val))
|
.map((val) => normalizeE164(val))
|
||||||
.filter((val) => val.length > 1);
|
.filter((val) => val.length > 1);
|
||||||
@@ -233,7 +245,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
|
|
||||||
const normalizedLast =
|
const normalizedLast =
|
||||||
typeof resolvedTo === "string" && resolvedTo.trim()
|
typeof resolvedTo === "string" && resolvedTo.trim()
|
||||||
? normalizeE164(resolvedTo)
|
? normalizeWhatsAppTarget(resolvedTo)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (normalizedLast && allowFrom.includes(normalizedLast)) {
|
if (normalizedLast && allowFrom.includes(normalizedLast)) {
|
||||||
return 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", () => {
|
it("keeps explicit telegram targets", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } },
|
agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } },
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ describe("resolveOutboundTarget", () => {
|
|||||||
expect(res).toEqual({ ok: true, to: "+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", () => {
|
it("normalizes whatsapp target when provided", () => {
|
||||||
const res = resolveOutboundTarget({
|
const res = resolveOutboundTarget({
|
||||||
provider: "whatsapp",
|
provider: "whatsapp",
|
||||||
@@ -21,6 +30,14 @@ describe("resolveOutboundTarget", () => {
|
|||||||
expect(res.to).toBe("+5551234567");
|
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", () => {
|
it("rejects telegram with missing target", () => {
|
||||||
const res = resolveOutboundTarget({ provider: "telegram", to: " " });
|
const res = resolveOutboundTarget({ provider: "telegram", to: " " });
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import type {
|
|||||||
DeliverableMessageProvider,
|
DeliverableMessageProvider,
|
||||||
GatewayMessageProvider,
|
GatewayMessageProvider,
|
||||||
} from "../../utils/message-provider.js";
|
} from "../../utils/message-provider.js";
|
||||||
import { normalizeE164 } from "../../utils.js";
|
import {
|
||||||
|
isWhatsAppGroupJid,
|
||||||
|
normalizeE164,
|
||||||
|
normalizeWhatsAppTarget,
|
||||||
|
} from "../../utils.js";
|
||||||
|
|
||||||
export type OutboundProvider = DeliverableMessageProvider | "none";
|
export type OutboundProvider = DeliverableMessageProvider | "none";
|
||||||
|
|
||||||
@@ -28,16 +32,28 @@ export function resolveOutboundTarget(params: {
|
|||||||
const trimmed = params.to?.trim() || "";
|
const trimmed = params.to?.trim() || "";
|
||||||
if (params.provider === "whatsapp") {
|
if (params.provider === "whatsapp") {
|
||||||
if (trimmed) {
|
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();
|
const fallback = params.allowFrom?.[0]?.trim();
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
return { ok: true, to: fallback };
|
const normalized = normalizeWhatsAppTarget(fallback);
|
||||||
|
if (normalized) {
|
||||||
|
return { ok: true, to: normalized };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: new Error(
|
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" };
|
return { provider: "none", reason: "no-target" };
|
||||||
}
|
}
|
||||||
if (rawAllow.includes("*")) return { provider, to: resolved.to };
|
if (rawAllow.includes("*")) return { provider, to: resolved.to };
|
||||||
|
if (isWhatsAppGroupJid(resolved.to)) return { provider, to: resolved.to };
|
||||||
const allowFrom = rawAllow
|
const allowFrom = rawAllow
|
||||||
.map((val) => normalizeE164(val))
|
.map((val) => normalizeE164(val))
|
||||||
.filter((val) => val.length > 1);
|
.filter((val) => val.length > 1);
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
assertProvider,
|
assertProvider,
|
||||||
CONFIG_DIR,
|
CONFIG_DIR,
|
||||||
ensureDir,
|
ensureDir,
|
||||||
|
isWhatsAppGroupJid,
|
||||||
jidToE164,
|
jidToE164,
|
||||||
normalizeE164,
|
normalizeE164,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
|
normalizeWhatsAppTarget,
|
||||||
resolveJidToE164,
|
resolveJidToE164,
|
||||||
resolveUserPath,
|
resolveUserPath,
|
||||||
sleep,
|
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", () => {
|
describe("jidToE164", () => {
|
||||||
it("maps @lid using reverse mapping file", () => {
|
it("maps @lid using reverse mapping file", () => {
|
||||||
const mappingPath = path.join(
|
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}`;
|
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 {
|
export function normalizeE164(number: string): string {
|
||||||
const withoutPrefix = number.replace(/^whatsapp:/, "").trim();
|
const withoutPrefix = number.replace(/^whatsapp:/, "").trim();
|
||||||
const digits = withoutPrefix.replace(/[^\d+]/g, "");
|
const digits = withoutPrefix.replace(/[^\d+]/g, "");
|
||||||
|
|||||||
Reference in New Issue
Block a user