diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 1a7a68058..86d7bf63e 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; -import { resolveAckReaction } from "clawdbot/plugin-sdk"; +import { resolveAckReaction, resolveControlCommandGate } from "clawdbot/plugin-sdk"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js"; import { downloadBlueBubblesAttachment } from "./attachments.js"; @@ -1346,18 +1346,19 @@ async function processMessage( }) : false; const dmAuthorized = dmPolicy === "open" || ownerAllowedForCommands; - const commandAuthorized = isGroup - ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ - useAccessGroups, - authorizers: [ - { configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands }, - { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, - ], - }) - : dmAuthorized; + const commandGate = resolveControlCommandGate({ + useAccessGroups, + authorizers: [ + { configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands }, + { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, + ], + allowTextCommands: true, + hasControlCommand: hasControlCmd, + }); + const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized; // Block control commands from unauthorized senders in groups - if (isGroup && hasControlCmd && !commandAuthorized) { + if (isGroup && commandGate.shouldBlock) { logVerbose( core, runtime, diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index fdb3029b1..8bc43879f 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -4,6 +4,7 @@ import { createReplyPrefixContext, createTypingCallbacks, formatAllowlistMatchMeta, + resolveControlCommandGate, type RuntimeEnv, } from "clawdbot/plugin-sdk"; import type { CoreConfig, ReplyToMode } from "../../types.js"; @@ -378,20 +379,19 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam userName: senderName, }) : false; - const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ + const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg); + const commandGate = resolveControlCommandGate({ useAccessGroups, authorizers: [ { configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }, { configured: roomUsers.length > 0, allowed: senderAllowedForRoomUsers }, { configured: groupAllowConfigured, allowed: senderAllowedForGroup }, ], + allowTextCommands, + hasControlCommand: hasControlCommandInMessage, }); - if ( - isRoom && - allowTextCommands && - core.channel.text.hasControlCommand(bodyText, cfg) && - !commandAuthorized - ) { + const commandAuthorized = commandGate.commandAuthorized; + if (isRoom && commandGate.shouldBlock) { logVerboseMessage(`matrix: drop control command from unauthorized sender ${senderId}`); return; } @@ -411,7 +411,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam !wasMentioned && !hasExplicitMention && commandAuthorized && - core.channel.text.hasControlCommand(bodyText); + hasControlCommandInMessage; const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention; if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) { logger.info({ roomId, reason: "no-mention" }, "skipping room message"); diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 03a591924..8dd5c6f9b 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -13,6 +13,7 @@ import { clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntryIfEnabled, + resolveControlCommandGate, resolveChannelMediaMaxBytes, type HistoryEntry, } from "clawdbot/plugin-sdk"; @@ -398,7 +399,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} cfg, surface: "mattermost", }); - const isControlCommand = allowTextCommands && core.channel.text.hasControlCommand(rawText, cfg); + const hasControlCommand = core.channel.text.hasControlCommand(rawText, cfg); + const isControlCommand = allowTextCommands && hasControlCommand; const useAccessGroups = cfg.commands?.useAccessGroups !== false; const senderAllowedForCommands = isSenderAllowed({ senderId, @@ -410,19 +412,20 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} senderName, allowFrom: effectiveGroupAllowFrom, }); + const commandGate = resolveControlCommandGate({ + useAccessGroups, + authorizers: [ + { configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }, + { + configured: effectiveGroupAllowFrom.length > 0, + allowed: groupAllowedForCommands, + }, + ], + allowTextCommands, + hasControlCommand, + }); const commandAuthorized = - kind === "dm" - ? dmPolicy === "open" || senderAllowedForCommands - : core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ - useAccessGroups, - authorizers: [ - { configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }, - { - configured: effectiveGroupAllowFrom.length > 0, - allowed: groupAllowedForCommands, - }, - ], - }); + kind === "dm" ? dmPolicy === "open" || senderAllowedForCommands : commandGate.commandAuthorized; if (kind === "dm") { if (dmPolicy === "disabled") { @@ -483,7 +486,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} } } - if (kind !== "dm" && isControlCommand && !commandAuthorized) { + if (kind !== "dm" && commandGate.shouldBlock) { logVerboseMessage( `mattermost: drop control command from unauthorized sender ${senderId}`, ); diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 715b6adf0..9bf113584 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -3,6 +3,7 @@ import { clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntryIfEnabled, + resolveControlCommandGate, resolveMentionGating, formatAllowlistMatchMeta, type HistoryEntry, @@ -251,14 +252,18 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { senderId, senderName, }); - const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ + const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg); + const commandGate = resolveControlCommandGate({ useAccessGroups, authorizers: [ { configured: effectiveDmAllowFrom.length > 0, allowed: ownerAllowedForCommands }, { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, ], + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, }); - if (core.channel.text.hasControlCommand(text, cfg) && !commandAuthorized) { + const commandAuthorized = commandGate.commandAuthorized; + if (commandGate.shouldBlock) { logVerboseMessage(`msteams: drop control command from unauthorized sender ${senderId}`); return; } diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index bfa18d834..ec99ba8f3 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; +import { resolveControlCommandGate, type ClawdbotConfig, type RuntimeEnv } from "clawdbot/plugin-sdk"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { @@ -118,7 +118,11 @@ export async function handleNextcloudTalkInbound(params: { senderId, senderName, }).allowed; - const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ + const hasControlCommand = core.channel.text.hasControlCommand( + rawBody, + config as ClawdbotConfig, + ); + const commandGate = resolveControlCommandGate({ useAccessGroups, authorizers: [ { @@ -127,7 +131,10 @@ export async function handleNextcloudTalkInbound(params: { allowed: senderAllowedForCommands, }, ], + allowTextCommands, + hasControlCommand, }); + const commandAuthorized = commandGate.commandAuthorized; if (isGroup) { const groupAllow = resolveNextcloudTalkGroupAllow({ @@ -188,12 +195,7 @@ export async function handleNextcloudTalkInbound(params: { } } - if ( - isGroup && - allowTextCommands && - core.channel.text.hasControlCommand(rawBody, config as ClawdbotConfig) && - commandAuthorized !== true - ) { + if (isGroup && commandGate.shouldBlock) { runtime.log?.( `nextcloud-talk: drop control command from unauthorized sender ${senderId}`, ); @@ -212,10 +214,6 @@ export async function handleNextcloudTalkInbound(params: { wildcardConfig: roomMatch.wildcardConfig, }) : false; - const hasControlCommand = core.channel.text.hasControlCommand( - rawBody, - config as ClawdbotConfig, - ); const mentionGate = resolveNextcloudTalkMentionGate({ isGroup, requireMention: shouldRequireMention, diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 8db3831ef..f9126346e 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -41,7 +41,7 @@ import { } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { truncateUtf16Safe } from "../../utils.js"; -import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { resolveIMessageAccount } from "../accounts.js"; import { createIMessageRpcClient } from "../client.js"; import { probeIMessage } from "../probe.js"; @@ -372,25 +372,23 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P chatIdentifier, }) : false; - const commandAuthorized = isGroup - ? resolveCommandAuthorizedFromAuthorizers({ - useAccessGroups, - authorizers: [ - { configured: effectiveDmAllowFrom.length > 0, allowed: ownerAllowedForCommands }, - { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, - ], - }) - : dmAuthorized; - if (isGroup && hasControlCommand(messageText, cfg) && !commandAuthorized) { + const hasControlCommandInMessage = hasControlCommand(messageText, cfg); + const commandGate = resolveControlCommandGate({ + useAccessGroups, + authorizers: [ + { configured: effectiveDmAllowFrom.length > 0, allowed: ownerAllowedForCommands }, + { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, + ], + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, + }); + const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized; + if (isGroup && commandGate.shouldBlock) { logVerbose(`imessage: drop control command from unauthorized sender ${sender}`); return; } const shouldBypassMention = - isGroup && - requireMention && - !mentioned && - commandAuthorized && - hasControlCommand(messageText); + isGroup && requireMention && !mentioned && commandAuthorized && hasControlCommandInMessage; const effectiveWasMentioned = mentioned || shouldBypassMention; if (isGroup && requireMention && canDetectMention && !mentioned && !shouldBypassMention) { logVerbose(`imessage: skipping group message (no mention)`); diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 23f6582cb..9ee7015bf 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -134,6 +134,7 @@ export { createReplyPrefixContext } from "../channels/reply-prefix.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; export type { NormalizedLocation } from "../channels/location.js"; export { formatLocationText, toLocationContext } from "../channels/location.js"; +export { resolveControlCommandGate } from "../channels/command-gating.js"; export { resolveBlueBubblesGroupRequireMention, resolveDiscordGroupRequireMention, diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 944b66cd0..2c9105664 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -30,7 +30,7 @@ import { } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { normalizeE164 } from "../../utils.js"; -import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { formatSignalPairingIdLine, formatSignalSenderDisplay, @@ -439,16 +439,18 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const useAccessGroups = deps.cfg.commands?.useAccessGroups !== false; const ownerAllowedForCommands = isSignalSenderAllowed(sender, effectiveDmAllow); const groupAllowedForCommands = isSignalSenderAllowed(sender, effectiveGroupAllow); - const commandAuthorized = isGroup - ? resolveCommandAuthorizedFromAuthorizers({ - useAccessGroups, - authorizers: [ - { configured: effectiveDmAllow.length > 0, allowed: ownerAllowedForCommands }, - { configured: effectiveGroupAllow.length > 0, allowed: groupAllowedForCommands }, - ], - }) - : dmAllowed; - if (isGroup && hasControlCommand(messageText, deps.cfg) && !commandAuthorized) { + const hasControlCommandInMessage = hasControlCommand(messageText, deps.cfg); + const commandGate = resolveControlCommandGate({ + useAccessGroups, + authorizers: [ + { configured: effectiveDmAllow.length > 0, allowed: ownerAllowedForCommands }, + { configured: effectiveGroupAllow.length > 0, allowed: groupAllowedForCommands }, + ], + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, + }); + const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAllowed; + if (isGroup && commandGate.shouldBlock) { logVerbose(`signal: drop control command from unauthorized sender ${senderDisplay}`); return; }