refactor(extensions): use scoped pairing helper

This commit is contained in:
Peter Steinberger
2026-02-26 21:55:56 +01:00
parent 36b6ea1446
commit a0c5e28f3b
12 changed files with 135 additions and 32 deletions

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import {
DM_GROUP_ACCESS_REASON,
createScopedPairingAccess,
createReplyPrefixOptions,
evictOldHistoryKeys,
logAckFailure,
@@ -421,6 +422,11 @@ export async function processMessage(
target: WebhookTarget,
): Promise<void> {
const { account, config, runtime, core, statusSink } = target;
const pairing = createScopedPairingAccess({
core,
channel: "bluebubbles",
accountId: account.accountId,
});
const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId);
const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
@@ -505,8 +511,9 @@ export async function processMessage(
const configuredAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "bluebubbles",
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
});
const accessDecision = resolveDmGroupAccessWithLists({
isGroup,
@@ -587,8 +594,7 @@ export async function processMessage(
}
if (accessDecision.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "bluebubbles",
const { code, created } = await pairing.upsertPairingRequest({
id: message.senderId,
meta: { name: message.senderName },
});
@@ -1381,6 +1387,11 @@ export async function processReaction(
target: WebhookTarget,
): Promise<void> {
const { account, config, runtime, core } = target;
const pairing = createScopedPairingAccess({
core,
channel: "bluebubbles",
accountId: account.accountId,
});
if (reaction.fromMe) {
return;
}
@@ -1389,8 +1400,9 @@ export async function processReaction(
const groupPolicy = account.config.groupPolicy ?? "allowlist";
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "bluebubbles",
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
});
const accessDecision = resolveDmGroupAccessWithLists({
isGroup: reaction.isGroup,

View File

@@ -3,6 +3,7 @@ import {
buildAgentMediaPayload,
buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled,
createScopedPairingAccess,
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
recordPendingHistoryEntryIfEnabled,
@@ -675,6 +676,11 @@ export async function handleFeishuMessage(params: {
try {
const core = getFeishuRuntime();
const pairing = createScopedPairingAccess({
core,
channel: "feishu",
accountId: account.accountId,
});
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
ctx.content,
cfg,
@@ -683,7 +689,7 @@ export async function handleFeishuMessage(params: {
!isGroup &&
dmPolicy !== "allowlist" &&
(dmPolicy !== "open" || shouldComputeCommandAuthorized)
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
? await pairing.readAllowFromStore().catch(() => [])
: [];
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const dmAllowed = resolveFeishuAllowlistMatch({
@@ -695,8 +701,7 @@ export async function handleFeishuMessage(params: {
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "feishu",
const { code, created } = await pairing.upsertPairingRequest({
id: ctx.senderOpenId,
meta: { name: ctx.senderName },
});

View File

@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import {
GROUP_POLICY_BLOCKED_LABEL,
createScopedPairingAccess,
createReplyPrefixOptions,
readJsonBodyWithLimit,
registerWebhookTarget,
@@ -396,6 +397,11 @@ async function processMessageWithPipeline(params: {
mediaMaxMb: number;
}): Promise<void> {
const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
const pairing = createScopedPairingAccess({
core,
channel: "googlechat",
accountId: account.accountId,
});
const space = event.space;
const message = event.message;
if (!space || !message) {
@@ -514,7 +520,7 @@ async function processMessageWithPipeline(params: {
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
const storeAllowFrom =
!isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth)
? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
? await pairing.readAllowFromStore().catch(() => [])
: [];
const access = resolveDmGroupAccessWithLists({
isGroup,
@@ -590,8 +596,7 @@ async function processMessageWithPipeline(params: {
if (access.decision !== "allow") {
if (access.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "googlechat",
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName || undefined, email: senderEmail },
});

View File

@@ -1,5 +1,6 @@
import {
GROUP_POLICY_BLOCKED_LABEL,
createScopedPairingAccess,
createNormalizedOutboundDeliverer,
createReplyPrefixOptions,
formatTextWithAttachmentLinks,
@@ -90,6 +91,11 @@ export async function handleIrcInbound(params: {
}): Promise<void> {
const { message, account, config, runtime, connectedNick, statusSink } = params;
const core = getIrcRuntime();
const pairing = createScopedPairingAccess({
core,
channel: CHANNEL_ID,
accountId: account.accountId,
});
const rawBody = message.text?.trim() ?? "";
if (!rawBody) {
@@ -123,8 +129,9 @@ export async function handleIrcInbound(params: {
const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: CHANNEL_ID,
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
});
const storeAllowList = normalizeIrcAllowlist(storeAllowFrom);
@@ -202,8 +209,7 @@ export async function handleIrcInbound(params: {
}).allowed;
if (!dmAllowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: CHANNEL_ID,
const { code, created } = await pairing.upsertPairingRequest({
id: senderDisplay.toLowerCase(),
meta: { name: message.senderNick || undefined },
});

View File

@@ -1,5 +1,7 @@
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
import {
DEFAULT_ACCOUNT_ID,
createScopedPairingAccess,
createReplyPrefixOptions,
createTypingCallbacks,
formatAllowlistMatchMeta,
@@ -98,6 +100,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
getMemberDisplayName,
accountId,
} = params;
const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID;
const pairing = createScopedPairingAccess({
core,
channel: "matrix",
accountId: resolvedAccountId,
});
return async (roomId: string, event: MatrixRawEvent) => {
try {
@@ -229,8 +237,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const storeAllowFrom = isDirectMessage
? await readStoreAllowFromForDmPolicy({
provider: "matrix",
accountId: resolvedAccountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
})
: [];
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
@@ -270,8 +279,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
});
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
if (access.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "matrix",
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName },
});

View File

@@ -8,6 +8,7 @@ import type {
import {
buildAgentMediaPayload,
DM_GROUP_ACCESS_REASON,
createScopedPairingAccess,
createReplyPrefixOptions,
createTypingCallbacks,
logInboundDrop,
@@ -171,6 +172,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
cfg,
accountId: opts.accountId,
});
const pairing = createScopedPairingAccess({
core,
channel: "mattermost",
accountId: account.accountId,
});
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
const botToken = opts.botToken?.trim() || account.botToken?.trim();
if (!botToken) {
@@ -362,8 +368,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const storeAllowFrom = normalizeMattermostAllowList(
await readStoreAllowFromForDmPolicy({
provider: "mattermost",
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
}),
);
const accessDecision = resolveDmGroupAccessWithLists({
@@ -424,8 +431,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
return;
}
if (accessDecision.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "mattermost",
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName },
});
@@ -862,8 +868,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const storeAllowFrom = normalizeMattermostAllowList(
await readStoreAllowFromForDmPolicy({
provider: "mattermost",
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
}),
);
const reactionAccess = resolveDmGroupAccessWithLists({

View File

@@ -1,7 +1,9 @@
import {
DEFAULT_ACCOUNT_ID,
buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
createScopedPairingAccess,
logInboundDrop,
recordPendingHistoryEntryIfEnabled,
resolveControlCommandGate,
@@ -57,6 +59,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
log,
} = deps;
const core = getMSTeamsRuntime();
const pairing = createScopedPairingAccess({
core,
channel: "msteams",
accountId: DEFAULT_ACCOUNT_ID,
});
const logVerboseMessage = (message: string) => {
if (core.logging.shouldLogVerbose()) {
log.debug?.(message);
@@ -132,8 +139,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
const storedAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "msteams",
accountId: pairing.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
});
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
@@ -200,8 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
});
if (access.decision === "pairing") {
const request = await core.channel.pairing.upsertPairingRequest({
channel: "msteams",
const request = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName },
});

View File

@@ -1,5 +1,6 @@
import {
GROUP_POLICY_BLOCKED_LABEL,
createScopedPairingAccess,
createNormalizedOutboundDeliverer,
createReplyPrefixOptions,
formatTextWithAttachmentLinks,
@@ -58,6 +59,11 @@ export async function handleNextcloudTalkInbound(params: {
}): Promise<void> {
const { message, account, config, runtime, statusSink } = params;
const core = getNextcloudTalkRuntime();
const pairing = createScopedPairingAccess({
core,
channel: CHANNEL_ID,
accountId: account.accountId,
});
const rawBody = message.text?.trim() ?? "";
if (!rawBody) {
@@ -99,8 +105,9 @@ export async function handleNextcloudTalkInbound(params: {
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: CHANNEL_ID,
accountId: account.accountId,
dmPolicy,
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
readStore: pairing.readStoreForDmPolicy,
});
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
@@ -167,8 +174,7 @@ export async function handleNextcloudTalkInbound(params: {
} else {
if (access.decision !== "allow") {
if (access.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: CHANNEL_ID,
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName || undefined },
});

View File

@@ -1,6 +1,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
import {
createScopedPairingAccess,
createReplyPrefixOptions,
resolveSenderCommandAuthorization,
resolveOutboundMediaUrls,
@@ -303,6 +304,11 @@ async function processMessageWithPipeline(params: {
statusSink,
fetcher,
} = params;
const pairing = createScopedPairingAccess({
core,
channel: "zalo",
accountId: account.accountId,
});
const { from, chat, message_id, date } = message;
const isGroup = chat.chat_type === "GROUP";
@@ -358,7 +364,7 @@ async function processMessageWithPipeline(params: {
configuredGroupAllowFrom: groupAllowFrom,
senderId,
isSenderAllowed: isZaloSenderAllowed,
readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
readAllowFromStore: pairing.readAllowFromStore,
shouldComputeCommandAuthorized: (body, cfg) =>
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
resolveCommandAuthorizedFromAuthorizers: (params) =>
@@ -376,8 +382,7 @@ async function processMessageWithPipeline(params: {
if (!allowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "zalo",
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName ?? undefined },
});

View File

@@ -6,6 +6,7 @@ import type {
RuntimeEnv,
} from "openclaw/plugin-sdk";
import {
createScopedPairingAccess,
createReplyPrefixOptions,
resolveOutboundMediaUrls,
mergeAllowlist,
@@ -177,6 +178,11 @@ async function processMessage(
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
): Promise<void> {
const { threadId, content, timestamp, metadata } = message;
const pairing = createScopedPairingAccess({
core,
channel: "zalouser",
accountId: account.accountId,
});
if (!content?.trim()) {
return;
}
@@ -225,7 +231,7 @@ async function processMessage(
configuredAllowFrom: configAllowFrom,
senderId,
isSenderAllowed,
readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalouser"),
readAllowFromStore: pairing.readAllowFromStore,
shouldComputeCommandAuthorized: (body, cfg) =>
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
resolveCommandAuthorizedFromAuthorizers: (params) =>
@@ -243,8 +249,7 @@ async function processMessage(
if (!allowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "zalouser",
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName || undefined },
});

View File

@@ -216,6 +216,7 @@ export {
type SenderGroupAccessReason,
} from "./group-access.js";
export { resolveSenderCommandAuthorization } from "./command-auth.js";
export { createScopedPairingAccess } from "./pairing-access.js";
export { handleSlackMessageAction } from "./slack-message-actions.js";
export { extractToolSend } from "./tool-send.js";
export {

View File

@@ -0,0 +1,36 @@
import type { ChannelId } from "../channels/plugins/types.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { normalizeAccountId } from "../routing/session-key.js";
type PairingApi = PluginRuntime["channel"]["pairing"];
type ScopedUpsertInput = Omit<
Parameters<PairingApi["upsertPairingRequest"]>[0],
"channel" | "accountId"
>;
export function createScopedPairingAccess(params: {
core: PluginRuntime;
channel: ChannelId;
accountId: string;
}) {
const resolvedAccountId = normalizeAccountId(params.accountId);
return {
accountId: resolvedAccountId,
readAllowFromStore: () =>
params.core.channel.pairing.readAllowFromStore({
channel: params.channel,
accountId: resolvedAccountId,
}),
readStoreForDmPolicy: (provider: ChannelId, accountId: string) =>
params.core.channel.pairing.readAllowFromStore({
channel: provider,
accountId: normalizeAccountId(accountId),
}),
upsertPairingRequest: (input: ScopedUpsertInput) =>
params.core.channel.pairing.upsertPairingRequest({
channel: params.channel,
accountId: resolvedAccountId,
...input,
}),
};
}