web: default to self-only without config

This commit is contained in:
Peter Steinberger
2025-12-12 00:50:40 +00:00
parent 0242383ec3
commit f1ff24d634
6 changed files with 119 additions and 10 deletions

View File

@@ -473,10 +473,19 @@ export async function getReplyFromConfig(
} }
// Optional allowlist by origin number (E.164 without whatsapp: prefix) // Optional allowlist by origin number (E.164 without whatsapp: prefix)
const allowFrom = cfg.inbound?.allowFrom; const configuredAllowFrom = cfg.inbound?.allowFrom;
const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
const isSamePhone = from && to && from === to; const isSamePhone = from && to && from === to;
// If no config is present, default to self-only DM access.
const defaultAllowFrom =
(!configuredAllowFrom || configuredAllowFrom.length === 0) && to
? [to]
: undefined;
const allowFrom =
configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: defaultAllowFrom;
const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined); const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined);
const rawBodyNormalized = triggerBodyNormalized; const rawBodyNormalized = triggerBodyNormalized;

View File

@@ -18,6 +18,7 @@ import {
runWebHeartbeatOnce, runWebHeartbeatOnce,
stripHeartbeatToken, stripHeartbeatToken,
} from "./auto-reply.js"; } from "./auto-reply.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { sendMessageWhatsApp } from "./outbound.js"; import type { sendMessageWhatsApp } from "./outbound.js";
import { import {
resetBaileysMocks, resetBaileysMocks,
@@ -201,6 +202,39 @@ describe("partial reply gating", () => {
expect(reply).toHaveBeenCalledTimes(1); expect(reply).toHaveBeenCalledTimes(1);
expect(reply).toHaveBeenCalledWith("final reply"); expect(reply).toHaveBeenCalledWith("final reply");
}); });
it("defaults to self-only when no config is present", async () => {
const cfg: ClawdisConfig = {
inbound: {
// No allowFrom provided; this simulates zero config file while keeping reply simple
reply: { mode: "text", text: "ok" },
},
};
// Not self: should be blocked
const blocked = await getReplyFromConfig(
{
Body: "hi",
From: "whatsapp:+999",
To: "whatsapp:+123",
},
undefined,
cfg,
);
expect(blocked).toBeUndefined();
// Self: should be allowed
const allowed = await getReplyFromConfig(
{
Body: "hi",
From: "whatsapp:+123",
To: "whatsapp:+123",
},
undefined,
cfg,
);
expect(allowed).toEqual({ text: "ok" });
});
}); });
describe("runWebHeartbeatOnce", () => { describe("runWebHeartbeatOnce", () => {
@@ -531,7 +565,7 @@ describe("web auto-reply", () => {
await run; await run;
}); });
it("stops after hitting max reconnect attempts", async () => { it("stops after hitting max reconnect attempts", { timeout: 20000 }, async () => {
const closeResolvers: Array<() => void> = []; const closeResolvers: Array<() => void> = [];
const sleep = vi.fn(async () => {}); const sleep = vi.fn(async () => {});
const listenerFactory = vi.fn(async () => { const listenerFactory = vi.fn(async () => {
@@ -570,7 +604,7 @@ describe("web auto-reply", () => {
await run; await run;
expect(runtime.error).toHaveBeenCalledWith( expect(runtime.error).toHaveBeenCalledWith(
expect.stringContaining("Reached max retries"), expect.stringContaining("max attempts reached"),
); );
}); });

View File

@@ -1383,7 +1383,7 @@ export async function monitorWebProvider(
}, },
"web reconnect: max attempts reached; continuing in degraded mode", "web reconnect: max attempts reached; continuing in degraded mode",
); );
reconnectAttempts = 0; break;
} }
const delay = computeBackoff(reconnectPolicy, reconnectAttempts); const delay = computeBackoff(reconnectPolicy, reconnectAttempts);

View File

@@ -149,7 +149,16 @@ export async function monitorWebInbox(options: {
// Filter unauthorized senders early to prevent wasted processing // Filter unauthorized senders early to prevent wasted processing
// and potential session corruption from Bad MAC errors // and potential session corruption from Bad MAC errors
const cfg = loadConfig(); const cfg = loadConfig();
const allowFrom = cfg.inbound?.allowFrom; const configuredAllowFrom = cfg.inbound?.allowFrom;
// Without user config, default to self-only DM access so the owner can talk to themselves
const defaultAllowFrom =
(!configuredAllowFrom || configuredAllowFrom.length === 0) && selfE164
? [selfE164]
: undefined;
const allowFrom =
configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: defaultAllowFrom;
const isSamePhone = from === selfE164; const isSamePhone = from === selfE164;
const allowlistEnabled = const allowlistEnabled =

View File

@@ -11,7 +11,7 @@ vi.mock("../media/store.js", () => ({
const mockLoadConfig = vi.fn().mockReturnValue({ const mockLoadConfig = vi.fn().mockReturnValue({
inbound: { inbound: {
allowFrom: ["*"], // Allow all in tests allowFrom: ["*"], // Allow all in tests by default
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -553,3 +553,59 @@ describe("web monitor inbox", () => {
await listener.close(); await listener.close();
}); });
}); });
it("defaults to self-only when no config is present", async () => {
// No config file => allowFrom should be derived from selfE164
mockLoadConfig.mockReturnValue({});
const onMessage = vi.fn();
const listener = await monitorWebInbox({ verbose: false, onMessage });
const sock = await createWaSocket();
// Message from someone else should be blocked
const upsertBlocked = {
type: "notify",
messages: [
{
key: { id: "no-config-1", fromMe: false, remoteJid: "999@s.whatsapp.net" },
message: { conversation: "ping" },
messageTimestamp: 1_700_000_000,
},
],
};
sock.ev.emit("messages.upsert", upsertBlocked);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).not.toHaveBeenCalled();
// Message from self should be allowed
const upsertSelf = {
type: "notify",
messages: [
{
key: { id: "no-config-2", fromMe: false, remoteJid: "123@s.whatsapp.net" },
message: { conversation: "self ping" },
messageTimestamp: 1_700_000_001,
},
],
};
sock.ev.emit("messages.upsert", upsertSelf);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).toHaveBeenCalledTimes(1);
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({ body: "self ping", from: "+123", to: "+123" }),
);
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
inbound: {
allowFrom: ["*"],
messagePrefix: undefined,
responsePrefix: undefined,
timestampPrefix: false,
},
});
await listener.close();
});

View File

@@ -7,10 +7,11 @@ import { createMockBaileys } from "../../test/mocks/baileys.js";
const CONFIG_KEY = Symbol.for("clawdis:testConfigMock"); const CONFIG_KEY = Symbol.for("clawdis:testConfigMock");
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
inbound: { inbound: {
allowFrom: ["*"], // Allow all in tests by default // Tests can override; default remains open to avoid surprising fixtures
messagePrefix: undefined, // No message prefix in tests allowFrom: ["*"],
responsePrefix: undefined, // No response prefix in tests messagePrefix: undefined,
timestampPrefix: false, // No timestamp in tests responsePrefix: undefined,
timestampPrefix: false,
}, },
}; };