mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 02:22:46 +00:00
fix(whatsapp): resolve approval reactions across jid drift (#95935)
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
registerWhatsAppApprovalReactionTargetForOutboundMessage,
|
||||
resolveWhatsAppApprovalReactionTargetWithPersistence,
|
||||
} from "./approval-reactions.js";
|
||||
import { resolveEquivalentWhatsAppDirectChatJids, type LidLookup } from "./text-runtime.js";
|
||||
|
||||
const resolverMocks = vi.hoisted(() => ({
|
||||
resolveWhatsAppApproval: vi.fn(),
|
||||
@@ -19,6 +20,53 @@ vi.mock("./approval-resolver.js", () => ({
|
||||
isApprovalNotFoundError: resolverMocks.isApprovalNotFoundError,
|
||||
}));
|
||||
|
||||
function approvalConfig(allowFrom: string[]) {
|
||||
return {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function registerExecApprovalTarget(params: { remoteJid: string; approvalId?: string }): void {
|
||||
registerWhatsAppApprovalReactionTarget({
|
||||
accountId: "default",
|
||||
remoteJid: params.remoteJid,
|
||||
messageId: "approval-message",
|
||||
approvalId: params.approvalId ?? "exec-direct",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
});
|
||||
}
|
||||
|
||||
function buildReactionMessage(params: {
|
||||
remoteJid: string;
|
||||
reactionRemoteJid?: string;
|
||||
participant?: string;
|
||||
fromMe?: boolean;
|
||||
reactionFromMe?: boolean;
|
||||
}) {
|
||||
return {
|
||||
key: {
|
||||
id: "reaction-message",
|
||||
remoteJid: params.remoteJid,
|
||||
...(params?.participant ? { participant: params.participant } : {}),
|
||||
fromMe: params.fromMe ?? false,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: params.reactionRemoteJid ?? params.remoteJid,
|
||||
id: "approval-message",
|
||||
...(params.reactionFromMe === undefined ? {} : { fromMe: params.reactionFromMe }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
describe("WhatsApp approval reactions", () => {
|
||||
beforeEach(() => {
|
||||
clearWhatsAppApprovalReactionTargetsForTest();
|
||||
@@ -131,43 +179,19 @@ describe("WhatsApp approval reactions", () => {
|
||||
});
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230000"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
participant: "15551230000@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
id: "approval-message",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
participant: "15551230000@s.whatsapp.net",
|
||||
}),
|
||||
resolveInboundJid: async (jid) =>
|
||||
jid === "15551230000@s.whatsapp.net" ? "+15551230000" : null,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(resolverMocks.resolveWhatsAppApproval).toHaveBeenCalledWith({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230000"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
approvalId: "plugin:abc",
|
||||
decision: "allow-once",
|
||||
senderId: "+15551230000",
|
||||
@@ -185,44 +209,20 @@ describe("WhatsApp approval reactions", () => {
|
||||
});
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230001"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230001"]),
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
id: "reaction-message",
|
||||
remoteJid: "276853659042038@lid",
|
||||
fromMe: true,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "276853659042038@lid",
|
||||
id: "approval-message",
|
||||
fromMe: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: "276853659042038@lid",
|
||||
fromMe: true,
|
||||
reactionFromMe: true,
|
||||
}),
|
||||
selfLid: "276853659042038@lid",
|
||||
resolveInboundJid: async (jid) => (jid === "276853659042038@lid" ? "+15551230001" : null),
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(resolverMocks.resolveWhatsAppApproval).toHaveBeenCalledWith({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230001"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230001"]),
|
||||
approvalId: "exec-self",
|
||||
decision: "allow-once",
|
||||
senderId: "+15551230001",
|
||||
@@ -230,6 +230,124 @@ describe("WhatsApp approval reactions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "stored PN target from outer chat JID",
|
||||
storedRemoteJid: "15551230001@s.whatsapp.net",
|
||||
eventRemoteJid: "15551230001@s.whatsapp.net",
|
||||
reactionRemoteJid: "276853659042038@lid",
|
||||
actorId: "+15551230001",
|
||||
},
|
||||
{
|
||||
name: "stored LID target from PN event",
|
||||
storedRemoteJid: "276853659042038@lid",
|
||||
eventRemoteJid: "15551230001@s.whatsapp.net",
|
||||
actorId: "+15551230001",
|
||||
lidForPn: "276853659042038@lid",
|
||||
},
|
||||
{
|
||||
name: "stored PN target from LID event",
|
||||
storedRemoteJid: "15551230001@s.whatsapp.net",
|
||||
eventRemoteJid: "276853659042038@lid",
|
||||
actorId: "+15551230001",
|
||||
pnForLid: "15551230001:0@s.whatsapp.net",
|
||||
},
|
||||
{
|
||||
name: "stored PN target from device-qualified PN event",
|
||||
storedRemoteJid: "15551230001@s.whatsapp.net",
|
||||
eventRemoteJid: "15551230001:0@s.whatsapp.net",
|
||||
actorId: "+15551230001",
|
||||
},
|
||||
{
|
||||
name: "stored LID target from device-qualified LID event",
|
||||
storedRemoteJid: "276853659042038@lid",
|
||||
eventRemoteJid: "276853659042038:1@lid",
|
||||
actorId: "+15551230001",
|
||||
},
|
||||
])("resolves direct approval reactions across PN/LID target drift: $name", async (testCase) => {
|
||||
registerExecApprovalTarget({ remoteJid: testCase.storedRemoteJid });
|
||||
const lidLookup: LidLookup = {
|
||||
getLIDForPN: vi.fn().mockResolvedValue(testCase.lidForPn ?? null),
|
||||
getPNForLID: vi.fn().mockResolvedValue(testCase.pnForLid ?? null),
|
||||
};
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: approvalConfig([testCase.actorId]),
|
||||
accountId: "default",
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: testCase.eventRemoteJid,
|
||||
reactionRemoteJid: testCase.reactionRemoteJid,
|
||||
}),
|
||||
resolveInboundJid: async (jid) => (jid === testCase.eventRemoteJid ? testCase.actorId : null),
|
||||
resolveReactionTargetJids: async (jid) =>
|
||||
resolveEquivalentWhatsAppDirectChatJids(jid, { lidLookup }),
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(resolverMocks.resolveWhatsAppApproval).toHaveBeenCalledWith({
|
||||
cfg: approvalConfig([testCase.actorId]),
|
||||
approvalId: "exec-direct",
|
||||
decision: "allow-once",
|
||||
senderId: testCase.actorId,
|
||||
gatewayUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use a group reaction actor as a direct-chat target candidate", async () => {
|
||||
registerExecApprovalTarget({ remoteJid: "15551230000@s.whatsapp.net" });
|
||||
const lidLookup: LidLookup = {
|
||||
getLIDForPN: vi.fn().mockResolvedValue("15551230000@s.whatsapp.net"),
|
||||
getPNForLID: vi.fn().mockResolvedValue("15551230000@s.whatsapp.net"),
|
||||
};
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
accountId: "default",
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
participant: "15551230000@s.whatsapp.net",
|
||||
}),
|
||||
resolveInboundJid: async () => "+15551230000",
|
||||
resolveReactionTargetJids: async (jid) =>
|
||||
resolveEquivalentWhatsAppDirectChatJids(jid, { lidLookup }),
|
||||
});
|
||||
|
||||
expect(handled).toBe(false);
|
||||
expect(resolverMocks.resolveWhatsAppApproval).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unregisters the matched target candidate when an approval expired", async () => {
|
||||
registerExecApprovalTarget({
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
approvalId: "exec-expired",
|
||||
});
|
||||
resolverMocks.resolveWhatsAppApproval.mockRejectedValueOnce(
|
||||
new Error("unknown or expired approval id"),
|
||||
);
|
||||
resolverMocks.isApprovalNotFoundError.mockReturnValue(true);
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
accountId: "default",
|
||||
msg: buildReactionMessage({ remoteJid: "276853659042038@lid" }),
|
||||
resolveInboundJid: async () => "+15551230000",
|
||||
resolveReactionTargetJids: async (jid) =>
|
||||
resolveEquivalentWhatsAppDirectChatJids(jid, {
|
||||
lidLookup: { getPNForLID: vi.fn().mockResolvedValue("15551230000@s.whatsapp.net") },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
await expect(
|
||||
resolveWhatsAppApprovalReactionTargetWithPersistence({
|
||||
accountId: "default",
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
messageId: "approval-message",
|
||||
reactionKey: "👍",
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("does not attribute a peer DM fromMe reaction to the peer", async () => {
|
||||
registerWhatsAppApprovalReactionTarget({
|
||||
accountId: "default",
|
||||
@@ -240,31 +358,13 @@ describe("WhatsApp approval reactions", () => {
|
||||
});
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230000"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
id: "reaction-message",
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
fromMe: true,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
id: "approval-message",
|
||||
fromMe: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
fromMe: true,
|
||||
reactionFromMe: true,
|
||||
}),
|
||||
selfLid: "276853659042038@lid",
|
||||
resolveInboundJid: async (jid) => {
|
||||
if (jid === "15551230000@s.whatsapp.net") {
|
||||
@@ -291,29 +391,9 @@ describe("WhatsApp approval reactions", () => {
|
||||
});
|
||||
|
||||
const handled = await maybeResolveWhatsAppApprovalReaction({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551230000"],
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: approvalConfig(["+15551230000"]),
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
fromMe: false,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
id: "approval-message",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({ remoteJid: "120363401234567890@g.us" }),
|
||||
resolveInboundJid: async () => null,
|
||||
});
|
||||
|
||||
@@ -337,21 +417,7 @@ describe("WhatsApp approval reactions", () => {
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "15551230000@s.whatsapp.net",
|
||||
id: "approval-message",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({ remoteJid: "15551230000@s.whatsapp.net" }),
|
||||
resolveInboundJid: async () => "+15551230000",
|
||||
});
|
||||
|
||||
@@ -375,22 +441,10 @@ describe("WhatsApp approval reactions", () => {
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
msg: {
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
participant: "15551230000@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
message: {
|
||||
reactionMessage: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
id: "approval-message",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
msg: buildReactionMessage({
|
||||
remoteJid: "120363401234567890@g.us",
|
||||
participant: "15551230000@s.whatsapp.net",
|
||||
}),
|
||||
resolveInboundJid: async () => "+15551230000",
|
||||
});
|
||||
|
||||
|
||||
@@ -26,12 +26,16 @@ type WhatsAppApprovalReactionResolution = {
|
||||
type WhatsAppApprovalReactionTarget = ApprovalReactionTargetRecord;
|
||||
|
||||
type WhatsAppApprovalReactionEvent = {
|
||||
remoteJid: string;
|
||||
remoteJids: string[];
|
||||
messageId: string;
|
||||
actorJid: string;
|
||||
reactionKey: string;
|
||||
};
|
||||
|
||||
type ResolvedWhatsAppApprovalReactionTarget = WhatsAppApprovalReactionResolution & {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
let resolverRuntimePromise: Promise<typeof import("./approval-resolver.js")> | undefined;
|
||||
|
||||
const whatsappApprovalReactionTargets =
|
||||
@@ -63,6 +67,13 @@ function buildReactionTargetKey(params: {
|
||||
return `${accountId}:${remoteJid}:${messageId}`;
|
||||
}
|
||||
|
||||
function addCandidateRemoteJid(target: string[], value: string | null | undefined): void {
|
||||
const remoteJid = value?.trim();
|
||||
if (remoteJid && !target.includes(remoteJid)) {
|
||||
target.push(remoteJid);
|
||||
}
|
||||
}
|
||||
|
||||
function reportPersistentApprovalReactionError(error: unknown): void {
|
||||
try {
|
||||
getOptionalWhatsAppRuntime()
|
||||
@@ -228,6 +239,42 @@ export async function resolveWhatsAppApprovalReactionTargetWithPersistence(param
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveWhatsAppApprovalReactionTargetFromCandidates(params: {
|
||||
accountId: string;
|
||||
observedRemoteJids: readonly string[];
|
||||
messageId: string;
|
||||
reactionKey: string;
|
||||
resolveReactionTargetJids?: (jid: string) => Promise<readonly string[]>;
|
||||
logVerboseMessage?: (message: string) => void;
|
||||
}): Promise<ResolvedWhatsAppApprovalReactionTarget | null> {
|
||||
const candidateRemoteJids: string[] = [];
|
||||
for (const observedRemoteJid of params.observedRemoteJids) {
|
||||
addCandidateRemoteJid(candidateRemoteJids, observedRemoteJid);
|
||||
try {
|
||||
for (const candidate of (await params.resolveReactionTargetJids?.(observedRemoteJid)) ?? []) {
|
||||
addCandidateRemoteJid(candidateRemoteJids, candidate);
|
||||
}
|
||||
} catch (error) {
|
||||
params.logVerboseMessage?.(
|
||||
`whatsapp: approval reaction target JID mapping failed for ${observedRemoteJid}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const remoteJid of candidateRemoteJids) {
|
||||
const target = await resolveWhatsAppApprovalReactionTargetWithPersistence({
|
||||
accountId: params.accountId,
|
||||
remoteJid,
|
||||
messageId: params.messageId,
|
||||
reactionKey: params.reactionKey,
|
||||
});
|
||||
if (target) {
|
||||
return { ...target, remoteJid };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readWhatsAppApprovalReactionEvent(params: {
|
||||
msg: WAMessage;
|
||||
selfJid?: string | null;
|
||||
@@ -237,17 +284,19 @@ function readWhatsAppApprovalReactionEvent(params: {
|
||||
const reaction = msg.message?.reactionMessage;
|
||||
const reactionKey = reaction?.text?.trim() ?? "";
|
||||
const messageId = reaction?.key?.id?.trim() ?? "";
|
||||
const remoteJid = (reaction?.key?.remoteJid ?? msg.key?.remoteJid ?? "").trim();
|
||||
const remoteJids: string[] = [];
|
||||
addCandidateRemoteJid(remoteJids, reaction?.key?.remoteJid);
|
||||
addCandidateRemoteJid(remoteJids, msg.key?.remoteJid);
|
||||
const actorJid =
|
||||
msg.key?.participant?.trim() ||
|
||||
(msg.key?.fromMe
|
||||
? (params.selfLid?.trim() ?? params.selfJid?.trim() ?? "")
|
||||
: (msg.key?.remoteJid?.trim() ?? ""));
|
||||
if (!reactionKey || !messageId || !remoteJid || !actorJid) {
|
||||
if (!reactionKey || !messageId || remoteJids.length === 0 || !actorJid) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
remoteJid,
|
||||
remoteJids,
|
||||
messageId,
|
||||
actorJid,
|
||||
reactionKey,
|
||||
@@ -262,6 +311,7 @@ export async function maybeResolveWhatsAppApprovalReaction(params: {
|
||||
selfJid?: string | null;
|
||||
selfLid?: string | null;
|
||||
resolveInboundJid: (jid: string | null | undefined) => Promise<string | null>;
|
||||
resolveReactionTargetJids?: (jid: string) => Promise<readonly string[]>;
|
||||
logVerboseMessage?: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
const event = readWhatsAppApprovalReactionEvent({
|
||||
@@ -272,11 +322,13 @@ export async function maybeResolveWhatsAppApprovalReaction(params: {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const target = await resolveWhatsAppApprovalReactionTargetWithPersistence({
|
||||
const target = await resolveWhatsAppApprovalReactionTargetFromCandidates({
|
||||
accountId: params.accountId,
|
||||
remoteJid: event.remoteJid,
|
||||
observedRemoteJids: event.remoteJids,
|
||||
messageId: event.messageId,
|
||||
reactionKey: event.reactionKey,
|
||||
resolveReactionTargetJids: params.resolveReactionTargetJids,
|
||||
logVerboseMessage: params.logVerboseMessage,
|
||||
});
|
||||
if (!target) {
|
||||
return false;
|
||||
@@ -329,7 +381,7 @@ export async function maybeResolveWhatsAppApprovalReaction(params: {
|
||||
if (isApprovalNotFoundError(error)) {
|
||||
unregisterWhatsAppApprovalReactionTarget({
|
||||
accountId: params.accountId,
|
||||
remoteJid: event.remoteJid,
|
||||
remoteJid: target.remoteJid,
|
||||
messageId: event.messageId,
|
||||
});
|
||||
params.logVerboseMessage?.(
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
type WhatsAppSocketOperationAdapter,
|
||||
type WhatsAppSocketTimingOptions,
|
||||
} from "../socket-timing.js";
|
||||
import { resolveJidToE164 } from "../text-runtime.js";
|
||||
import { resolveEquivalentWhatsAppDirectChatJids, resolveJidToE164 } from "../text-runtime.js";
|
||||
import {
|
||||
checkInboundAccessControl,
|
||||
type AcceptedInboundAccessControlResult,
|
||||
@@ -556,6 +556,8 @@ export async function attachWebInboxToSocket(
|
||||
|
||||
const resolveInboundJid = async (jid: string | null | undefined): Promise<string | null> =>
|
||||
resolveJidToE164(jid, { authDir: options.authDir, lidLookup });
|
||||
const resolveReactionTargetJids = async (jid: string): Promise<string[]> =>
|
||||
resolveEquivalentWhatsAppDirectChatJids(jid, { authDir: options.authDir, lidLookup });
|
||||
|
||||
const rememberBaileysMessage = (
|
||||
remoteJid: string | null | undefined,
|
||||
@@ -1357,6 +1359,7 @@ export async function attachWebInboxToSocket(
|
||||
selfJid: self.jid,
|
||||
selfLid: self.lid,
|
||||
resolveInboundJid,
|
||||
resolveReactionTargetJids,
|
||||
logVerboseMessage: (message) => logWhatsAppVerbose(options.verbose, message),
|
||||
})
|
||||
) {
|
||||
|
||||
@@ -75,10 +75,89 @@ export type JidToE164Options = {
|
||||
logMissing?: boolean;
|
||||
};
|
||||
|
||||
type LidLookup = {
|
||||
export type LidLookup = {
|
||||
getLIDForPN?: (jid: string) => Promise<string | null>;
|
||||
getPNForLID?: (jid: string) => Promise<string | null>;
|
||||
};
|
||||
|
||||
function addUniqueString(target: string[], value: string | null | undefined): void {
|
||||
const normalized = value?.trim();
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryLookupMappedJid(
|
||||
lookup: (() => Promise<string | null> | undefined) | undefined,
|
||||
): Promise<string | null> {
|
||||
if (!lookup) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (await lookup()) ?? null;
|
||||
} catch (err) {
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(`LID mapping lookup failed: ${String(err)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DIRECT_PN_JID_RE = /^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted)$/i;
|
||||
const DIRECT_LID_JID_RE = /^(\d+)(?::\d+)?@(lid|hosted\.lid)$/i;
|
||||
|
||||
function addEquivalentDirectChatCandidate(target: string[], jid: string | null | undefined): void {
|
||||
addUniqueString(target, jid);
|
||||
const pnMatch = jid?.match(DIRECT_PN_JID_RE);
|
||||
if (pnMatch) {
|
||||
addUniqueString(target, `${pnMatch[1]}@${pnMatch[2]}`);
|
||||
return;
|
||||
}
|
||||
const lidMatch = jid?.match(DIRECT_LID_JID_RE);
|
||||
if (lidMatch) {
|
||||
addUniqueString(target, `${lidMatch[1]}@${lidMatch[2]}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveEquivalentWhatsAppDirectChatJids(
|
||||
jid: string | null | undefined,
|
||||
opts?: JidToE164Options & { lidLookup?: LidLookup },
|
||||
): Promise<string[]> {
|
||||
const normalized = jid?.trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: string[] = [];
|
||||
addEquivalentDirectChatCandidate(candidates, normalized);
|
||||
const pnMatch = normalized.match(DIRECT_PN_JID_RE);
|
||||
if (pnMatch) {
|
||||
const mappedLid = await tryLookupMappedJid(() => opts?.lidLookup?.getLIDForPN?.(normalized));
|
||||
addEquivalentDirectChatCandidate(candidates, mappedLid);
|
||||
|
||||
const mappedLocalLid = readLidForwardMapping({ phoneDigits: pnMatch[1], opts });
|
||||
const localLidDomain = pnMatch[2].toLowerCase() === "hosted" ? "hosted.lid" : "lid";
|
||||
addUniqueString(candidates, mappedLocalLid ? `${mappedLocalLid}@${localLidDomain}` : null);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
const lidMatch = normalized.match(DIRECT_LID_JID_RE);
|
||||
if (lidMatch) {
|
||||
const mappedPn = await tryLookupMappedJid(() => opts?.lidLookup?.getPNForLID?.(normalized));
|
||||
addEquivalentDirectChatCandidate(candidates, mappedPn);
|
||||
|
||||
const e164 = jidToE164(normalized, { ...opts, logMissing: false });
|
||||
const localPnJid =
|
||||
e164 && lidMatch[2].toLowerCase() === "hosted.lid"
|
||||
? `${e164.replace(/\D/g, "")}@hosted`
|
||||
: e164
|
||||
? toWhatsappJid(e164)
|
||||
: null;
|
||||
addUniqueString(candidates, localPnJid);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function resolveLidMappingDirs(params: { opts?: JidToE164Options }): string[] {
|
||||
const dirs = new Set<string>();
|
||||
const addDir = (dir?: string | null) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
assertWebChannel,
|
||||
jidToE164,
|
||||
markdownToWhatsApp,
|
||||
resolveEquivalentWhatsAppDirectChatJids,
|
||||
resolveJidToE164,
|
||||
toWhatsappJid,
|
||||
toWhatsappJidWithLid,
|
||||
@@ -210,3 +211,34 @@ describe("resolveJidToE164", () => {
|
||||
expect(lidLookup.getPNForLID).toHaveBeenCalledWith("777@lid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEquivalentWhatsAppDirectChatJids", () => {
|
||||
it.each([
|
||||
["15551230000:0@s.whatsapp.net", "15551230000@s.whatsapp.net"],
|
||||
["15551230000:2@hosted", "15551230000@hosted"],
|
||||
["777:1@lid", "777@lid"],
|
||||
["777:2@hosted.lid", "777@hosted.lid"],
|
||||
])("includes the bare direct-chat form for %s", async (observedJid, bareJid) => {
|
||||
await expect(resolveEquivalentWhatsAppDirectChatJids(observedJid)).resolves.toEqual([
|
||||
observedJid,
|
||||
bareJid,
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves hosted direct-chat domains for local PN/LID mappings", async () => {
|
||||
await withTempDir("whatsapp-hosted-lid-map-", async (authDir) => {
|
||||
fs.writeFileSync(path.join(authDir, "lid-mapping-15551230000.json"), JSON.stringify("777"));
|
||||
fs.writeFileSync(
|
||||
path.join(authDir, "lid-mapping-777_reverse.json"),
|
||||
JSON.stringify("15551230000"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveEquivalentWhatsAppDirectChatJids("15551230000@hosted", { authDir }),
|
||||
).resolves.toEqual(["15551230000@hosted", "777@hosted.lid"]);
|
||||
await expect(
|
||||
resolveEquivalentWhatsAppDirectChatJids("777@hosted.lid", { authDir }),
|
||||
).resolves.toEqual(["777@hosted.lid", "15551230000@hosted"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,11 @@ export {
|
||||
isSelfChatMode,
|
||||
jidToE164,
|
||||
markdownToWhatsApp,
|
||||
resolveEquivalentWhatsAppDirectChatJids,
|
||||
resolveJidToE164,
|
||||
toWhatsappJid,
|
||||
toWhatsappJidWithLid,
|
||||
type JidToE164Options,
|
||||
type LidLookup,
|
||||
type WebChannel,
|
||||
} from "./targets-runtime.js";
|
||||
|
||||
Reference in New Issue
Block a user