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