feat(whatsapp,telegram): add groupPolicy config option (#216)
Co-authored-by: Marcus Neves <conhecendo.contato@gmail.com> Co-authored-by: Shadow <hi@shadowing.dev>
This commit is contained in:
@@ -176,16 +176,44 @@ export async function monitorWebInbox(options: {
|
||||
const isSamePhone = from === selfE164;
|
||||
const isSelfChat = isSelfChatMode(selfE164, configuredAllowFrom);
|
||||
|
||||
// Pre-compute normalized allowlist for filtering (used by both group and DM checks)
|
||||
const hasWildcard = allowFrom?.includes("*") ?? false;
|
||||
const normalizedAllowFrom =
|
||||
allowFrom && allowFrom.length > 0 ? allowFrom.map(normalizeE164) : [];
|
||||
|
||||
// Group policy filtering: controls how group messages are handled
|
||||
// - "open" (default): groups bypass allowFrom, only mention-gating applies
|
||||
// - "disabled": block all group messages entirely
|
||||
// - "allowlist": only allow group messages from senders in allowFrom
|
||||
const groupPolicy = cfg.whatsapp?.groupPolicy ?? "open";
|
||||
if (group && groupPolicy === "disabled") {
|
||||
logVerbose(`Blocked group message (groupPolicy: disabled)`);
|
||||
continue;
|
||||
}
|
||||
if (group && groupPolicy === "allowlist") {
|
||||
// For allowlist mode, the sender (participant) must be in allowFrom
|
||||
// If we can't resolve the sender E164, block the message for safety
|
||||
const senderAllowed =
|
||||
hasWildcard ||
|
||||
(senderE164 != null && normalizedAllowFrom.includes(senderE164));
|
||||
if (!senderAllowed) {
|
||||
logVerbose(
|
||||
`Blocked group message from ${senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// DM allowlist filtering (unchanged behavior)
|
||||
const allowlistEnabled =
|
||||
!group && Array.isArray(allowFrom) && allowFrom.length > 0;
|
||||
if (!isSamePhone && allowlistEnabled) {
|
||||
const candidate = from;
|
||||
const allowedList = allowFrom.map(normalizeE164);
|
||||
if (!allowFrom.includes("*") && !allowedList.includes(candidate)) {
|
||||
if (!hasWildcard && !normalizedAllowFrom.includes(candidate)) {
|
||||
logVerbose(
|
||||
`Blocked unauthorized sender ${candidate} (not in allowFrom list)`,
|
||||
);
|
||||
continue; // Skip processing entirely
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -670,6 +670,175 @@ describe("web monitor inbox", () => {
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("blocks all group messages when groupPolicy is 'disabled'", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
whatsapp: {
|
||||
allowFrom: ["+1234"],
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
timestampPrefix: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "grp-disabled",
|
||||
fromMe: false,
|
||||
remoteJid: "11111@g.us",
|
||||
participant: "999@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "group message should be blocked" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// Should NOT call onMessage because groupPolicy is disabled
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("blocks group messages from senders not in allowFrom when groupPolicy is 'allowlist'", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
whatsapp: {
|
||||
allowFrom: ["+1234"], // Does not include +999
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
timestampPrefix: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "grp-allowlist-blocked",
|
||||
fromMe: false,
|
||||
remoteJid: "11111@g.us",
|
||||
participant: "999@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "unauthorized group sender" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// Should NOT call onMessage because sender +999 not in allowFrom
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("allows group messages from senders in allowFrom when groupPolicy is 'allowlist'", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
whatsapp: {
|
||||
allowFrom: ["+15551234567"], // Includes the sender
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
timestampPrefix: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "grp-allowlist-allowed",
|
||||
fromMe: false,
|
||||
remoteJid: "11111@g.us",
|
||||
participant: "15551234567@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "authorized group sender" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// Should call onMessage because sender is in allowFrom
|
||||
expect(onMessage).toHaveBeenCalledTimes(1);
|
||||
const payload = onMessage.mock.calls[0][0];
|
||||
expect(payload.chatType).toBe("group");
|
||||
expect(payload.senderE164).toBe("+15551234567");
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("allows all group senders with wildcard in groupPolicy allowlist", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
whatsapp: {
|
||||
allowFrom: ["*"], // Wildcard allows everyone
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
timestampPrefix: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "grp-wildcard-test",
|
||||
fromMe: false,
|
||||
remoteJid: "22222@g.us",
|
||||
participant: "9999999999@s.whatsapp.net", // Random sender
|
||||
},
|
||||
message: { conversation: "wildcard group sender" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// Should call onMessage because wildcard allows all senders
|
||||
expect(onMessage).toHaveBeenCalledTimes(1);
|
||||
const payload = onMessage.mock.calls[0][0];
|
||||
expect(payload.chatType).toBe("group");
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("allows messages from senders in allowFrom list", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
whatsapp: {
|
||||
|
||||
Reference in New Issue
Block a user