fix: tighten group elevated targeting
This commit is contained in:
@@ -340,6 +340,93 @@ describe("trigger handling", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores elevated directive in groups when not mentioned", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "group:123@g.us",
|
||||
To: "whatsapp:+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1000",
|
||||
ChatType: "group",
|
||||
WasMentioned: false,
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(text).not.toContain("Elevated mode enabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("allows elevated directive in groups when mentioned", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "group:123@g.us",
|
||||
To: "whatsapp:+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1000",
|
||||
ChatType: "group",
|
||||
WasMentioned: true,
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe(
|
||||
"on",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores inline elevated directive for unapproved sender", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
|
||||
@@ -329,8 +329,10 @@ export async function getReplyFromConfig(
|
||||
.map((entry) => entry.alias?.trim())
|
||||
.filter((alias): alias is string => Boolean(alias))
|
||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||
const disableElevatedInGroup = isGroup && ctx.WasMentioned !== true;
|
||||
let parsedDirectives = parseInlineDirectives(rawBody, {
|
||||
modelAliases: configuredAliases,
|
||||
disableElevated: disableElevatedInGroup,
|
||||
});
|
||||
const hasDirective =
|
||||
parsedDirectives.hasThinkDirective ||
|
||||
@@ -342,7 +344,9 @@ export async function getReplyFromConfig(
|
||||
parsedDirectives.hasQueueDirective;
|
||||
if (hasDirective) {
|
||||
const stripped = stripStructuralPrefixes(parsedDirectives.cleaned);
|
||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
||||
const noMentions = isGroup
|
||||
? stripMentions(stripped, ctx, cfg, agentId)
|
||||
: stripped;
|
||||
if (noMentions.trim().length > 0) {
|
||||
parsedDirectives = clearInlineDirectives(parsedDirectives.cleaned);
|
||||
}
|
||||
@@ -467,6 +471,7 @@ export async function getReplyFromConfig(
|
||||
cleanedBody: directives.cleaned,
|
||||
ctx,
|
||||
cfg,
|
||||
agentId,
|
||||
isGroup,
|
||||
})
|
||||
) {
|
||||
@@ -549,6 +554,7 @@ export async function getReplyFromConfig(
|
||||
const command = buildCommandContext({
|
||||
ctx,
|
||||
cfg,
|
||||
agentId,
|
||||
sessionKey,
|
||||
isGroup,
|
||||
triggerBodyNormalized,
|
||||
@@ -579,6 +585,7 @@ export async function getReplyFromConfig(
|
||||
ctx,
|
||||
cfg,
|
||||
command,
|
||||
agentId,
|
||||
directives,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
|
||||
@@ -125,11 +125,12 @@ function extractCompactInstructions(params: {
|
||||
rawBody?: string;
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
}): string | undefined {
|
||||
const raw = stripStructuralPrefixes(params.rawBody ?? "");
|
||||
const stripped = params.isGroup
|
||||
? stripMentions(raw, params.ctx, params.cfg)
|
||||
? stripMentions(raw, params.ctx, params.cfg, params.agentId)
|
||||
: raw;
|
||||
const trimmed = stripped.trim();
|
||||
if (!trimmed) return undefined;
|
||||
@@ -144,12 +145,14 @@ function extractCompactInstructions(params: {
|
||||
export function buildCommandContext(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
isGroup: boolean;
|
||||
triggerBodyNormalized: string;
|
||||
commandAuthorized: boolean;
|
||||
}): CommandContext {
|
||||
const { ctx, cfg, sessionKey, isGroup, triggerBodyNormalized } = params;
|
||||
const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } =
|
||||
params;
|
||||
const auth = resolveCommandAuthorization({
|
||||
ctx,
|
||||
cfg,
|
||||
@@ -161,7 +164,9 @@ export function buildCommandContext(params: {
|
||||
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const rawBodyNormalized = triggerBodyNormalized;
|
||||
const commandBodyNormalized = normalizeCommandBody(
|
||||
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg) : rawBodyNormalized,
|
||||
isGroup
|
||||
? stripMentions(rawBodyNormalized, ctx, cfg, agentId)
|
||||
: rawBodyNormalized,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -206,6 +211,7 @@ export async function handleCommands(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
command: CommandContext;
|
||||
agentId?: string;
|
||||
directives: InlineDirectives;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
@@ -530,6 +536,7 @@ export async function handleCommands(params: {
|
||||
rawBody: ctx.Body,
|
||||
ctx,
|
||||
cfg,
|
||||
agentId: params.agentId,
|
||||
isGroup,
|
||||
});
|
||||
const result = await compactEmbeddedPiSession({
|
||||
|
||||
@@ -184,7 +184,7 @@ export type InlineDirectives = {
|
||||
|
||||
export function parseInlineDirectives(
|
||||
body: string,
|
||||
options?: { modelAliases?: string[] },
|
||||
options?: { modelAliases?: string[]; disableElevated?: boolean },
|
||||
): InlineDirectives {
|
||||
const {
|
||||
cleaned: thinkCleaned,
|
||||
@@ -209,7 +209,14 @@ export function parseInlineDirectives(
|
||||
elevatedLevel,
|
||||
rawLevel: rawElevatedLevel,
|
||||
hasDirective: hasElevatedDirective,
|
||||
} = extractElevatedDirective(reasoningCleaned);
|
||||
} = options?.disableElevated
|
||||
? {
|
||||
cleaned: reasoningCleaned,
|
||||
elevatedLevel: undefined,
|
||||
rawLevel: undefined,
|
||||
hasDirective: false,
|
||||
}
|
||||
: extractElevatedDirective(reasoningCleaned);
|
||||
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } =
|
||||
extractStatusDirective(elevatedCleaned);
|
||||
const {
|
||||
@@ -272,9 +279,10 @@ export function isDirectiveOnly(params: {
|
||||
cleanedBody: string;
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
}): boolean {
|
||||
const { directives, cleanedBody, ctx, cfg, isGroup } = params;
|
||||
const { directives, cleanedBody, ctx, cfg, agentId, isGroup } = params;
|
||||
if (
|
||||
!directives.hasThinkDirective &&
|
||||
!directives.hasVerboseDirective &&
|
||||
@@ -285,7 +293,9 @@ export function isDirectiveOnly(params: {
|
||||
)
|
||||
return false;
|
||||
const stripped = stripStructuralPrefixes(cleanedBody ?? "");
|
||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
||||
const noMentions = isGroup
|
||||
? stripMentions(stripped, ctx, cfg, agentId)
|
||||
: stripped;
|
||||
return noMentions.length === 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,4 +27,20 @@ describe("mention helpers", () => {
|
||||
});
|
||||
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses per-agent mention patterns when configured", () => {
|
||||
const regexes = buildMentionRegexes(
|
||||
{
|
||||
routing: {
|
||||
groupChat: { mentionPatterns: ["\\bglobal\\b"] },
|
||||
agents: {
|
||||
work: { mentionPatterns: ["\\bworkbot\\b"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
"work",
|
||||
);
|
||||
expect(matchesMentionPatterns("workbot: hi", regexes)).toBe(true);
|
||||
expect(matchesMentionPatterns("global: hi", regexes)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
export function buildMentionRegexes(cfg: ClawdbotConfig | undefined): RegExp[] {
|
||||
const patterns = cfg?.routing?.groupChat?.mentionPatterns ?? [];
|
||||
function resolveMentionPatterns(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
agentId?: string,
|
||||
): string[] {
|
||||
if (!cfg) return [];
|
||||
const agentConfig = agentId ? cfg.routing?.agents?.[agentId] : undefined;
|
||||
if (agentConfig && Object.hasOwn(agentConfig, "mentionPatterns")) {
|
||||
return agentConfig.mentionPatterns ?? [];
|
||||
}
|
||||
return cfg.routing?.groupChat?.mentionPatterns ?? [];
|
||||
}
|
||||
|
||||
export function buildMentionRegexes(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
agentId?: string,
|
||||
): RegExp[] {
|
||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
||||
return patterns
|
||||
.map((pattern) => {
|
||||
try {
|
||||
@@ -48,9 +63,10 @@ export function stripMentions(
|
||||
text: string,
|
||||
ctx: MsgContext,
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
agentId?: string,
|
||||
): string {
|
||||
let result = text;
|
||||
const patterns = cfg?.routing?.groupChat?.mentionPatterns ?? [];
|
||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
||||
for (const p of patterns) {
|
||||
try {
|
||||
const re = new RegExp(p, "gi");
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function initSessionState(params: {
|
||||
// web inbox before we get here. They prevented reset triggers like "/new"
|
||||
// from matching, so strip structural wrappers when checking for resets.
|
||||
const strippedForReset = isGroup
|
||||
? stripMentions(triggerBodyNormalized, ctx, cfg)
|
||||
? stripMentions(triggerBodyNormalized, ctx, cfg, agentId)
|
||||
: triggerBodyNormalized;
|
||||
for (const trigger of resetTriggers) {
|
||||
if (!trigger) continue;
|
||||
|
||||
@@ -723,6 +723,8 @@ export type RoutingConfig = {
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
/** Per-agent override for group mention patterns. */
|
||||
mentionPatterns?: string[];
|
||||
subagents?: {
|
||||
/** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */
|
||||
allowAgents?: string[];
|
||||
|
||||
@@ -479,7 +479,6 @@ export function createDiscordMessageHandler(params: {
|
||||
guildEntries,
|
||||
} = params;
|
||||
const logger = getChildLogger({ module: "discord-auto-reply" });
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const groupPolicy = discordConfig?.groupPolicy ?? "open";
|
||||
@@ -576,6 +575,17 @@ export function createDiscordMessageHandler(params: {
|
||||
}
|
||||
const botId = botUserId;
|
||||
const baseText = resolveDiscordMessageText(message);
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "discord",
|
||||
accountId,
|
||||
guildId: data.guild_id ?? undefined,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
|
||||
id: isDirectMessage ? author.id : message.channelId,
|
||||
},
|
||||
});
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const wasMentioned =
|
||||
!isDirectMessage &&
|
||||
(Boolean(
|
||||
@@ -647,16 +657,6 @@ export function createDiscordMessageHandler(params: {
|
||||
guildInfo?.slug ||
|
||||
(data.guild?.name ? normalizeDiscordSlug(data.guild.name) : "");
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "discord",
|
||||
accountId,
|
||||
guildId: data.guild_id ?? undefined,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
|
||||
id: isDirectMessage ? author.id : message.channelId,
|
||||
},
|
||||
});
|
||||
const baseSessionKey = route.sessionKey;
|
||||
const channelConfig = isGuildMessage
|
||||
? resolveDiscordChannelConfig({
|
||||
|
||||
@@ -149,7 +149,6 @@ export async function monitorIMessageProvider(
|
||||
);
|
||||
const groupPolicy = imessageCfg.groupPolicy ?? "open";
|
||||
const dmPolicy = imessageCfg.dmPolicy ?? "pairing";
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
const includeAttachments =
|
||||
opts.includeAttachments ?? imessageCfg.includeAttachments ?? false;
|
||||
const mediaMaxBytes =
|
||||
@@ -287,6 +286,18 @@ export async function monitorIMessageProvider(
|
||||
}
|
||||
}
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: isGroup
|
||||
? String(chatId ?? "unknown")
|
||||
: normalizeIMessageHandle(sender),
|
||||
},
|
||||
});
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const messageText = (message.text ?? "").trim();
|
||||
const mentioned = isGroup
|
||||
? matchesMentionPatterns(messageText, mentionRegexes)
|
||||
@@ -357,17 +368,6 @@ export async function monitorIMessageProvider(
|
||||
body: bodyText,
|
||||
});
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: isGroup
|
||||
? String(chatId ?? "unknown")
|
||||
: normalizeIMessageHandle(sender),
|
||||
},
|
||||
});
|
||||
const imessageTo = chatTarget || `imessage:${sender}`;
|
||||
const ctxPayload = {
|
||||
Body: body,
|
||||
|
||||
@@ -508,7 +508,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
opts.slashCommand ?? slackCfg.slashCommand,
|
||||
);
|
||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const mediaMaxBytes =
|
||||
@@ -855,6 +854,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "slack",
|
||||
accountId: account.accountId,
|
||||
teamId: teamId || undefined,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "dm" : isRoom ? "channel" : "group",
|
||||
id: isDirectMessage ? (message.user ?? "unknown") : message.channel,
|
||||
},
|
||||
});
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const wasMentioned =
|
||||
opts.wasMentioned ??
|
||||
(!isDirectMessage &&
|
||||
@@ -963,16 +973,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
: isRoom
|
||||
? `slack:channel:${message.channel}`
|
||||
: `slack:group:${message.channel}`;
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "slack",
|
||||
accountId: account.accountId,
|
||||
teamId: teamId || undefined,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "dm" : isRoom ? "channel" : "group",
|
||||
id: isDirectMessage ? (message.user ?? "unknown") : message.channel,
|
||||
},
|
||||
});
|
||||
const baseSessionKey = route.sessionKey;
|
||||
const threadTs = message.thread_ts;
|
||||
const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0;
|
||||
|
||||
@@ -225,7 +225,6 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
const mediaMaxBytes =
|
||||
(opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024;
|
||||
const logger = getChildLogger({ module: "telegram-auto-reply" });
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
let botHasTopicsEnabled: boolean | undefined;
|
||||
const resolveBotTopicsEnabled = async (ctx?: TelegramContext) => {
|
||||
const fromCtx = ctx?.me as { has_topics_enabled?: boolean } | undefined;
|
||||
@@ -322,6 +321,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
id: peerId,
|
||||
},
|
||||
});
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const effectiveDmAllow = normalizeAllowFrom([
|
||||
...(allowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
|
||||
@@ -1132,6 +1132,81 @@ describe("web auto-reply", () => {
|
||||
expect(payload.Body).toContain("[from: Bob (+222)]");
|
||||
});
|
||||
|
||||
it("uses per-agent mention patterns for group gating", 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: { "*": { requireMention: true } },
|
||||
},
|
||||
routing: {
|
||||
groupChat: { mentionPatterns: ["@global"] },
|
||||
agents: {
|
||||
work: { mentionPatterns: ["@workbot"] },
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", peer: { kind: "group", id: "123@g.us" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
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: "@global ping",
|
||||
from: "123@g.us",
|
||||
conversationId: "123@g.us",
|
||||
chatId: "123@g.us",
|
||||
chatType: "group",
|
||||
to: "+2",
|
||||
id: "g1",
|
||||
senderE164: "+111",
|
||||
senderName: "Alice",
|
||||
selfE164: "+999",
|
||||
sendComposing,
|
||||
reply,
|
||||
sendMedia,
|
||||
});
|
||||
expect(resolver).not.toHaveBeenCalled();
|
||||
|
||||
await capturedOnMessage?.({
|
||||
body: "@workbot ping",
|
||||
from: "123@g.us",
|
||||
conversationId: "123@g.us",
|
||||
chatId: "123@g.us",
|
||||
chatType: "group",
|
||||
to: "+2",
|
||||
id: "g2",
|
||||
senderE164: "+222",
|
||||
senderName: "Bob",
|
||||
selfE164: "+999",
|
||||
sendComposing,
|
||||
reply,
|
||||
sendMedia,
|
||||
});
|
||||
expect(resolver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows group messages when whatsapp groups default disables mention gating", async () => {
|
||||
const sendMedia = vi.fn();
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -162,8 +162,11 @@ type MentionConfig = {
|
||||
allowFrom?: Array<string | number>;
|
||||
};
|
||||
|
||||
function buildMentionConfig(cfg: ReturnType<typeof loadConfig>): MentionConfig {
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
function buildMentionConfig(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
agentId?: string,
|
||||
): MentionConfig {
|
||||
const mentionRegexes = buildMentionRegexes(cfg, agentId);
|
||||
return { mentionRegexes, allowFrom: cfg.whatsapp?.allowFrom };
|
||||
}
|
||||
|
||||
@@ -793,7 +796,9 @@ export async function monitorWebProvider(
|
||||
tuning.heartbeatSeconds,
|
||||
);
|
||||
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
||||
const mentionConfig = buildMentionConfig(cfg);
|
||||
const resolveMentionConfig = (agentId?: string) =>
|
||||
buildMentionConfig(cfg, agentId);
|
||||
const baseMentionConfig = resolveMentionConfig();
|
||||
const groupHistoryLimit =
|
||||
cfg.routing?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
|
||||
const groupHistories = new Map<
|
||||
@@ -913,7 +918,7 @@ export async function monitorWebProvider(
|
||||
};
|
||||
|
||||
const resolveOwnerList = (selfE164?: string | null) => {
|
||||
const allowFrom = mentionConfig.allowFrom;
|
||||
const allowFrom = baseMentionConfig.allowFrom;
|
||||
const raw =
|
||||
Array.isArray(allowFrom) && allowFrom.length > 0
|
||||
? allowFrom
|
||||
@@ -943,9 +948,13 @@ export async function monitorWebProvider(
|
||||
);
|
||||
};
|
||||
|
||||
const stripMentionsForCommand = (text: string, selfE164?: string | null) => {
|
||||
const stripMentionsForCommand = (
|
||||
text: string,
|
||||
mentionRegexes: RegExp[],
|
||||
selfE164?: string | null,
|
||||
) => {
|
||||
let result = text;
|
||||
for (const re of mentionConfig.mentionRegexes) {
|
||||
for (const re of mentionRegexes) {
|
||||
result = result.replace(re, " ");
|
||||
}
|
||||
if (selfE164) {
|
||||
@@ -1362,7 +1371,12 @@ export async function monitorWebProvider(
|
||||
});
|
||||
}
|
||||
noteGroupMember(groupHistoryKey, msg.senderE164, msg.senderName);
|
||||
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
||||
const mentionConfig = resolveMentionConfig(route.agentId);
|
||||
const commandBody = stripMentionsForCommand(
|
||||
msg.body,
|
||||
mentionConfig.mentionRegexes,
|
||||
msg.selfE164,
|
||||
);
|
||||
const activationCommand = parseActivationCommand(commandBody);
|
||||
const isOwner = isOwnerSender(msg);
|
||||
const statusCommand = isStatusCommand(commandBody);
|
||||
|
||||
Reference in New Issue
Block a user