refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger
2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@@ -31,7 +31,7 @@ export type ResolvedWhatsAppAccount = {
};
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
const accounts = cfg.whatsapp?.accounts;
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return [];
return Object.keys(accounts).filter(Boolean);
}
@@ -52,7 +52,7 @@ function resolveAccountConfig(
cfg: ClawdbotConfig,
accountId: string,
): WhatsAppAccountConfig | undefined {
const accounts = cfg.whatsapp?.accounts;
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return undefined;
const entry = accounts[accountId] as WhatsAppAccountConfig | undefined;
return entry;
@@ -115,23 +115,31 @@ export function resolveWhatsAppAccount(params: {
enabled,
messagePrefix:
accountCfg?.messagePrefix ??
params.cfg.whatsapp?.messagePrefix ??
params.cfg.channels?.whatsapp?.messagePrefix ??
params.cfg.messages?.messagePrefix,
authDir,
isLegacyAuthDir: isLegacy,
selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode,
dmPolicy: accountCfg?.dmPolicy ?? params.cfg.whatsapp?.dmPolicy,
allowFrom: accountCfg?.allowFrom ?? params.cfg.whatsapp?.allowFrom,
selfChatMode:
accountCfg?.selfChatMode ?? params.cfg.channels?.whatsapp?.selfChatMode,
dmPolicy: accountCfg?.dmPolicy ?? params.cfg.channels?.whatsapp?.dmPolicy,
allowFrom:
accountCfg?.allowFrom ?? params.cfg.channels?.whatsapp?.allowFrom,
groupAllowFrom:
accountCfg?.groupAllowFrom ?? params.cfg.whatsapp?.groupAllowFrom,
groupPolicy: accountCfg?.groupPolicy ?? params.cfg.whatsapp?.groupPolicy,
accountCfg?.groupAllowFrom ??
params.cfg.channels?.whatsapp?.groupAllowFrom,
groupPolicy:
accountCfg?.groupPolicy ?? params.cfg.channels?.whatsapp?.groupPolicy,
textChunkLimit:
accountCfg?.textChunkLimit ?? params.cfg.whatsapp?.textChunkLimit,
mediaMaxMb: accountCfg?.mediaMaxMb ?? params.cfg.whatsapp?.mediaMaxMb,
accountCfg?.textChunkLimit ??
params.cfg.channels?.whatsapp?.textChunkLimit,
mediaMaxMb:
accountCfg?.mediaMaxMb ?? params.cfg.channels?.whatsapp?.mediaMaxMb,
blockStreaming:
accountCfg?.blockStreaming ?? params.cfg.whatsapp?.blockStreaming,
ackReaction: accountCfg?.ackReaction ?? params.cfg.whatsapp?.ackReaction,
groups: accountCfg?.groups ?? params.cfg.whatsapp?.groups,
accountCfg?.blockStreaming ??
params.cfg.channels?.whatsapp?.blockStreaming,
ackReaction:
accountCfg?.ackReaction ?? params.cfg.channels?.whatsapp?.ackReaction,
groups: accountCfg?.groups ?? params.cfg.channels?.whatsapp?.groups,
};
}

View File

@@ -7,7 +7,7 @@ import { info, success } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import type { Provider } from "../utils.js";
import type { WebChannel } from "../utils.js";
import { jidToE164, resolveUserPath } from "../utils.js";
export function resolveDefaultWebAuthDir(): string {
@@ -161,7 +161,7 @@ export function getWebAuthAgeMs(
export function logWebSelfId(
authDir: string = resolveDefaultWebAuthDir(),
runtime: RuntimeEnv = defaultRuntime,
includeProviderPrefix = false,
includeChannelPrefix = false,
) {
// Human-friendly log of the currently linked personal web session.
const { e164, jid } = readWebSelfId(authDir);
@@ -169,19 +169,19 @@ export function logWebSelfId(
e164 || jid
? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}`
: "unknown";
const prefix = includeProviderPrefix ? "Web Provider: " : "";
const prefix = includeChannelPrefix ? "Web Channel: " : "";
runtime.log(info(`${prefix}${details}`));
}
export async function pickProvider(
pref: Provider | "auto",
export async function pickWebChannel(
pref: WebChannel | "auto",
authDir: string = resolveDefaultWebAuthDir(),
): Promise<Provider> {
const choice: Provider = pref === "auto" ? "web" : pref;
): Promise<WebChannel> {
const choice: WebChannel = pref === "auto" ? "web" : pref;
const hasWeb = await webAuthExists(authDir);
if (!hasWeb) {
throw new Error(
"No WhatsApp Web session found. Run `clawdbot providers login --verbose` to link.",
"No WhatsApp Web session found. Run `clawdbot channels login --channel whatsapp --verbose` to link.",
);
}
return choice;

View File

@@ -12,7 +12,7 @@ describe("WhatsApp ack reaction logic", () => {
},
groupActivation?: "always" | "mention",
): boolean {
const ackConfig = cfg.whatsapp?.ackReaction;
const ackConfig = cfg.channels?.whatsapp?.ackReaction;
const emoji = (ackConfig?.emoji ?? "").trim();
const directEnabled = ackConfig?.direct ?? true;
const groupMode = ackConfig?.group ?? "mentions";
@@ -43,7 +43,7 @@ describe("WhatsApp ack reaction logic", () => {
describe("direct chat", () => {
it("should react when direct=true", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", direct: true } },
channels: { whatsapp: { ackReaction: { emoji: "👀", direct: true } } },
};
expect(
shouldSendReaction(cfg, {
@@ -55,7 +55,7 @@ describe("WhatsApp ack reaction logic", () => {
it("should not react when direct=false", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", direct: false } },
channels: { whatsapp: { ackReaction: { emoji: "👀", direct: false } } },
};
expect(
shouldSendReaction(cfg, {
@@ -67,7 +67,7 @@ describe("WhatsApp ack reaction logic", () => {
it("should not react when emoji is empty", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "", direct: true } },
channels: { whatsapp: { ackReaction: { emoji: "", direct: true } } },
};
expect(
shouldSendReaction(cfg, {
@@ -79,7 +79,7 @@ describe("WhatsApp ack reaction logic", () => {
it("should not react when message id is missing", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", direct: true } },
channels: { whatsapp: { ackReaction: { emoji: "👀", direct: true } } },
};
expect(
shouldSendReaction(cfg, {
@@ -92,7 +92,9 @@ describe("WhatsApp ack reaction logic", () => {
describe("group chat - always mode", () => {
it("should react to all messages when group=always", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "always" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "always" } },
},
};
expect(
shouldSendReaction(cfg, {
@@ -105,7 +107,9 @@ describe("WhatsApp ack reaction logic", () => {
it("should react even with mention when group=always", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "always" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "always" } },
},
};
expect(
shouldSendReaction(cfg, {
@@ -120,7 +124,9 @@ describe("WhatsApp ack reaction logic", () => {
describe("group chat - mentions mode", () => {
it("should react when mentioned", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
},
};
expect(
shouldSendReaction(cfg, {
@@ -133,7 +139,9 @@ describe("WhatsApp ack reaction logic", () => {
it("should not react when not mentioned", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
},
};
expect(
shouldSendReaction(
@@ -150,7 +158,9 @@ describe("WhatsApp ack reaction logic", () => {
it("should react to all messages when group activation is always", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "mentions" } },
},
};
expect(
shouldSendReaction(
@@ -169,7 +179,9 @@ describe("WhatsApp ack reaction logic", () => {
describe("group chat - never mode", () => {
it("should not react even with mention", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "never" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "never" } },
},
};
expect(
shouldSendReaction(cfg, {
@@ -182,7 +194,9 @@ describe("WhatsApp ack reaction logic", () => {
it("should not react without mention", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀", group: "never" } },
channels: {
whatsapp: { ackReaction: { emoji: "👀", group: "never" } },
},
};
expect(
shouldSendReaction(cfg, {
@@ -197,8 +211,10 @@ describe("WhatsApp ack reaction logic", () => {
describe("combinations", () => {
it("direct=false, group=always: only groups", () => {
const cfg: ClawdbotConfig = {
whatsapp: {
ackReaction: { emoji: "✅", direct: false, group: "always" },
channels: {
whatsapp: {
ackReaction: { emoji: "✅", direct: false, group: "always" },
},
},
};
@@ -217,8 +233,10 @@ describe("WhatsApp ack reaction logic", () => {
it("direct=true, group=never: only direct", () => {
const cfg: ClawdbotConfig = {
whatsapp: {
ackReaction: { emoji: "🤖", direct: true, group: "never" },
channels: {
whatsapp: {
ackReaction: { emoji: "🤖", direct: true, group: "never" },
},
},
};
@@ -239,7 +257,7 @@ describe("WhatsApp ack reaction logic", () => {
describe("defaults", () => {
it("should default direct=true", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀" } },
channels: { whatsapp: { ackReaction: { emoji: "👀" } } },
};
expect(shouldSendReaction(cfg, { id: "m1", chatType: "direct" })).toBe(
true,
@@ -248,7 +266,7 @@ describe("WhatsApp ack reaction logic", () => {
it("should default group=mentions", () => {
const cfg: ClawdbotConfig = {
whatsapp: { ackReaction: { emoji: "👀" } },
channels: { whatsapp: { ackReaction: { emoji: "👀" } } },
};
expect(

View File

@@ -23,7 +23,7 @@ import type { ClawdbotConfig } from "../config/config.js";
import { resetLogger, setLoggerOverride } from "../logging.js";
import {
HEARTBEAT_TOKEN,
monitorWebProvider,
monitorWebChannel,
SILENT_REPLY_TOKEN,
} from "./auto-reply.js";
import {
@@ -115,14 +115,12 @@ describe("partial reply gating", () => {
const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" });
const mockConfig: ClawdbotConfig = {
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
};
setLoadConfigMock(mockConfig);
await monitorWebProvider(
await monitorWebChannel(
false,
async ({ onMessage }) => {
await onMessage({
@@ -210,15 +208,13 @@ describe("partial reply gating", () => {
const replyResolver = vi.fn().mockResolvedValue(undefined);
const mockConfig: ClawdbotConfig = {
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: store.storePath },
};
setLoadConfigMock(mockConfig);
await monitorWebProvider(
await monitorWebChannel(
false,
async ({ onMessage }) => {
await onMessage({
@@ -242,22 +238,19 @@ describe("partial reply gating", () => {
let stored: Record<
string,
{ lastProvider?: string; lastTo?: string }
{ lastChannel?: string; lastTo?: string }
> | null = null;
for (let attempt = 0; attempt < 50; attempt += 1) {
stored = JSON.parse(await fs.readFile(store.storePath, "utf8")) as Record<
string,
{ lastProvider?: string; lastTo?: string }
{ lastChannel?: string; lastTo?: string }
>;
if (
stored[mainSessionKey]?.lastProvider &&
stored[mainSessionKey]?.lastTo
)
if (stored[mainSessionKey]?.lastChannel && stored[mainSessionKey]?.lastTo)
break;
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (!stored) throw new Error("store not loaded");
expect(stored[mainSessionKey]?.lastProvider).toBe("whatsapp");
expect(stored[mainSessionKey]?.lastChannel).toBe("whatsapp");
expect(stored[mainSessionKey]?.lastTo).toBe("+1000");
resetLoadConfigMock();
@@ -274,15 +267,13 @@ describe("partial reply gating", () => {
const replyResolver = vi.fn().mockResolvedValue(undefined);
const mockConfig: ClawdbotConfig = {
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: store.storePath },
};
setLoadConfigMock(mockConfig);
await monitorWebProvider(
await monitorWebChannel(
false,
async ({ onMessage }) => {
await onMessage({
@@ -310,15 +301,15 @@ describe("partial reply gating", () => {
let stored: Record<
string,
{ lastProvider?: string; lastTo?: string; lastAccountId?: string }
{ lastChannel?: string; lastTo?: string; lastAccountId?: string }
> | null = null;
for (let attempt = 0; attempt < 50; attempt += 1) {
stored = JSON.parse(await fs.readFile(store.storePath, "utf8")) as Record<
string,
{ lastProvider?: string; lastTo?: string; lastAccountId?: string }
{ lastChannel?: string; lastTo?: string; lastAccountId?: string }
>;
if (
stored[groupSessionKey]?.lastProvider &&
stored[groupSessionKey]?.lastChannel &&
stored[groupSessionKey]?.lastTo &&
stored[groupSessionKey]?.lastAccountId
)
@@ -326,7 +317,7 @@ describe("partial reply gating", () => {
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (!stored) throw new Error("store not loaded");
expect(stored[groupSessionKey]?.lastProvider).toBe("whatsapp");
expect(stored[groupSessionKey]?.lastChannel).toBe("whatsapp");
expect(stored[groupSessionKey]?.lastTo).toBe("123@g.us");
expect(stored[groupSessionKey]?.lastAccountId).toBe("work");
@@ -394,14 +385,12 @@ describe("typing controller idle", () => {
});
const mockConfig: ClawdbotConfig = {
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
};
setLoadConfigMock(mockConfig);
await monitorWebProvider(
await monitorWebChannel(
false,
async ({ onMessage }) => {
await onMessage({
@@ -459,7 +448,7 @@ describe("web auto-reply", () => {
exit: vi.fn(),
};
const controller = new AbortController();
const run = monitorWebProvider(
const run = monitorWebChannel(
false,
listenerFactory,
true,
@@ -530,7 +519,7 @@ describe("web auto-reply", () => {
exit: vi.fn(),
};
const controller = new AbortController();
const run = monitorWebProvider(
const run = monitorWebChannel(
false,
listenerFactory,
true,
@@ -589,7 +578,7 @@ describe("web auto-reply", () => {
exit: vi.fn(),
};
const run = monitorWebProvider(
const run = monitorWebChannel(
false,
listenerFactory,
true,
@@ -653,7 +642,7 @@ describe("web auto-reply", () => {
session: { store: store.storePath },
}));
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
// Two messages from the same sender with fixed timestamps
@@ -742,7 +731,7 @@ describe("web auto-reply", () => {
status: 200,
} as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -791,7 +780,7 @@ describe("web auto-reply", () => {
headers: { get: () => "text/plain" },
} as unknown as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -858,7 +847,7 @@ describe("web auto-reply", () => {
status: 200,
} as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -960,7 +949,7 @@ describe("web auto-reply", () => {
status: 200,
} as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1034,7 +1023,7 @@ describe("web auto-reply", () => {
status: 200,
} as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1089,7 +1078,7 @@ describe("web auto-reply", () => {
status: 200,
} as Response);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1134,7 +1123,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1211,15 +1200,17 @@ describe("web auto-reply", () => {
);
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
accounts: {
default: { authDir },
channels: {
whatsapp: {
allowFrom: ["*"],
accounts: {
default: { authDir },
},
},
},
}));
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1292,15 +1283,17 @@ describe("web auto-reply", () => {
);
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
accounts: {
default: { authDir },
channels: {
whatsapp: {
allowFrom: ["*"],
accounts: {
default: { authDir },
},
},
},
}));
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1345,7 +1338,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1373,9 +1366,11 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
groups: { "*": { requireMention: true } },
channels: {
whatsapp: {
allowFrom: ["*"],
groups: { "*": { requireMention: true } },
},
},
messages: {
groupChat: { mentionPatterns: ["@global"] },
@@ -1411,7 +1406,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1456,9 +1451,11 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
groups: { "*": { requireMention: false } },
channels: {
whatsapp: {
allowFrom: ["*"],
groups: { "*": { requireMention: false } },
},
},
messages: { groupChat: { mentionPatterns: ["@clawd"] } },
}));
@@ -1475,7 +1472,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1505,9 +1502,11 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
groups: { "999@g.us": { requireMention: false } },
channels: {
whatsapp: {
allowFrom: ["*"],
groups: { "999@g.us": { requireMention: false } },
},
},
messages: { groupChat: { mentionPatterns: ["@clawd"] } },
}));
@@ -1524,7 +1523,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1556,11 +1555,13 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
groups: {
"*": { requireMention: true },
"123@g.us": { requireMention: false },
channels: {
whatsapp: {
allowFrom: ["*"],
groups: {
"*": { requireMention: true },
"123@g.us": { requireMention: false },
},
},
},
messages: { groupChat: { mentionPatterns: ["@clawd"] } },
@@ -1578,7 +1579,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1637,7 +1638,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1694,10 +1695,12 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({
whatsapp: {
// Self-chat heuristic: allowFrom includes selfE164.
allowFrom: ["+999"],
groups: { "*": { requireMention: true } },
channels: {
whatsapp: {
// Self-chat heuristic: allowFrom includes selfE164.
allowFrom: ["+999"],
groups: { "*": { requireMention: true } },
},
},
messages: {
groupChat: {
@@ -1718,7 +1721,7 @@ describe("web auto-reply", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
// WhatsApp @mention of the owner should NOT trigger the bot in self-chat mode.
@@ -1784,7 +1787,7 @@ describe("web auto-reply", () => {
return { close: vi.fn(), onClose };
});
const run = monitorWebProvider(
const run = monitorWebChannel(
false,
listenerFactory,
true,
@@ -1825,7 +1828,7 @@ describe("web auto-reply", () => {
};
const resolver = vi.fn().mockResolvedValue({ text: "auto" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1846,9 +1849,7 @@ describe("web auto-reply", () => {
it("prefixes body with same-phone marker when from === to", async () => {
// Enable messagePrefix for same-phone mode testing
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: "[same-phone]",
responsePrefix: undefined,
@@ -1869,7 +1870,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "reply" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1905,7 +1906,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "reply" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1939,7 +1940,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "reply" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -1970,9 +1971,7 @@ describe("web auto-reply", () => {
it("applies responsePrefix to regular replies", async () => {
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: "🦞",
@@ -1994,7 +1993,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "hello there" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2014,9 +2013,7 @@ describe("web auto-reply", () => {
it("does not deliver HEARTBEAT_OK responses", async () => {
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: "🦞",
@@ -2039,7 +2036,7 @@ describe("web auto-reply", () => {
// Resolver returns exact HEARTBEAT_OK
const resolver = vi.fn().mockResolvedValue({ text: HEARTBEAT_TOKEN });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2058,9 +2055,7 @@ describe("web auto-reply", () => {
it("does not double-prefix if responsePrefix already present", async () => {
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: "🦞",
@@ -2083,7 +2078,7 @@ describe("web auto-reply", () => {
// Resolver returns text that already has prefix
const resolver = vi.fn().mockResolvedValue({ text: "🦞 already prefixed" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2103,9 +2098,7 @@ describe("web auto-reply", () => {
it("sends tool summaries immediately with responsePrefix", async () => {
setLoadConfigMock(() => ({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: "🦞",
@@ -2138,7 +2131,7 @@ describe("web auto-reply", () => {
},
);
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2175,7 +2168,7 @@ describe("web auto-reply", () => {
{
agentId: "rich",
match: {
provider: "whatsapp",
channel: "whatsapp",
peer: { kind: "dm", id: "+1555" },
},
},
@@ -2197,7 +2190,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "hello" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2237,7 +2230,7 @@ describe("web auto-reply", () => {
{
agentId: "rich",
match: {
provider: "whatsapp",
channel: "whatsapp",
peer: { kind: "dm", id: "+1555" },
},
},
@@ -2259,7 +2252,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "hello there" });
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2281,7 +2274,7 @@ describe("web auto-reply", () => {
describe("broadcast groups", () => {
it("broadcasts sequentially in configured order", async () => {
setLoadConfigMock({
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
@@ -2313,7 +2306,7 @@ describe("broadcast groups", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2338,7 +2331,7 @@ describe("broadcast groups", () => {
it("shares group history across broadcast agents and clears after replying", async () => {
setLoadConfigMock({
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
@@ -2366,7 +2359,7 @@ describe("broadcast groups", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2445,7 +2438,7 @@ describe("broadcast groups", () => {
it("broadcasts in parallel by default", async () => {
setLoadConfigMock({
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
@@ -2488,7 +2481,7 @@ describe("broadcast groups", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
@@ -2511,7 +2504,7 @@ describe("broadcast groups", () => {
it("skips unknown broadcast agent ids when agents.list is present", async () => {
setLoadConfigMock({
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }],
@@ -2542,7 +2535,7 @@ describe("broadcast groups", () => {
return { close: vi.fn() };
};
await monitorWebProvider(false, listenerFactory, false, resolver);
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({

View File

@@ -29,11 +29,13 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/pr
import { getReplyFromConfig } from "../auto-reply/reply.js";
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import { toLocationContext } from "../channels/location.js";
import { resolveWhatsAppHeartbeatRecipients } from "../channels/plugins/whatsapp-heartbeat.js";
import { waitForever } from "../cli/wait.js";
import { loadConfig } from "../config/config.js";
import {
resolveProviderGroupPolicy,
resolveProviderGroupRequireMention,
resolveChannelGroupPolicy,
resolveChannelGroupRequireMention,
} from "../config/group-policy.js";
import {
DEFAULT_IDLE_MINUTES,
@@ -50,8 +52,6 @@ import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { toLocationContext } from "../providers/location.js";
import { resolveWhatsAppHeartbeatRecipients } from "../providers/plugins/whatsapp-heartbeat.js";
import {
buildAgentSessionKey,
resolveAgentRoute,
@@ -80,7 +80,7 @@ import {
} from "./reconnect.js";
import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp");
const whatsappInboundLog = whatsappLog.child("inbound");
const whatsappOutboundLog = whatsappLog.child("outbound");
const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
@@ -145,14 +145,14 @@ export type WebMonitorTuning = {
reconnect?: Partial<ReconnectPolicy>;
heartbeatSeconds?: number;
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
statusSink?: (status: WebProviderStatus) => void;
statusSink?: (status: WebChannelStatus) => void;
/** WhatsApp account id. Default: "default". */
accountId?: string;
};
export { HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN };
export type WebProviderStatus = {
export type WebChannelStatus = {
running: boolean;
connected: boolean;
reconnectAttempts: number;
@@ -190,7 +190,7 @@ function buildMentionConfig(
agentId?: string,
): MentionConfig {
const mentionRegexes = buildMentionRegexes(cfg, agentId);
return { mentionRegexes, allowFrom: cfg.whatsapp?.allowFrom };
return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
}
function resolveMentionTargets(
@@ -726,7 +726,7 @@ async function deliverWebReply(params: {
}
}
export async function monitorWebProvider(
export async function monitorWebChannel(
verbose: boolean,
listenerFactory: typeof monitorWebInbox | undefined = monitorWebInbox,
keepAlive = true,
@@ -739,7 +739,7 @@ export async function monitorWebProvider(
const replyLogger = getChildLogger({ module: "web-auto-reply", runId });
const heartbeatLogger = getChildLogger({ module: "web-heartbeat", runId });
const reconnectLogger = getChildLogger({ module: "web-reconnect", runId });
const status: WebProviderStatus = {
const status: WebChannelStatus = {
running: true,
connected: false,
reconnectAttempts: 0,
@@ -765,17 +765,20 @@ export async function monitorWebProvider(
});
const cfg = {
...baseCfg,
whatsapp: {
...baseCfg.whatsapp,
ackReaction: account.ackReaction,
messagePrefix: account.messagePrefix,
allowFrom: account.allowFrom,
groupAllowFrom: account.groupAllowFrom,
groupPolicy: account.groupPolicy,
textChunkLimit: account.textChunkLimit,
mediaMaxMb: account.mediaMaxMb,
blockStreaming: account.blockStreaming,
groups: account.groups,
channels: {
...baseCfg.channels,
whatsapp: {
...baseCfg.channels?.whatsapp,
ackReaction: account.ackReaction,
messagePrefix: account.messagePrefix,
allowFrom: account.allowFrom,
groupAllowFrom: account.groupAllowFrom,
groupPolicy: account.groupPolicy,
textChunkLimit: account.textChunkLimit,
mediaMaxMb: account.mediaMaxMb,
blockStreaming: account.blockStreaming,
groups: account.groups,
},
},
} satisfies ReturnType<typeof loadConfig>;
const configuredMaxMb = cfg.agents?.defaults?.mediaMaxMb;
@@ -792,8 +795,8 @@ export async function monitorWebProvider(
buildMentionConfig(cfg, agentId);
const baseMentionConfig = resolveMentionConfig();
const groupHistoryLimit =
cfg.whatsapp?.accounts?.[tuning.accountId ?? ""]?.historyLimit ??
cfg.whatsapp?.historyLimit ??
cfg.channels?.whatsapp?.accounts?.[tuning.accountId ?? ""]?.historyLimit ??
cfg.channels?.whatsapp?.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT;
const groupHistories = new Map<
@@ -884,9 +887,9 @@ export async function monitorWebProvider(
const resolveGroupPolicyFor = (conversationId: string) => {
const groupId =
resolveGroupResolution(conversationId)?.id ?? conversationId;
return resolveProviderGroupPolicy({
return resolveChannelGroupPolicy({
cfg,
provider: "whatsapp",
channel: "whatsapp",
groupId,
});
};
@@ -894,9 +897,9 @@ export async function monitorWebProvider(
const resolveGroupRequireMentionFor = (conversationId: string) => {
const groupId =
resolveGroupResolution(conversationId)?.id ?? conversationId;
return resolveProviderGroupRequireMention({
return resolveChannelGroupRequireMention({
cfg,
provider: "whatsapp",
channel: "whatsapp",
groupId,
});
};
@@ -1046,10 +1049,10 @@ export async function monitorWebProvider(
};
const buildLine = (msg: WebInboundMsg, agentId: string) => {
// WhatsApp inbound prefix: whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults
// WhatsApp inbound prefix: channels.whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults
const messagePrefix = resolveMessagePrefix(cfg, agentId, {
configured: cfg.whatsapp?.messagePrefix,
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
configured: cfg.channels?.whatsapp?.messagePrefix,
hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0,
});
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
const senderLabel =
@@ -1063,7 +1066,7 @@ export async function monitorWebProvider(
// Wrap with standardized envelope for the agent.
return formatAgentEnvelope({
provider: "WhatsApp",
channel: "WhatsApp",
from:
msg.chatType === "group"
? msg.from
@@ -1108,7 +1111,7 @@ export async function monitorWebProvider(
? `${m.body}\n[message_id: ${m.id}]`
: m.body;
return formatAgentEnvelope({
provider: "WhatsApp",
channel: "WhatsApp",
from: conversationId,
timestamp: m.timestamp,
body: `${m.sender}: ${bodyWithId}`,
@@ -1143,7 +1146,7 @@ export async function monitorWebProvider(
// Send ack reaction immediately upon message receipt (post-gating)
if (msg.id) {
const ackConfig = cfg.whatsapp?.ackReaction;
const ackConfig = cfg.channels?.whatsapp?.ackReaction;
const emoji = (ackConfig?.emoji ?? "").trim();
const directEnabled = ackConfig?.direct ?? true;
const groupMode = ackConfig?.group ?? "mentions";
@@ -1239,7 +1242,7 @@ export async function monitorWebProvider(
const task = updateLastRoute({
storePath,
sessionKey: route.mainSessionKey,
provider: "whatsapp",
channel: "whatsapp",
to,
accountId: route.accountId,
}).catch((err) => {
@@ -1368,8 +1371,8 @@ export async function monitorWebProvider(
},
replyOptions: {
disableBlockStreaming:
typeof cfg.whatsapp?.blockStreaming === "boolean"
? !cfg.whatsapp.blockStreaming
typeof cfg.channels?.whatsapp?.blockStreaming === "boolean"
? !cfg.channels.whatsapp.blockStreaming
: undefined,
},
});
@@ -1428,7 +1431,7 @@ export async function monitorWebProvider(
agentId: normalizedAgentId,
sessionKey: buildAgentSessionKey({
agentId: normalizedAgentId,
provider: "whatsapp",
channel: "whatsapp",
peer: {
kind: msg.chatType === "group" ? "group" : "dm",
id: peerId,
@@ -1501,7 +1504,7 @@ export async function monitorWebProvider(
})();
const route = resolveAgentRoute({
cfg,
provider: "whatsapp",
channel: "whatsapp",
accountId: msg.accountId,
peer: {
kind: msg.chatType === "group" ? "group" : "dm",
@@ -1511,7 +1514,7 @@ export async function monitorWebProvider(
const groupHistoryKey =
msg.chatType === "group"
? buildGroupHistoryKey({
provider: "whatsapp",
channel: "whatsapp",
accountId: route.accountId,
peerKind: "group",
peerId,
@@ -1550,7 +1553,7 @@ export async function monitorWebProvider(
const task = updateLastRoute({
storePath,
sessionKey: route.sessionKey,
provider: "whatsapp",
channel: "whatsapp",
to: conversationId,
accountId: route.accountId,
}).catch((err) => {
@@ -1665,7 +1668,7 @@ export async function monitorWebProvider(
const { e164: selfE164 } = readWebSelfId(account.authDir);
const connectRoute = resolveAgentRoute({
cfg,
provider: "whatsapp",
channel: "whatsapp",
accountId: account.accountId,
});
enqueueSystemEvent(

View File

@@ -24,8 +24,10 @@ vi.mock("../config/config.js", async (importOriginal) => {
return {
...actual,
loadConfig: vi.fn().mockReturnValue({
whatsapp: {
allowFrom: ["*"], // Allow all in tests
channels: {
whatsapp: {
allowFrom: ["*"], // Allow all in tests
},
},
messages: {
messagePrefix: undefined,
@@ -36,9 +38,9 @@ vi.mock("../config/config.js", async (importOriginal) => {
});
vi.mock("../pairing/pairing-store.js", () => ({
readProviderAllowFromStore: (...args: unknown[]) =>
readChannelAllowFromStore: (...args: unknown[]) =>
readAllowFromStoreMock(...args),
upsertProviderPairingRequest: (...args: unknown[]) =>
upsertChannelPairingRequest: (...args: unknown[]) =>
upsertPairingRequestMock(...args),
}));

View File

@@ -11,22 +11,21 @@ import {
isJidGroup,
normalizeMessageContent,
} from "@whiskeysockets/baileys";
import {
formatLocationText,
type NormalizedLocation,
} from "../channels/location.js";
import { loadConfig } from "../config/config.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { createDedupeCache } from "../infra/dedupe.js";
import { recordProviderActivity } from "../infra/provider-activity.js";
import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { saveMediaBuffer } from "../media/store.js";
import { buildPairingReply } from "../pairing/pairing-messages.js";
import {
readProviderAllowFromStore,
upsertProviderPairingRequest,
readChannelAllowFromStore,
upsertChannelPairingRequest,
} from "../pairing/pairing-store.js";
import {
formatLocationText,
type NormalizedLocation,
} from "../providers/location.js";
import {
isSelfChatMode,
jidToE164,
@@ -101,7 +100,7 @@ export async function monitorWebInbox(options: {
}) {
const inboundLogger = getChildLogger({ module: "web-inbound" });
const inboundConsoleLog = createSubsystemLogger(
"gateway/providers/whatsapp",
"gateway/channels/whatsapp",
).child("inbound");
const sock = await createWaSocket(false, options.verbose, {
authDir: options.authDir,
@@ -174,8 +173,8 @@ export async function monitorWebInbox(options: {
}) => {
if (upsert.type !== "notify" && upsert.type !== "append") return;
for (const msg of upsert.messages ?? []) {
recordProviderActivity({
provider: "whatsapp",
recordChannelActivity({
channel: "whatsapp",
accountId: options.accountId,
direction: "inbound",
});
@@ -215,9 +214,9 @@ export async function monitorWebInbox(options: {
cfg,
accountId: options.accountId,
});
const dmPolicy = cfg.whatsapp?.dmPolicy ?? "pairing";
const dmPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing";
const configuredAllowFrom = account.allowFrom;
const storeAllowFrom = await readProviderAllowFromStore("whatsapp").catch(
const storeAllowFrom = await readChannelAllowFromStore("whatsapp").catch(
() => [],
);
// Without user config, default to self-only DM access so the owner can talk to themselves
@@ -296,8 +295,8 @@ export async function monitorWebInbox(options: {
normalizedAllowFrom.includes(candidate));
if (!allowed) {
if (dmPolicy === "pairing") {
const { code, created } = await upsertProviderPairingRequest({
provider: "whatsapp",
const { code, created } = await upsertChannelPairingRequest({
channel: "whatsapp",
id: candidate,
meta: {
name: (msg.pushName ?? "").trim() || undefined,
@@ -310,7 +309,7 @@ export async function monitorWebInbox(options: {
try {
await sock.sendMessage(remoteJid, {
text: buildPairingReply({
provider: "whatsapp",
channel: "whatsapp",
idLine: `Your WhatsApp phone number: ${candidate}`,
code,
}),
@@ -583,8 +582,8 @@ export async function monitorWebInbox(options: {
}
const result = await sock.sendMessage(jid, payload);
const accountId = sendOptions?.accountId ?? options.accountId;
recordProviderActivity({
provider: "whatsapp",
recordChannelActivity({
channel: "whatsapp",
accountId,
direction: "outbound",
});
@@ -606,8 +605,8 @@ export async function monitorWebInbox(options: {
selectableCount: poll.maxSelections ?? 1,
},
});
recordProviderActivity({
provider: "whatsapp",
recordChannelActivity({
channel: "whatsapp",
accountId: options.accountId,
direction: "outbound",
});

View File

@@ -14,9 +14,11 @@ const authDir = path.join(os.tmpdir(), "wa-creds");
vi.mock("../config/config.js", () => ({
loadConfig: () =>
({
whatsapp: {
accounts: {
default: { enabled: true, authDir },
channels: {
whatsapp: {
accounts: {
default: { enabled: true, authDir },
},
},
},
}) as never,

View File

@@ -10,8 +10,11 @@ vi.mock("../media/store.js", () => ({
}));
const mockLoadConfig = vi.fn().mockReturnValue({
whatsapp: {
allowFrom: ["*"], // Allow all in tests by default
channels: {
whatsapp: {
// Allow all in tests by default
allowFrom: ["*"],
},
},
messages: {
messagePrefix: undefined,
@@ -33,9 +36,9 @@ vi.mock("../config/config.js", async (importOriginal) => {
});
vi.mock("../pairing/pairing-store.js", () => ({
readProviderAllowFromStore: (...args: unknown[]) =>
readChannelAllowFromStore: (...args: unknown[]) =>
readAllowFromStoreMock(...args),
upsertProviderPairingRequest: (...args: unknown[]) =>
upsertChannelPairingRequest: (...args: unknown[]) =>
upsertPairingRequestMock(...args),
}));
@@ -680,9 +683,12 @@ describe("web monitor inbox", () => {
it("still forwards group messages (with sender info) even when allowFrom is restrictive", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+111"], // does not include +777
groupPolicy: "open",
channels: {
whatsapp: {
// does not include +777
allowFrom: ["+111"],
groupPolicy: "open",
},
},
messages: {
messagePrefix: undefined,
@@ -736,8 +742,11 @@ describe("web monitor inbox", () => {
// Test for auto-recovery fix: early allowFrom filtering prevents Bad MAC errors
// from unauthorized senders corrupting sessions
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+111"], // Only allow +111
channels: {
whatsapp: {
// Only allow +111
allowFrom: ["+111"],
},
},
messages: {
messagePrefix: undefined,
@@ -782,9 +791,7 @@ describe("web monitor inbox", () => {
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -796,9 +803,11 @@ describe("web monitor inbox", () => {
it("skips read receipts in self-chat mode", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
// Self-chat heuristic: allowFrom includes selfE164 (+123).
allowFrom: ["+123"],
channels: {
whatsapp: {
// Self-chat heuristic: allowFrom includes selfE164 (+123).
allowFrom: ["+123"],
},
},
messages: {
messagePrefix: undefined,
@@ -832,9 +841,7 @@ describe("web monitor inbox", () => {
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -846,10 +853,7 @@ describe("web monitor inbox", () => {
it("lets group messages through even when sender not in allowFrom", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+1234"],
groupPolicy: "open",
},
channels: { whatsapp: { allowFrom: ["+1234"], groupPolicy: "open" } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -888,10 +892,7 @@ describe("web monitor inbox", () => {
it("blocks all group messages when groupPolicy is 'disabled'", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+1234"],
groupPolicy: "disabled",
},
channels: { whatsapp: { allowFrom: ["+1234"], groupPolicy: "disabled" } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -929,9 +930,11 @@ describe("web monitor inbox", () => {
it("blocks group messages from senders not in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
groupAllowFrom: ["+1234"], // Does not include +999
groupPolicy: "allowlist",
channels: {
whatsapp: {
groupAllowFrom: ["+1234"], // Does not include +999
groupPolicy: "allowlist",
},
},
messages: {
messagePrefix: undefined,
@@ -970,9 +973,11 @@ describe("web monitor inbox", () => {
it("allows group messages from senders in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
groupAllowFrom: ["+15551234567"], // Includes the sender
groupPolicy: "allowlist",
channels: {
whatsapp: {
groupAllowFrom: ["+15551234567"], // Includes the sender
groupPolicy: "allowlist",
},
},
messages: {
messagePrefix: undefined,
@@ -1014,9 +1019,11 @@ describe("web monitor inbox", () => {
it("allows all group senders with wildcard in groupPolicy allowlist", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
groupAllowFrom: ["*"], // Wildcard allows everyone
groupPolicy: "allowlist",
channels: {
whatsapp: {
groupAllowFrom: ["*"], // Wildcard allows everyone
groupPolicy: "allowlist",
},
},
messages: {
messagePrefix: undefined,
@@ -1057,8 +1064,10 @@ describe("web monitor inbox", () => {
it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
groupPolicy: "allowlist",
channels: {
whatsapp: {
groupPolicy: "allowlist",
},
},
messages: {
messagePrefix: undefined,
@@ -1096,8 +1105,11 @@ describe("web monitor inbox", () => {
it("allows messages from senders in allowFrom list", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+111", "+999"], // Allow +999
channels: {
whatsapp: {
// Allow +999
allowFrom: ["+111", "+999"],
},
},
messages: {
messagePrefix: undefined,
@@ -1134,9 +1146,7 @@ describe("web monitor inbox", () => {
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -1150,8 +1160,11 @@ describe("web monitor inbox", () => {
// Same-phone mode: when from === selfJid, should always be allowed
// This allows users to message themselves even with restrictive allowFrom
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+111"], // Only allow +111, but self is +123
channels: {
whatsapp: {
// Only allow +111, but self is +123
allowFrom: ["+111"],
},
},
messages: {
messagePrefix: undefined,
@@ -1185,9 +1198,7 @@ describe("web monitor inbox", () => {
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -1285,9 +1296,7 @@ describe("web monitor inbox", () => {
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -1299,9 +1308,11 @@ describe("web monitor inbox", () => {
it("skips pairing replies for outbound DMs in same-phone mode", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
dmPolicy: "pairing",
selfChatMode: true,
channels: {
whatsapp: {
dmPolicy: "pairing",
selfChatMode: true,
},
},
messages: {
messagePrefix: undefined,
@@ -1336,9 +1347,7 @@ describe("web monitor inbox", () => {
expect(sock.sendMessage).not.toHaveBeenCalled();
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
@@ -1350,9 +1359,11 @@ describe("web monitor inbox", () => {
it("skips pairing replies for outbound DMs when same-phone mode is disabled", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
dmPolicy: "pairing",
selfChatMode: false,
channels: {
whatsapp: {
dmPolicy: "pairing",
selfChatMode: false,
},
},
messages: {
messagePrefix: undefined,
@@ -1387,9 +1398,7 @@ describe("web monitor inbox", () => {
expect(sock.sendMessage).not.toHaveBeenCalled();
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"],
},
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,

View File

@@ -9,7 +9,7 @@ import {
} from "./active-listener.js";
import { loadWebMedia } from "./media.js";
const outboundLog = createSubsystemLogger("gateway/providers/whatsapp").child(
const outboundLog = createSubsystemLogger("gateway/channels/whatsapp").child(
"outbound",
);

View File

@@ -89,9 +89,7 @@ describe("web session", () => {
logWebSelfId("/tmp/wa-creds", runtime as never, true);
expect(runtime.log).toHaveBeenCalledWith(
expect.stringContaining(
"Web Provider: +12345 (jid 12345@s.whatsapp.net)",
),
expect.stringContaining("Web Channel: +12345 (jid 12345@s.whatsapp.net)"),
);
existsSpy.mockRestore();
readSpy.mockRestore();

View File

@@ -24,7 +24,7 @@ export {
getWebAuthAgeMs,
logoutWeb,
logWebSelfId,
pickProvider,
pickWebChannel,
readWebSelfId,
WA_WEB_AUTH_DIR,
webAuthExists,

View File

@@ -6,9 +6,11 @@ import { createMockBaileys } from "../../test/mocks/baileys.js";
// Use globalThis to store the mock config so it survives vi.mock hoisting
const CONFIG_KEY = Symbol.for("clawdbot:testConfigMock");
const DEFAULT_CONFIG = {
whatsapp: {
// Tests can override; default remains open to avoid surprising fixtures
allowFrom: ["*"],
channels: {
whatsapp: {
// Tests can override; default remains open to avoid surprising fixtures
allowFrom: ["*"],
},
},
messages: {
messagePrefix: undefined,