mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:41:23 +00:00
fix: suppress WhatsApp pairing replies for historical DMs
This commit is contained in:
86
src/web/inbound/access-control.pairing-history.test.ts
Normal file
86
src/web/inbound/access-control.pairing-history.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { checkInboundAccessControl } from "./access-control.js";
|
||||
|
||||
const sendMessageMock = vi.fn();
|
||||
const readAllowFromStoreMock = vi.fn();
|
||||
const upsertPairingRequestMock = vi.fn();
|
||||
|
||||
let config: Record<string, unknown> = {};
|
||||
|
||||
vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => config,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
sendMessageMock.mockReset().mockResolvedValue(undefined);
|
||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
});
|
||||
|
||||
describe("checkInboundAccessControl", () => {
|
||||
it("suppresses pairing replies for historical DMs on connect", async () => {
|
||||
const connectedAtMs = 1_000_000;
|
||||
const messageTimestampMs = connectedAtMs - 31_000;
|
||||
|
||||
const result = await checkInboundAccessControl({
|
||||
accountId: "default",
|
||||
from: "+15550001111",
|
||||
selfE164: "+15550009999",
|
||||
senderE164: "+15550001111",
|
||||
group: false,
|
||||
pushName: "Sam",
|
||||
isFromMe: false,
|
||||
messageTimestampMs,
|
||||
connectedAtMs,
|
||||
pairingGraceMs: 30_000,
|
||||
sock: { sendMessage: sendMessageMock },
|
||||
remoteJid: "15550001111@s.whatsapp.net",
|
||||
});
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(upsertPairingRequestMock).not.toHaveBeenCalled();
|
||||
expect(sendMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends pairing replies for live DMs", async () => {
|
||||
const connectedAtMs = 1_000_000;
|
||||
const messageTimestampMs = connectedAtMs - 10_000;
|
||||
|
||||
const result = await checkInboundAccessControl({
|
||||
accountId: "default",
|
||||
from: "+15550001111",
|
||||
selfE164: "+15550009999",
|
||||
senderE164: "+15550001111",
|
||||
group: false,
|
||||
pushName: "Sam",
|
||||
isFromMe: false,
|
||||
messageTimestampMs,
|
||||
connectedAtMs,
|
||||
pairingGraceMs: 30_000,
|
||||
sock: { sendMessage: sendMessageMock },
|
||||
remoteJid: "15550001111@s.whatsapp.net",
|
||||
});
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalled();
|
||||
expect(sendMessageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ export type InboundAccessControlResult = {
|
||||
resolvedAccountId: string;
|
||||
};
|
||||
|
||||
const PAIRING_REPLY_HISTORY_GRACE_MS = 30_000;
|
||||
|
||||
export async function checkInboundAccessControl(params: {
|
||||
accountId: string;
|
||||
from: string;
|
||||
@@ -23,6 +25,9 @@ export async function checkInboundAccessControl(params: {
|
||||
group: boolean;
|
||||
pushName?: string;
|
||||
isFromMe: boolean;
|
||||
messageTimestampMs?: number;
|
||||
connectedAtMs?: number;
|
||||
pairingGraceMs?: number;
|
||||
sock: {
|
||||
sendMessage: (jid: string, content: { text: string }) => Promise<unknown>;
|
||||
};
|
||||
@@ -48,6 +53,14 @@ export async function checkInboundAccessControl(params: {
|
||||
(configuredAllowFrom && configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined);
|
||||
const isSamePhone = params.from === params.selfE164;
|
||||
const isSelfChat = isSelfChatMode(params.selfE164, configuredAllowFrom);
|
||||
const pairingGraceMs =
|
||||
typeof params.pairingGraceMs === "number" && params.pairingGraceMs > 0
|
||||
? params.pairingGraceMs
|
||||
: PAIRING_REPLY_HISTORY_GRACE_MS;
|
||||
const suppressPairingReply =
|
||||
typeof params.connectedAtMs === "number" &&
|
||||
typeof params.messageTimestampMs === "number" &&
|
||||
params.messageTimestampMs < params.connectedAtMs - pairingGraceMs;
|
||||
|
||||
// Pre-compute normalized allowlists for filtering.
|
||||
const dmHasWildcard = allowFrom?.includes("*") ?? false;
|
||||
@@ -128,25 +141,29 @@ export async function checkInboundAccessControl(params: {
|
||||
(normalizedAllowFrom.length > 0 && normalizedAllowFrom.includes(candidate));
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "whatsapp",
|
||||
id: candidate,
|
||||
meta: { name: (params.pushName ?? "").trim() || undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`,
|
||||
);
|
||||
try {
|
||||
await params.sock.sendMessage(params.remoteJid, {
|
||||
text: buildPairingReply({
|
||||
channel: "whatsapp",
|
||||
idLine: `Your WhatsApp phone number: ${candidate}`,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`whatsapp pairing reply failed for ${candidate}: ${String(err)}`);
|
||||
if (suppressPairingReply) {
|
||||
logVerbose(`Skipping pairing reply for historical DM from ${candidate}.`);
|
||||
} else {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "whatsapp",
|
||||
id: candidate,
|
||||
meta: { name: (params.pushName ?? "").trim() || undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`,
|
||||
);
|
||||
try {
|
||||
await params.sock.sendMessage(params.remoteJid, {
|
||||
text: buildPairingReply({
|
||||
channel: "whatsapp",
|
||||
idLine: `Your WhatsApp phone number: ${candidate}`,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`whatsapp pairing reply failed for ${candidate}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function monitorWebInbox(options: {
|
||||
authDir: options.authDir,
|
||||
});
|
||||
await waitForWaConnection(sock);
|
||||
const connectedAtMs = Date.now();
|
||||
|
||||
let onCloseResolve: ((reason: WebListenerCloseReason) => void) | null = null;
|
||||
const onClose = new Promise<WebListenerCloseReason>((resolve) => {
|
||||
@@ -171,6 +172,9 @@ export async function monitorWebInbox(options: {
|
||||
groupSubject = meta.subject;
|
||||
groupParticipants = meta.participants;
|
||||
}
|
||||
const messageTimestampMs = msg.messageTimestamp
|
||||
? Number(msg.messageTimestamp) * 1000
|
||||
: undefined;
|
||||
|
||||
const access = await checkInboundAccessControl({
|
||||
accountId: options.accountId,
|
||||
@@ -180,6 +184,8 @@ export async function monitorWebInbox(options: {
|
||||
group,
|
||||
pushName: msg.pushName ?? undefined,
|
||||
isFromMe: Boolean(msg.key?.fromMe),
|
||||
messageTimestampMs,
|
||||
connectedAtMs,
|
||||
sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) },
|
||||
remoteJid,
|
||||
});
|
||||
@@ -253,7 +259,7 @@ export async function monitorWebInbox(options: {
|
||||
const sendMedia = async (payload: AnyMessageContent) => {
|
||||
await sock.sendMessage(chatJid, payload);
|
||||
};
|
||||
const timestamp = msg.messageTimestamp ? Number(msg.messageTimestamp) * 1000 : undefined;
|
||||
const timestamp = messageTimestampMs;
|
||||
const mentionedJids = extractMentionedJids(msg.message as proto.IMessage | undefined);
|
||||
const senderName = msg.pushName ?? undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user