refactor: centralize WhatsApp target normalization
This commit is contained in:
@@ -48,12 +48,11 @@ 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 {
|
import {
|
||||||
isWhatsAppGroupJid,
|
isWhatsAppGroupJid,
|
||||||
normalizeE164,
|
|
||||||
normalizeWhatsAppTarget,
|
normalizeWhatsAppTarget,
|
||||||
truncateUtf16Safe,
|
} from "../whatsapp/normalize.js";
|
||||||
} from "../utils.js";
|
|
||||||
import type { CronJob } from "./types.js";
|
import type { CronJob } from "./types.js";
|
||||||
|
|
||||||
export type RunCronAgentTurnResult = {
|
export type RunCronAgentTurnResult = {
|
||||||
@@ -209,21 +208,21 @@ function resolveDeliveryTarget(
|
|||||||
const sanitizedWhatsappTo = (() => {
|
const sanitizedWhatsappTo = (() => {
|
||||||
if (provider !== "whatsapp") return rawTo;
|
if (provider !== "whatsapp") return rawTo;
|
||||||
if (rawTo && isWhatsAppGroupJid(rawTo)) {
|
if (rawTo && isWhatsAppGroupJid(rawTo)) {
|
||||||
return normalizeWhatsAppTarget(rawTo) || rawTo;
|
return normalizeWhatsAppTarget(rawTo) ?? rawTo;
|
||||||
}
|
}
|
||||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||||
if (rawAllow.includes("*")) {
|
if (rawAllow.includes("*")) {
|
||||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
return rawTo ? (normalizeWhatsAppTarget(rawTo) ?? 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) {
|
if (allowFrom.length === 0) {
|
||||||
return rawTo ? normalizeWhatsAppTarget(rawTo) : rawTo;
|
return rawTo ? (normalizeWhatsAppTarget(rawTo) ?? rawTo) : rawTo;
|
||||||
}
|
}
|
||||||
if (!rawTo) return allowFrom[0];
|
if (!rawTo) return allowFrom[0];
|
||||||
const normalized = normalizeWhatsAppTarget(rawTo);
|
const normalized = normalizeWhatsAppTarget(rawTo);
|
||||||
if (allowFrom.includes(normalized)) return normalized;
|
if (normalized && allowFrom.includes(normalized)) return normalized;
|
||||||
return allowFrom[0];
|
return allowFrom[0];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -555,7 +554,8 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
summary: "Delivery skipped (no WhatsApp recipient).",
|
summary: "Delivery skipped (no WhatsApp recipient).",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const to = normalizeWhatsAppTarget(resolvedDelivery.to);
|
const rawTo = resolvedDelivery.to;
|
||||||
|
const to = normalizeWhatsAppTarget(rawTo) ?? rawTo;
|
||||||
try {
|
try {
|
||||||
await deliverPayloadsWithMedia({
|
await deliverPayloadsWithMedia({
|
||||||
payloads,
|
payloads,
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import {
|
|||||||
isGatewayMessageProvider,
|
isGatewayMessageProvider,
|
||||||
normalizeMessageProvider,
|
normalizeMessageProvider,
|
||||||
} from "../../utils/message-provider.js";
|
} from "../../utils/message-provider.js";
|
||||||
|
import { normalizeE164 } from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
isWhatsAppGroupJid,
|
isWhatsAppGroupJid,
|
||||||
normalizeE164,
|
|
||||||
normalizeWhatsAppTarget,
|
normalizeWhatsAppTarget,
|
||||||
} from "../../utils.js";
|
} from "../../whatsapp/normalize.js";
|
||||||
import {
|
import {
|
||||||
type AgentWaitParams,
|
type AgentWaitParams,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
@@ -227,16 +227,18 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
: undefined;
|
: undefined;
|
||||||
if (explicit) {
|
if (explicit) {
|
||||||
if (!resolvedTo) return resolvedTo;
|
if (!resolvedTo) return resolvedTo;
|
||||||
return normalizeWhatsAppTarget(resolvedTo) || resolvedTo;
|
return normalizeWhatsAppTarget(resolvedTo) ?? resolvedTo;
|
||||||
}
|
}
|
||||||
if (resolvedTo && isWhatsAppGroupJid(resolvedTo)) {
|
if (resolvedTo && isWhatsAppGroupJid(resolvedTo)) {
|
||||||
return normalizeWhatsAppTarget(resolvedTo) || 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("*")) {
|
if (rawAllow.includes("*")) {
|
||||||
return resolvedTo ? normalizeWhatsAppTarget(resolvedTo) : resolvedTo;
|
return resolvedTo
|
||||||
|
? (normalizeWhatsAppTarget(resolvedTo) ?? resolvedTo)
|
||||||
|
: resolvedTo;
|
||||||
}
|
}
|
||||||
const allowFrom = rawAllow
|
const allowFrom = rawAllow
|
||||||
.map((val) => normalizeE164(val))
|
.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", () => {
|
it("skips when last route is webchat", () => {
|
||||||
const cfg: ClawdbotConfig = {};
|
const cfg: ClawdbotConfig = {};
|
||||||
const entry = {
|
const entry = {
|
||||||
|
|||||||
@@ -3,39 +3,64 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { resolveOutboundTarget } from "./targets.js";
|
import { resolveOutboundTarget } from "./targets.js";
|
||||||
|
|
||||||
describe("resolveOutboundTarget", () => {
|
describe("resolveOutboundTarget", () => {
|
||||||
it("falls back to whatsapp allowFrom", () => {
|
it.each([
|
||||||
const res = resolveOutboundTarget({
|
{
|
||||||
provider: "whatsapp",
|
name: "normalizes whatsapp target when provided",
|
||||||
to: "",
|
input: { provider: "whatsapp" as const, to: " (555) 123-4567 " },
|
||||||
allowFrom: ["+1555"],
|
expected: { ok: true as const, to: "+5551234567" },
|
||||||
});
|
},
|
||||||
expect(res).toEqual({ ok: true, to: "+1555" });
|
{
|
||||||
});
|
name: "keeps whatsapp group targets",
|
||||||
|
input: { provider: "whatsapp" as const, to: "120363401234567890@g.us" },
|
||||||
it("normalizes whatsapp allowFrom fallback targets", () => {
|
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||||
const res = resolveOutboundTarget({
|
},
|
||||||
provider: "whatsapp",
|
{
|
||||||
to: "",
|
name: "normalizes prefixed/uppercase whatsapp group targets",
|
||||||
allowFrom: ["whatsapp:(555) 123-4567"],
|
input: {
|
||||||
});
|
provider: "whatsapp" as const,
|
||||||
expect(res).toEqual({ ok: true, to: "+5551234567" });
|
to: " WhatsApp:Group:120363401234567890@G.US ",
|
||||||
});
|
},
|
||||||
|
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||||
it("normalizes whatsapp target when provided", () => {
|
},
|
||||||
const res = resolveOutboundTarget({
|
{
|
||||||
provider: "whatsapp",
|
name: "falls back to whatsapp allowFrom",
|
||||||
to: " (555) 123-4567 ",
|
input: { provider: "whatsapp" as const, to: "", allowFrom: ["+1555"] },
|
||||||
});
|
expected: { ok: true as const, to: "+1555" },
|
||||||
if (!res.ok) throw res.error;
|
},
|
||||||
expect(res.to).toBe("+5551234567");
|
{
|
||||||
});
|
name: "normalizes whatsapp allowFrom fallback targets",
|
||||||
|
input: {
|
||||||
it("keeps whatsapp group targets", () => {
|
provider: "whatsapp" as const,
|
||||||
const res = resolveOutboundTarget({
|
to: "",
|
||||||
provider: "whatsapp",
|
allowFrom: ["whatsapp:(555) 123-4567"],
|
||||||
to: "120363401234567890@g.us",
|
},
|
||||||
});
|
expected: { ok: true as const, to: "+5551234567" },
|
||||||
expect(res).toEqual({ ok: true, to: "120363401234567890@g.us" });
|
},
|
||||||
|
{
|
||||||
|
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", () => {
|
it("rejects telegram with missing target", () => {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import type {
|
|||||||
DeliverableMessageProvider,
|
DeliverableMessageProvider,
|
||||||
GatewayMessageProvider,
|
GatewayMessageProvider,
|
||||||
} from "../../utils/message-provider.js";
|
} from "../../utils/message-provider.js";
|
||||||
|
import { normalizeE164 } from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
isWhatsAppGroupJid,
|
isWhatsAppGroupJid,
|
||||||
normalizeE164,
|
|
||||||
normalizeWhatsAppTarget,
|
normalizeWhatsAppTarget,
|
||||||
} from "../../utils.js";
|
} from "../../whatsapp/normalize.js";
|
||||||
|
|
||||||
export type OutboundProvider = DeliverableMessageProvider | "none";
|
export type OutboundProvider = DeliverableMessageProvider | "none";
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export type OutboundTargetResolution =
|
|||||||
| { ok: true; to: string }
|
| { ok: true; to: string }
|
||||||
| { ok: false; error: Error };
|
| { ok: false; error: Error };
|
||||||
|
|
||||||
export function resolveOutboundTarget(params: {
|
export function normalizeOutboundTarget(params: {
|
||||||
provider: GatewayMessageProvider;
|
provider: GatewayMessageProvider;
|
||||||
to?: string;
|
to?: string;
|
||||||
allowFrom?: 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: {
|
export function resolveHeartbeatDeliveryTarget(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
entry?: SessionEntry;
|
entry?: SessionEntry;
|
||||||
@@ -194,14 +202,14 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider !== "whatsapp") {
|
if (provider !== "whatsapp") {
|
||||||
const resolved = resolveOutboundTarget({ provider, to });
|
const resolved = normalizeOutboundTarget({ provider, to });
|
||||||
return resolved.ok
|
return resolved.ok
|
||||||
? { provider, to: resolved.to }
|
? { provider, to: resolved.to }
|
||||||
: { provider: "none", reason: "no-target" };
|
: { provider: "none", reason: "no-target" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||||
const resolved = resolveOutboundTarget({
|
const resolved = normalizeOutboundTarget({
|
||||||
provider: "whatsapp",
|
provider: "whatsapp",
|
||||||
to,
|
to,
|
||||||
allowFrom: rawAllow,
|
allowFrom: rawAllow,
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import {
|
|||||||
assertProvider,
|
assertProvider,
|
||||||
CONFIG_DIR,
|
CONFIG_DIR,
|
||||||
ensureDir,
|
ensureDir,
|
||||||
isWhatsAppGroupJid,
|
|
||||||
jidToE164,
|
jidToE164,
|
||||||
normalizeE164,
|
normalizeE164,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
normalizeWhatsAppTarget,
|
|
||||||
resolveJidToE164,
|
resolveJidToE164,
|
||||||
resolveUserPath,
|
resolveUserPath,
|
||||||
sleep,
|
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", () => {
|
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,34 +25,6 @@ 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, "");
|
||||||
|
|||||||
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