refactor: unify group allowlist policy
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
- Docs: clarify Slack manifest scopes (current vs optional) with references. Thanks @jarvis-medmatic for PR #235.
|
- Docs: clarify Slack manifest scopes (current vs optional) with references. Thanks @jarvis-medmatic for PR #235.
|
||||||
- Control UI: avoid Slack config ReferenceError by reading slack config snapshots. Thanks @sreekaransrinath for PR #249.
|
- Control UI: avoid Slack config ReferenceError by reading slack config snapshots. Thanks @sreekaransrinath for PR #249.
|
||||||
- Telegram: honor routing.groupChat.mentionPatterns for group mention gating. Thanks @regenrek for PR #242.
|
- Telegram: honor routing.groupChat.mentionPatterns for group mention gating. Thanks @regenrek for PR #242.
|
||||||
|
- Telegram: gate groups via `telegram.groups` allowlist (align with WhatsApp/iMessage). Thanks @kitze for PR #241.
|
||||||
- Auto-reply: block unauthorized `/reset` and infer WhatsApp senders from E.164 inputs.
|
- Auto-reply: block unauthorized `/reset` and infer WhatsApp senders from E.164 inputs.
|
||||||
- Auto-reply: track compaction count in session status; verbose mode announces auto-compactions.
|
- Auto-reply: track compaction count in session status; verbose mode announces auto-compactions.
|
||||||
- Telegram: send GIF media as animations (auto-play) and improve filename sniffing.
|
- Telegram: send GIF media as animations (auto-play) and improve filename sniffing.
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
- Skills: add CodexBar model usage helper with macOS requirement metadata.
|
- Skills: add CodexBar model usage helper with macOS requirement metadata.
|
||||||
- Skills: add 1Password CLI skill with op examples.
|
- Skills: add 1Password CLI skill with op examples.
|
||||||
- Lint: organize imports and wrap long lines in reply commands.
|
- Lint: organize imports and wrap long lines in reply commands.
|
||||||
|
- Refactor: centralize group allowlist/mention policy across providers.
|
||||||
- Deps: update to latest across the repo.
|
- Deps: update to latest across the repo.
|
||||||
|
|
||||||
## 2026.1.5-3
|
## 2026.1.5-3
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ Notes:
|
|||||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||||
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
|
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
|
||||||
|
|
||||||
|
## Group allowlists
|
||||||
|
When `whatsapp.groups`, `telegram.groups`, or `imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||||
|
|
||||||
## Activation (owner-only)
|
## Activation (owner-only)
|
||||||
Group owners can toggle per-group activation:
|
Group owners can toggle per-group activation:
|
||||||
- `/activation mention`
|
- `/activation mention`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { resolveProviderGroupRequireMention } from "../../config/group-policy.js";
|
||||||
import type {
|
import type {
|
||||||
GroupKeyResolution,
|
GroupKeyResolution,
|
||||||
SessionEntry,
|
SessionEntry,
|
||||||
@@ -53,38 +54,16 @@ export function resolveGroupRequireMention(params: {
|
|||||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||||
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
||||||
const groupSpace = ctx.GroupSpace?.trim();
|
const groupSpace = ctx.GroupSpace?.trim();
|
||||||
if (surface === "telegram") {
|
if (
|
||||||
if (groupId) {
|
surface === "telegram" ||
|
||||||
const groupConfig = cfg.telegram?.groups?.[groupId];
|
surface === "whatsapp" ||
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
surface === "imessage"
|
||||||
return groupConfig.requireMention;
|
) {
|
||||||
}
|
return resolveProviderGroupRequireMention({
|
||||||
}
|
cfg,
|
||||||
const groupDefault = cfg.telegram?.groups?.["*"]?.requireMention;
|
surface,
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
groupId,
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
if (surface === "whatsapp") {
|
|
||||||
if (groupId) {
|
|
||||||
const groupConfig = cfg.whatsapp?.groups?.[groupId];
|
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
|
||||||
return groupConfig.requireMention;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupDefault = cfg.whatsapp?.groups?.["*"]?.requireMention;
|
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (surface === "imessage") {
|
|
||||||
if (groupId) {
|
|
||||||
const groupConfig = cfg.imessage?.groups?.[groupId];
|
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
|
||||||
return groupConfig.requireMention;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupDefault = cfg.imessage?.groups?.["*"]?.requireMention;
|
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (surface === "discord") {
|
if (surface === "discord") {
|
||||||
const guildEntry = resolveDiscordGuildEntry(
|
const guildEntry = resolveDiscordGuildEntry(
|
||||||
|
|||||||
85
src/config/group-policy.ts
Normal file
85
src/config/group-policy.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { ClawdbotConfig } from "./config.js";
|
||||||
|
|
||||||
|
export type GroupPolicySurface = "whatsapp" | "telegram" | "imessage";
|
||||||
|
|
||||||
|
export type ProviderGroupConfig = {
|
||||||
|
requireMention?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderGroupPolicy = {
|
||||||
|
allowlistEnabled: boolean;
|
||||||
|
allowed: boolean;
|
||||||
|
groupConfig?: ProviderGroupConfig;
|
||||||
|
defaultConfig?: ProviderGroupConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProviderGroups = Record<string, ProviderGroupConfig>;
|
||||||
|
|
||||||
|
function resolveProviderGroups(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
surface: GroupPolicySurface,
|
||||||
|
): ProviderGroups | undefined {
|
||||||
|
if (surface === "whatsapp") return cfg.whatsapp?.groups;
|
||||||
|
if (surface === "telegram") return cfg.telegram?.groups;
|
||||||
|
if (surface === "imessage") return cfg.imessage?.groups;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveProviderGroupPolicy(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
surface: GroupPolicySurface;
|
||||||
|
groupId?: string | null;
|
||||||
|
}): ProviderGroupPolicy {
|
||||||
|
const { cfg, surface } = params;
|
||||||
|
const groups = resolveProviderGroups(cfg, surface);
|
||||||
|
const allowlistEnabled = Boolean(groups && Object.keys(groups).length > 0);
|
||||||
|
const normalizedId = params.groupId?.trim();
|
||||||
|
const groupConfig = normalizedId && groups ? groups[normalizedId] : undefined;
|
||||||
|
const defaultConfig = groups?.["*"];
|
||||||
|
const allowAll =
|
||||||
|
allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*"));
|
||||||
|
const allowed =
|
||||||
|
!allowlistEnabled ||
|
||||||
|
allowAll ||
|
||||||
|
(normalizedId
|
||||||
|
? Boolean(groups && Object.hasOwn(groups, normalizedId))
|
||||||
|
: false);
|
||||||
|
return {
|
||||||
|
allowlistEnabled,
|
||||||
|
allowed,
|
||||||
|
groupConfig,
|
||||||
|
defaultConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveProviderGroupRequireMention(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
surface: GroupPolicySurface;
|
||||||
|
groupId?: string | null;
|
||||||
|
requireMentionOverride?: boolean;
|
||||||
|
overrideOrder?: "before-config" | "after-config";
|
||||||
|
}): boolean {
|
||||||
|
const { requireMentionOverride, overrideOrder = "after-config" } = params;
|
||||||
|
const { groupConfig, defaultConfig } = resolveProviderGroupPolicy(params);
|
||||||
|
const configMention =
|
||||||
|
typeof groupConfig?.requireMention === "boolean"
|
||||||
|
? groupConfig.requireMention
|
||||||
|
: typeof defaultConfig?.requireMention === "boolean"
|
||||||
|
? defaultConfig.requireMention
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
overrideOrder === "before-config" &&
|
||||||
|
typeof requireMentionOverride === "boolean"
|
||||||
|
) {
|
||||||
|
return requireMentionOverride;
|
||||||
|
}
|
||||||
|
if (typeof configMention === "boolean") return configMention;
|
||||||
|
if (
|
||||||
|
overrideOrder !== "before-config" &&
|
||||||
|
typeof requireMentionOverride === "boolean"
|
||||||
|
) {
|
||||||
|
return requireMentionOverride;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -169,6 +169,36 @@ describe("monitorIMessageProvider", () => {
|
|||||||
expect(replyMock).toHaveBeenCalled();
|
expect(replyMock).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks group messages when imessage.groups is set without a wildcard", async () => {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
imessage: { groups: { "99": { requireMention: false } } },
|
||||||
|
};
|
||||||
|
const run = monitorIMessageProvider();
|
||||||
|
await waitForSubscribe();
|
||||||
|
|
||||||
|
notificationHandler?.({
|
||||||
|
method: "message",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
id: 13,
|
||||||
|
chat_id: 123,
|
||||||
|
sender: "+15550001111",
|
||||||
|
is_from_me: false,
|
||||||
|
text: "@clawd hello",
|
||||||
|
is_group: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flush();
|
||||||
|
closeResolve?.();
|
||||||
|
await run;
|
||||||
|
|
||||||
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
|
expect(sendMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("prefixes tool and final replies with responsePrefix", async () => {
|
it("prefixes tool and final replies with responsePrefix", async () => {
|
||||||
config = {
|
config = {
|
||||||
...config,
|
...config,
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
|||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
resolveProviderGroupPolicy,
|
||||||
|
resolveProviderGroupRequireMention,
|
||||||
|
} from "../config/group-policy.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { mediaKindFromMime } from "../media/constants.js";
|
import { mediaKindFromMime } from "../media/constants.js";
|
||||||
@@ -71,24 +75,6 @@ function resolveAllowFrom(opts: MonitorIMessageOpts): string[] {
|
|||||||
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGroupRequireMention(
|
|
||||||
cfg: ReturnType<typeof loadConfig>,
|
|
||||||
opts: MonitorIMessageOpts,
|
|
||||||
chatId?: number | null,
|
|
||||||
): boolean {
|
|
||||||
if (typeof opts.requireMention === "boolean") return opts.requireMention;
|
|
||||||
const groupId = chatId != null ? String(chatId) : undefined;
|
|
||||||
if (groupId) {
|
|
||||||
const groupConfig = cfg.imessage?.groups?.[groupId];
|
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
|
||||||
return groupConfig.requireMention;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupDefault = cfg.imessage?.groups?.["*"]?.requireMention;
|
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deliverReplies(params: {
|
async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
target: string;
|
target: string;
|
||||||
@@ -152,6 +138,21 @@ export async function monitorIMessageProvider(
|
|||||||
const isGroup = Boolean(message.is_group);
|
const isGroup = Boolean(message.is_group);
|
||||||
if (isGroup && !chatId) return;
|
if (isGroup && !chatId) return;
|
||||||
|
|
||||||
|
const groupId = isGroup ? String(chatId) : undefined;
|
||||||
|
if (isGroup) {
|
||||||
|
const groupPolicy = resolveProviderGroupPolicy({
|
||||||
|
cfg,
|
||||||
|
surface: "imessage",
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
|
||||||
|
logVerbose(
|
||||||
|
`imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const commandAuthorized = isAllowedIMessageSender({
|
const commandAuthorized = isAllowedIMessageSender({
|
||||||
allowFrom,
|
allowFrom,
|
||||||
sender,
|
sender,
|
||||||
@@ -168,7 +169,13 @@ export async function monitorIMessageProvider(
|
|||||||
const mentioned = isGroup
|
const mentioned = isGroup
|
||||||
? matchesMentionPatterns(messageText, mentionRegexes)
|
? matchesMentionPatterns(messageText, mentionRegexes)
|
||||||
: true;
|
: true;
|
||||||
const requireMention = resolveGroupRequireMention(cfg, opts, chatId);
|
const requireMention = resolveProviderGroupRequireMention({
|
||||||
|
cfg,
|
||||||
|
surface: "imessage",
|
||||||
|
groupId,
|
||||||
|
requireMentionOverride: opts.requireMention,
|
||||||
|
overrideOrder: "before-config",
|
||||||
|
});
|
||||||
const canDetectMention = mentionRegexes.length > 0;
|
const canDetectMention = mentionRegexes.length > 0;
|
||||||
const shouldBypassMention =
|
const shouldBypassMention =
|
||||||
isGroup &&
|
isGroup &&
|
||||||
|
|||||||
@@ -411,6 +411,38 @@ describe("createTelegramBot", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks group messages when telegram.groups is set without a wildcard", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||||
|
typeof vi.fn
|
||||||
|
>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
telegram: {
|
||||||
|
groups: {
|
||||||
|
"123": { requireMention: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = onSpy.mock.calls[0][1] as (
|
||||||
|
ctx: Record<string, unknown>,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 456, type: "group", title: "Ops" },
|
||||||
|
text: "@clawdbot_bot hello",
|
||||||
|
date: 1736380800,
|
||||||
|
},
|
||||||
|
me: { username: "clawdbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips group messages without mention when requireMention is enabled", async () => {
|
it("skips group messages without mention when requireMention is enabled", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import type { ReplyToMode } from "../config/config.js";
|
import type { ReplyToMode } from "../config/config.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
resolveProviderGroupPolicy,
|
||||||
|
resolveProviderGroupRequireMention,
|
||||||
|
} from "../config/group-policy.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
@@ -73,17 +77,20 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
(opts.mediaMaxMb ?? cfg.telegram?.mediaMaxMb ?? 5) * 1024 * 1024;
|
(opts.mediaMaxMb ?? cfg.telegram?.mediaMaxMb ?? 5) * 1024 * 1024;
|
||||||
const logger = getChildLogger({ module: "telegram-auto-reply" });
|
const logger = getChildLogger({ module: "telegram-auto-reply" });
|
||||||
const mentionRegexes = buildMentionRegexes(cfg);
|
const mentionRegexes = buildMentionRegexes(cfg);
|
||||||
const resolveGroupRequireMention = (chatId: string | number) => {
|
const resolveGroupPolicy = (chatId: string | number) =>
|
||||||
const groupId = String(chatId);
|
resolveProviderGroupPolicy({
|
||||||
const groupConfig = cfg.telegram?.groups?.[groupId];
|
cfg,
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
surface: "telegram",
|
||||||
return groupConfig.requireMention;
|
groupId: String(chatId),
|
||||||
}
|
});
|
||||||
const groupDefault = cfg.telegram?.groups?.["*"]?.requireMention;
|
const resolveGroupRequireMention = (chatId: string | number) =>
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
resolveProviderGroupRequireMention({
|
||||||
if (typeof opts.requireMention === "boolean") return opts.requireMention;
|
cfg,
|
||||||
return true;
|
surface: "telegram",
|
||||||
};
|
groupId: String(chatId),
|
||||||
|
requireMentionOverride: opts.requireMention,
|
||||||
|
overrideOrder: "after-config",
|
||||||
|
});
|
||||||
|
|
||||||
bot.on("message", async (ctx) => {
|
bot.on("message", async (ctx) => {
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +100,17 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const isGroup =
|
const isGroup =
|
||||||
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
|
|
||||||
|
if (isGroup) {
|
||||||
|
const groupPolicy = resolveGroupPolicy(chatId);
|
||||||
|
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
|
||||||
|
logger.info(
|
||||||
|
{ chatId, title: msg.chat.title, reason: "not-allowed" },
|
||||||
|
"skipping group message",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendTyping = async () => {
|
const sendTyping = async () => {
|
||||||
try {
|
try {
|
||||||
await bot.api.sendChatAction(chatId, "typing");
|
await bot.api.sendChatAction(chatId, "typing");
|
||||||
@@ -143,16 +161,17 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
||||||
(ent) => ent.type === "mention",
|
(ent) => ent.type === "mention",
|
||||||
);
|
);
|
||||||
|
const requireMention = resolveGroupRequireMention(chatId);
|
||||||
const shouldBypassMention =
|
const shouldBypassMention =
|
||||||
isGroup &&
|
isGroup &&
|
||||||
resolveGroupRequireMention(chatId) &&
|
requireMention &&
|
||||||
!wasMentioned &&
|
!wasMentioned &&
|
||||||
!hasAnyMention &&
|
!hasAnyMention &&
|
||||||
commandAuthorized &&
|
commandAuthorized &&
|
||||||
hasControlCommand(msg.text ?? msg.caption ?? "");
|
hasControlCommand(msg.text ?? msg.caption ?? "");
|
||||||
const canDetectMention =
|
const canDetectMention =
|
||||||
Boolean(botUsername) || mentionRegexes.length > 0;
|
Boolean(botUsername) || mentionRegexes.length > 0;
|
||||||
if (isGroup && resolveGroupRequireMention(chatId) && canDetectMention) {
|
if (isGroup && requireMention && canDetectMention) {
|
||||||
if (!wasMentioned && !shouldBypassMention) {
|
if (!wasMentioned && !shouldBypassMention) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ chatId, reason: "no-mention" },
|
{ chatId, reason: "no-mention" },
|
||||||
|
|||||||
@@ -1045,6 +1045,57 @@ describe("web auto-reply", () => {
|
|||||||
resetLoadConfigMock();
|
resetLoadConfigMock();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks group messages when whatsapp groups is set without a wildcard", async () => {
|
||||||
|
const sendMedia = vi.fn();
|
||||||
|
const reply = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const sendComposing = vi.fn();
|
||||||
|
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
|
||||||
|
|
||||||
|
setLoadConfigMock(() => ({
|
||||||
|
whatsapp: {
|
||||||
|
allowFrom: ["*"],
|
||||||
|
groups: { "999@g.us": { requireMention: false } },
|
||||||
|
},
|
||||||
|
routing: { groupChat: { mentionPatterns: ["@clawd"] } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
let capturedOnMessage:
|
||||||
|
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
|
||||||
|
| undefined;
|
||||||
|
const listenerFactory = async (opts: {
|
||||||
|
onMessage: (
|
||||||
|
msg: import("./inbound.js").WebInboundMessage,
|
||||||
|
) => Promise<void>;
|
||||||
|
}) => {
|
||||||
|
capturedOnMessage = opts.onMessage;
|
||||||
|
return { close: vi.fn() };
|
||||||
|
};
|
||||||
|
|
||||||
|
await monitorWebProvider(false, listenerFactory, false, resolver);
|
||||||
|
expect(capturedOnMessage).toBeDefined();
|
||||||
|
|
||||||
|
await capturedOnMessage?.({
|
||||||
|
body: "@clawd hello",
|
||||||
|
from: "123@g.us",
|
||||||
|
conversationId: "123@g.us",
|
||||||
|
chatId: "123@g.us",
|
||||||
|
chatType: "group",
|
||||||
|
to: "+2",
|
||||||
|
id: "g-allowlist-block",
|
||||||
|
senderE164: "+111",
|
||||||
|
senderName: "Alice",
|
||||||
|
mentionedJids: ["999@s.whatsapp.net"],
|
||||||
|
selfE164: "+999",
|
||||||
|
selfJid: "999@s.whatsapp.net",
|
||||||
|
sendComposing,
|
||||||
|
reply,
|
||||||
|
sendMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolver).not.toHaveBeenCalled();
|
||||||
|
resetLoadConfigMock();
|
||||||
|
});
|
||||||
|
|
||||||
it("honors per-group mention overrides when conversationId uses session key", async () => {
|
it("honors per-group mention overrides when conversationId uses session key", async () => {
|
||||||
const sendMedia = vi.fn();
|
const sendMedia = vi.fn();
|
||||||
const reply = vi.fn().mockResolvedValue(undefined);
|
const reply = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import { waitForever } from "../cli/wait.js";
|
import { waitForever } from "../cli/wait.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
resolveProviderGroupPolicy,
|
||||||
|
resolveProviderGroupRequireMention,
|
||||||
|
} from "../config/group-policy.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_IDLE_MINUTES,
|
DEFAULT_IDLE_MINUTES,
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
@@ -850,16 +854,24 @@ export async function monitorWebProvider(
|
|||||||
Surface: "whatsapp",
|
Surface: "whatsapp",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolveGroupPolicyFor = (conversationId: string) => {
|
||||||
|
const groupId =
|
||||||
|
resolveGroupResolution(conversationId)?.id ?? conversationId;
|
||||||
|
return resolveProviderGroupPolicy({
|
||||||
|
cfg,
|
||||||
|
surface: "whatsapp",
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resolveGroupRequireMentionFor = (conversationId: string) => {
|
const resolveGroupRequireMentionFor = (conversationId: string) => {
|
||||||
const groupId =
|
const groupId =
|
||||||
resolveGroupResolution(conversationId)?.id ?? conversationId;
|
resolveGroupResolution(conversationId)?.id ?? conversationId;
|
||||||
const groupConfig = cfg.whatsapp?.groups?.[groupId];
|
return resolveProviderGroupRequireMention({
|
||||||
if (typeof groupConfig?.requireMention === "boolean") {
|
cfg,
|
||||||
return groupConfig.requireMention;
|
surface: "whatsapp",
|
||||||
}
|
groupId,
|
||||||
const groupDefault = cfg.whatsapp?.groups?.["*"]?.requireMention;
|
});
|
||||||
if (typeof groupDefault === "boolean") return groupDefault;
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveGroupActivationFor = (conversationId: string) => {
|
const resolveGroupActivationFor = (conversationId: string) => {
|
||||||
@@ -1275,6 +1287,13 @@ export async function monitorWebProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.chatType === "group") {
|
if (msg.chatType === "group") {
|
||||||
|
const groupPolicy = resolveGroupPolicyFor(conversationId);
|
||||||
|
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
|
||||||
|
logVerbose(
|
||||||
|
`Skipping group message ${conversationId} (not in allowlist)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
||||||
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
||||||
const activationCommand = parseActivationCommand(commandBody);
|
const activationCommand = parseActivationCommand(commandBody);
|
||||||
|
|||||||
Reference in New Issue
Block a user