mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:31:00 +00:00
refactor(discord): split component auth helpers
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
// Keep bundled registration fast: runtime wiring only needs the store setter,
|
||||
// while runtime-api.js remains the broad compatibility barrel.
|
||||
// while runtime-api.js remains the broad runtime surface.
|
||||
export { setDiscordRuntime } from "./src/runtime.js";
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
resolveEventCoverImage,
|
||||
} from "../send.js";
|
||||
import {
|
||||
createDiscordActionOptions,
|
||||
readDiscordChannelCreateParams,
|
||||
readDiscordChannelEditParams,
|
||||
readDiscordChannelMoveParams,
|
||||
@@ -70,7 +71,7 @@ type DiscordRoleMutation = (
|
||||
) => Promise<unknown>;
|
||||
|
||||
async function runRoleMutation(params: {
|
||||
cfgOptions: { cfg: OpenClawConfig };
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
values: Record<string, unknown>;
|
||||
mutate: DiscordRoleMutation;
|
||||
@@ -80,10 +81,7 @@ async function runRoleMutation(params: {
|
||||
const roleId = readStringParam(params.values, "roleId", { required: true });
|
||||
await params.mutate(
|
||||
{ guildId, userId, roleId },
|
||||
{
|
||||
...params.cfgOptions,
|
||||
...(params.accountId ? { accountId: params.accountId } : {}),
|
||||
},
|
||||
createDiscordActionOptions({ cfg: params.cfg, accountId: params.accountId }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,12 +103,8 @@ export async function handleDiscordGuildAction(
|
||||
if (!cfg) {
|
||||
throw new Error("Discord guild actions require a resolved runtime config.");
|
||||
}
|
||||
const cfgOptions = { cfg };
|
||||
const withOpts = (extra?: Record<string, unknown>) => ({
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...extra,
|
||||
});
|
||||
const withOpts = (extra?: Record<string, unknown>) =>
|
||||
createDiscordActionOptions({ cfg, accountId, extra });
|
||||
switch (action) {
|
||||
case "memberInfo": {
|
||||
if (!isActionEnabled("memberInfo")) {
|
||||
@@ -123,12 +117,11 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const effectiveAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
|
||||
const member = effectiveAccountId
|
||||
? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, {
|
||||
...cfgOptions,
|
||||
accountId: effectiveAccountId,
|
||||
})
|
||||
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, cfgOptions);
|
||||
const member = await discordGuildActionRuntime.fetchMemberInfoDiscord(
|
||||
guildId,
|
||||
userId,
|
||||
createDiscordActionOptions({ cfg, accountId: effectiveAccountId }),
|
||||
);
|
||||
const presence = getPresence(effectiveAccountId, userId);
|
||||
const activities = presence?.activities ?? undefined;
|
||||
const status = presence?.status ?? undefined;
|
||||
@@ -209,7 +202,7 @@ export async function handleDiscordGuildAction(
|
||||
throw new Error("Discord role changes are disabled.");
|
||||
}
|
||||
await runRoleMutation({
|
||||
cfgOptions,
|
||||
cfg,
|
||||
accountId,
|
||||
values: params,
|
||||
mutate: discordGuildActionRuntime.addRoleDiscord,
|
||||
@@ -221,7 +214,7 @@ export async function handleDiscordGuildAction(
|
||||
throw new Error("Discord role changes are disabled.");
|
||||
}
|
||||
await runRoleMutation({
|
||||
cfgOptions,
|
||||
cfg,
|
||||
accountId,
|
||||
values: params,
|
||||
mutate: discordGuildActionRuntime.removeRoleDiscord,
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type DiscordSendEmbeds,
|
||||
} from "../send.shared.js";
|
||||
import { resolveDiscordChannelId } from "../targets.js";
|
||||
import { createDiscordActionOptions } from "./runtime.shared.js";
|
||||
|
||||
export const discordMessagingActionRuntime = {
|
||||
createThreadDiscord,
|
||||
@@ -135,6 +136,8 @@ export async function handleDiscordMessagingAction(
|
||||
throw new Error("Discord messaging actions require a resolved runtime config.");
|
||||
}
|
||||
const cfgOptions = { cfg };
|
||||
const withOpts = (extra?: Record<string, unknown>) =>
|
||||
createDiscordActionOptions({ cfg, accountId, extra });
|
||||
const resolvedReactionAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
|
||||
const resolveReactionChannelId = async () => {
|
||||
const target =
|
||||
@@ -227,11 +230,7 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
label: "stickerIds",
|
||||
});
|
||||
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
content,
|
||||
});
|
||||
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, withOpts({ content }));
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "poll": {
|
||||
@@ -253,7 +252,7 @@ export async function handleDiscordMessagingAction(
|
||||
await discordMessagingActionRuntime.sendPollDiscord(
|
||||
to,
|
||||
{ question, options: answers, maxSelections, durationHours },
|
||||
{ ...cfgOptions, ...(accountId ? { accountId } : {}), content },
|
||||
withOpts({ content }),
|
||||
);
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@@ -262,12 +261,10 @@ export async function handleDiscordMessagingAction(
|
||||
throw new Error("Discord permissions are disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const permissions = accountId
|
||||
? await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, cfgOptions);
|
||||
const permissions = await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(
|
||||
channelId,
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({ ok: true, permissions });
|
||||
}
|
||||
case "fetchMessage": {
|
||||
@@ -289,12 +286,11 @@ export async function handleDiscordMessagingAction(
|
||||
"Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).",
|
||||
);
|
||||
}
|
||||
const message = accountId
|
||||
? await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, cfgOptions);
|
||||
const message = await discordMessagingActionRuntime.fetchMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
message: normalizeMessage(message),
|
||||
@@ -314,12 +310,11 @@ export async function handleDiscordMessagingAction(
|
||||
after: readStringParam(params, "after"),
|
||||
around: readStringParam(params, "around"),
|
||||
};
|
||||
const messages = accountId
|
||||
? await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, cfgOptions);
|
||||
const messages = await discordMessagingActionRuntime.readMessagesDiscord(
|
||||
channelId,
|
||||
query,
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
messages: messages.map((message) => normalizeMessage(message)),
|
||||
@@ -372,8 +367,7 @@ export async function handleDiscordMessagingAction(
|
||||
to,
|
||||
payload,
|
||||
{
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...withOpts(),
|
||||
silent,
|
||||
replyTo: replyTo ?? undefined,
|
||||
sessionKey: sessionKey ?? undefined,
|
||||
@@ -399,8 +393,7 @@ export async function handleDiscordMessagingAction(
|
||||
}
|
||||
assertMediaNotDataUrl(mediaUrl);
|
||||
const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...withOpts(),
|
||||
replyTo,
|
||||
silent,
|
||||
});
|
||||
@@ -408,8 +401,7 @@ export async function handleDiscordMessagingAction(
|
||||
}
|
||||
|
||||
const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...withOpts(),
|
||||
mediaUrl,
|
||||
filename: filename ?? undefined,
|
||||
mediaLocalRoots: options?.mediaLocalRoots,
|
||||
@@ -432,19 +424,12 @@ export async function handleDiscordMessagingAction(
|
||||
const content = readStringParam(params, "content", {
|
||||
required: true,
|
||||
});
|
||||
const message = accountId
|
||||
? await discordMessagingActionRuntime.editMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{ content },
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await discordMessagingActionRuntime.editMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{ content },
|
||||
cfgOptions,
|
||||
);
|
||||
const message = await discordMessagingActionRuntime.editMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{ content },
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({ ok: true, message });
|
||||
}
|
||||
case "deleteMessage": {
|
||||
@@ -455,14 +440,7 @@ export async function handleDiscordMessagingAction(
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, withOpts());
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "threadCreate": {
|
||||
@@ -482,12 +460,11 @@ export async function handleDiscordMessagingAction(
|
||||
content,
|
||||
appliedTags: appliedTags ?? undefined,
|
||||
};
|
||||
const thread = accountId
|
||||
? await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, cfgOptions);
|
||||
const thread = await discordMessagingActionRuntime.createThreadDiscord(
|
||||
channelId,
|
||||
payload,
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({ ok: true, thread });
|
||||
}
|
||||
case "threadList": {
|
||||
@@ -501,27 +478,16 @@ export async function handleDiscordMessagingAction(
|
||||
const includeArchived = readBooleanParam(params, "includeArchived");
|
||||
const before = readStringParam(params, "before");
|
||||
const limit = readNumberParam(params, "limit");
|
||||
const threads = accountId
|
||||
? await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
includeArchived,
|
||||
before,
|
||||
limit,
|
||||
},
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
includeArchived,
|
||||
before,
|
||||
limit,
|
||||
},
|
||||
cfgOptions,
|
||||
);
|
||||
const threads = await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
includeArchived,
|
||||
before,
|
||||
limit,
|
||||
},
|
||||
withOpts(),
|
||||
);
|
||||
return jsonResult({ ok: true, threads });
|
||||
}
|
||||
case "threadReply": {
|
||||
@@ -538,8 +504,7 @@ export async function handleDiscordMessagingAction(
|
||||
`channel:${channelId}`,
|
||||
content,
|
||||
{
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...withOpts(),
|
||||
mediaUrl,
|
||||
mediaLocalRoots: options?.mediaLocalRoots,
|
||||
mediaReadFile: options?.mediaReadFile,
|
||||
@@ -556,14 +521,7 @@ export async function handleDiscordMessagingAction(
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, withOpts());
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "unpinMessage": {
|
||||
@@ -574,14 +532,7 @@ export async function handleDiscordMessagingAction(
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, withOpts());
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "listPins": {
|
||||
@@ -589,12 +540,7 @@ export async function handleDiscordMessagingAction(
|
||||
throw new Error("Discord pins are disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const pins = accountId
|
||||
? await discordMessagingActionRuntime.listPinsDiscord(channelId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.listPinsDiscord(channelId, cfgOptions);
|
||||
const pins = await discordMessagingActionRuntime.listPinsDiscord(channelId, withOpts());
|
||||
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
|
||||
}
|
||||
case "searchMessages": {
|
||||
@@ -614,27 +560,16 @@ export async function handleDiscordMessagingAction(
|
||||
const limit = readNumberParam(params, "limit");
|
||||
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
|
||||
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
|
||||
const results = accountId
|
||||
? await discordMessagingActionRuntime.searchMessagesDiscord(
|
||||
{
|
||||
guildId,
|
||||
content,
|
||||
channelIds: channelIdList.length ? channelIdList : undefined,
|
||||
authorIds: authorIdList.length ? authorIdList : undefined,
|
||||
limit,
|
||||
},
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await discordMessagingActionRuntime.searchMessagesDiscord(
|
||||
{
|
||||
guildId,
|
||||
content,
|
||||
channelIds: channelIdList.length ? channelIdList : undefined,
|
||||
authorIds: authorIdList.length ? authorIdList : undefined,
|
||||
limit,
|
||||
},
|
||||
cfgOptions,
|
||||
);
|
||||
const results = await discordMessagingActionRuntime.searchMessagesDiscord(
|
||||
{
|
||||
guildId,
|
||||
content,
|
||||
channelIds: channelIdList.length ? channelIdList : undefined,
|
||||
authorIds: authorIdList.length ? authorIdList : undefined,
|
||||
limit,
|
||||
},
|
||||
withOpts(),
|
||||
);
|
||||
if (!results || typeof results !== "object") {
|
||||
return jsonResult({ ok: true, results });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
readDiscordModerationCommand,
|
||||
requiredGuildPermissionForModerationAction,
|
||||
} from "./runtime.moderation-shared.js";
|
||||
import { createDiscordActionOptions } from "./runtime.shared.js";
|
||||
|
||||
export const discordModerationActionRuntime = {
|
||||
banMemberDiscord,
|
||||
@@ -30,7 +31,7 @@ async function verifySenderModerationPermission(params: {
|
||||
senderUserId?: string;
|
||||
requiredPermission: bigint;
|
||||
accountId?: string;
|
||||
cfgOptions: { cfg: OpenClawConfig };
|
||||
cfg: OpenClawConfig;
|
||||
}) {
|
||||
// CLI/manual flows may not have sender context; enforce only when present.
|
||||
if (!params.senderUserId) {
|
||||
@@ -40,10 +41,7 @@ async function verifySenderModerationPermission(params: {
|
||||
params.guildId,
|
||||
params.senderUserId,
|
||||
[params.requiredPermission],
|
||||
{
|
||||
...params.cfgOptions,
|
||||
...(params.accountId ? { accountId: params.accountId } : {}),
|
||||
},
|
||||
createDiscordActionOptions({ cfg: params.cfg, accountId: params.accountId }),
|
||||
);
|
||||
if (!hasPermission) {
|
||||
throw new Error("Sender does not have required permissions for this moderation action.");
|
||||
@@ -65,20 +63,16 @@ export async function handleDiscordModerationAction(
|
||||
if (!cfg) {
|
||||
throw new Error("Discord moderation actions require a resolved runtime config.");
|
||||
}
|
||||
const cfgOptions = { cfg };
|
||||
const command = readDiscordModerationCommand(action, params);
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
const command = readDiscordModerationCommand(action, params);
|
||||
const senderUserId = readStringParam(params, "senderUserId");
|
||||
const withOpts = () => ({
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
});
|
||||
const withOpts = () => createDiscordActionOptions({ cfg, accountId });
|
||||
await verifySenderModerationPermission({
|
||||
guildId: command.guildId,
|
||||
senderUserId,
|
||||
requiredPermission: requiredGuildPermissionForModerationAction(command.action),
|
||||
accountId,
|
||||
cfgOptions,
|
||||
cfg,
|
||||
});
|
||||
switch (command.action) {
|
||||
case "timeout": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseAvailableTags, readNumberParam, readStringParam } from "../runtime-api.js";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import type {
|
||||
DiscordChannelCreate,
|
||||
DiscordChannelEdit,
|
||||
@@ -24,6 +25,20 @@ function readDiscordBooleanParam(
|
||||
return typeof params[key] === "boolean" ? params[key] : undefined;
|
||||
}
|
||||
|
||||
export function createDiscordActionOptions<
|
||||
T extends Record<string, unknown> = Record<string, never>,
|
||||
>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
extra?: T;
|
||||
}): { cfg: OpenClawConfig; accountId?: string } & T {
|
||||
return {
|
||||
cfg: params.cfg,
|
||||
...(params.accountId ? { accountId: params.accountId } : {}),
|
||||
...(params.extra ?? ({} as T)),
|
||||
};
|
||||
}
|
||||
|
||||
export function readDiscordChannelCreateParams(
|
||||
params: Record<string, unknown>,
|
||||
): DiscordChannelCreate {
|
||||
|
||||
@@ -1,514 +1,8 @@
|
||||
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import type { DiscordComponentEntry } from "../components.js";
|
||||
import {
|
||||
resolveComponentInteractionContext,
|
||||
resolveDiscordChannelContext,
|
||||
} from "./agent-components-context.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
upsertChannelPairingRequest,
|
||||
} from "./agent-components-helpers.runtime.js";
|
||||
import {
|
||||
type AgentComponentContext,
|
||||
type AgentComponentInteraction,
|
||||
type ComponentInteractionContext,
|
||||
type DiscordChannelContext,
|
||||
type DiscordUser,
|
||||
} from "./agent-components.types.js";
|
||||
import {
|
||||
isDiscordGroupAllowedByPolicy,
|
||||
normalizeDiscordAllowList,
|
||||
resolveDiscordAllowListMatch,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
resolveDiscordGuildEntry,
|
||||
resolveDiscordMemberAccessState,
|
||||
resolveDiscordOwnerAccess,
|
||||
resolveGroupDmAllow,
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
|
||||
async function replySilently(
|
||||
interaction: AgentComponentInteraction,
|
||||
params: { content: string; ephemeral?: boolean },
|
||||
) {
|
||||
try {
|
||||
await interaction.reply(params);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function ensureGuildComponentMemberAllowed(params: {
|
||||
interaction: AgentComponentInteraction;
|
||||
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
||||
channelId: string;
|
||||
rawGuildId: string | undefined;
|
||||
channelCtx: DiscordChannelContext;
|
||||
memberRoleIds: string[];
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
allowNameMatching: boolean;
|
||||
groupPolicy: "open" | "disabled" | "allowlist";
|
||||
}) {
|
||||
const {
|
||||
interaction,
|
||||
guildInfo,
|
||||
channelId,
|
||||
rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds,
|
||||
user,
|
||||
replyOpts,
|
||||
componentLabel,
|
||||
unauthorizedReply,
|
||||
} = params;
|
||||
|
||||
if (!rawGuildId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const replyUnauthorized = async () => {
|
||||
await replySilently(interaction, { content: unauthorizedReply, ...replyOpts });
|
||||
};
|
||||
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
parentId: channelCtx.parentId,
|
||||
parentName: channelCtx.parentName,
|
||||
parentSlug: channelCtx.parentSlug,
|
||||
scope: channelCtx.isThread ? "thread" : "channel",
|
||||
});
|
||||
|
||||
if (channelConfig?.enabled === false) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
if (
|
||||
!isDiscordGroupAllowedByPolicy({
|
||||
groupPolicy: params.groupPolicy,
|
||||
guildAllowlisted: Boolean(guildInfo),
|
||||
channelAllowlistConfigured,
|
||||
channelAllowed,
|
||||
})
|
||||
) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
if (channelConfig?.allowed === false) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
|
||||
const { memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
memberRoleIds,
|
||||
sender: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
if (memberAllowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureComponentUserAllowed(params: {
|
||||
entry: DiscordComponentEntry;
|
||||
interaction: AgentComponentInteraction;
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
allowNameMatching: boolean;
|
||||
}) {
|
||||
const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
|
||||
"discord:",
|
||||
"user:",
|
||||
"pk:",
|
||||
]);
|
||||
if (!allowList) {
|
||||
return true;
|
||||
}
|
||||
const match = resolveDiscordAllowListMatch({
|
||||
allowList,
|
||||
candidate: {
|
||||
id: params.user.id,
|
||||
name: params.user.username,
|
||||
tag: formatDiscordUserTag(params.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
if (match.allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(
|
||||
`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`,
|
||||
);
|
||||
await replySilently(params.interaction, {
|
||||
content: params.unauthorizedReply,
|
||||
...params.replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureAgentComponentInteractionAllowed(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelId: string;
|
||||
rawGuildId: string | undefined;
|
||||
memberRoleIds: string[];
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
}) {
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: params.rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
const memberAllowed = await ensureGuildComponentMemberAllowed({
|
||||
interaction: params.interaction,
|
||||
guildInfo,
|
||||
channelId: params.channelId,
|
||||
rawGuildId: params.rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds: params.memberRoleIds,
|
||||
user: params.user,
|
||||
replyOpts: params.replyOpts,
|
||||
componentLabel: params.componentLabel,
|
||||
unauthorizedReply: params.unauthorizedReply,
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||
groupPolicy: resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: params.ctx.cfg.channels?.discord !== undefined,
|
||||
groupPolicy: params.ctx.discordConfig?.groupPolicy,
|
||||
defaultGroupPolicy: params.ctx.cfg.channels?.defaults?.groupPolicy,
|
||||
}).groupPolicy,
|
||||
});
|
||||
if (!memberAllowed) {
|
||||
return null;
|
||||
}
|
||||
return { parentId: channelCtx.parentId };
|
||||
}
|
||||
|
||||
async function ensureDmComponentAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
user: DiscordUser;
|
||||
componentLabel: string;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
}) {
|
||||
const { ctx, interaction, user, componentLabel, replyOpts } = params;
|
||||
const allowFromPrefixes = ["discord:", "user:", "pk:"];
|
||||
const resolveAllowMatch = (entries: string[]) => {
|
||||
const allowList = normalizeDiscordAllowList(entries, allowFromPrefixes);
|
||||
return allowList
|
||||
? resolveDiscordAllowListMatch({
|
||||
allowList,
|
||||
candidate: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
tag: formatDiscordUserTag(user),
|
||||
},
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
|
||||
})
|
||||
: { allowed: false };
|
||||
};
|
||||
const dmPolicy = ctx.dmPolicy ?? "pairing";
|
||||
if (dmPolicy === "disabled") {
|
||||
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
|
||||
await replySilently(interaction, { content: "DM interactions are disabled.", ...replyOpts });
|
||||
return false;
|
||||
}
|
||||
if (dmPolicy === "allowlist") {
|
||||
const allowMatch = resolveAllowMatch(ctx.allowFrom ?? []);
|
||||
if (allowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const storeAllowFrom =
|
||||
dmPolicy === "open"
|
||||
? []
|
||||
: await readStoreAllowFromForDmPolicy({
|
||||
provider: "discord",
|
||||
accountId: ctx.accountId,
|
||||
dmPolicy,
|
||||
});
|
||||
const allowMatch = resolveAllowMatch([...(ctx.allowFrom ?? []), ...storeAllowFrom]);
|
||||
if (allowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dmPolicy === "pairing") {
|
||||
const pairingResult = await createChannelPairingChallengeIssuer({
|
||||
channel: "discord",
|
||||
upsertPairingRequest: async ({ id, meta }) => {
|
||||
return await upsertChannelPairingRequest({
|
||||
channel: "discord",
|
||||
id,
|
||||
accountId: ctx.accountId,
|
||||
meta,
|
||||
});
|
||||
},
|
||||
})({
|
||||
senderId: user.id,
|
||||
senderIdLine: `Your Discord user id: ${user.id}`,
|
||||
meta: {
|
||||
tag: formatDiscordUserTag(user),
|
||||
name: user.username,
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await interaction.reply({
|
||||
content: text,
|
||||
...replyOpts,
|
||||
});
|
||||
},
|
||||
});
|
||||
if (!pairingResult.created) {
|
||||
await replySilently(interaction, {
|
||||
content: "Pairing already requested. Ask the bot owner to approve your code.",
|
||||
...replyOpts,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
async function ensureGroupDmComponentAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelId: string;
|
||||
componentLabel: string;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
}) {
|
||||
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
|
||||
const groupDmEnabled = ctx.discordConfig?.dm?.groupEnabled ?? false;
|
||||
if (!groupDmEnabled) {
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
|
||||
await replySilently(interaction, {
|
||||
content: "Group DM interactions are disabled.",
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelCtx = resolveDiscordChannelContext(interaction);
|
||||
const allowed = resolveGroupDmAllow({
|
||||
channels: ctx.discordConfig?.dm?.groupChannels,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
});
|
||||
if (allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveInteractionContextWithDmAuth(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
label: string;
|
||||
componentLabel: string;
|
||||
defer?: boolean;
|
||||
}) {
|
||||
const interactionCtx = await resolveComponentInteractionContext({
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
defer: params.defer,
|
||||
});
|
||||
if (!interactionCtx) {
|
||||
return null;
|
||||
}
|
||||
if (interactionCtx.isDirectMessage) {
|
||||
const authorized = await ensureDmComponentAuthorized({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
user: interactionCtx.user,
|
||||
componentLabel: params.componentLabel,
|
||||
replyOpts: interactionCtx.replyOpts,
|
||||
});
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (interactionCtx.isGroupDm) {
|
||||
const authorized = await ensureGroupDmComponentAuthorized({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
channelId: interactionCtx.channelId,
|
||||
componentLabel: params.componentLabel,
|
||||
replyOpts: interactionCtx.replyOpts,
|
||||
});
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return interactionCtx;
|
||||
}
|
||||
|
||||
export async function resolveAuthorizedComponentInteraction(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
label: string;
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
defer?: boolean;
|
||||
}) {
|
||||
const interactionCtx = await resolveInteractionContextWithDmAuth({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
componentLabel: params.componentLabel,
|
||||
defer: params.defer,
|
||||
});
|
||||
if (!interactionCtx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(params.ctx.discordConfig);
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
parentId: channelCtx.parentId,
|
||||
parentName: channelCtx.parentName,
|
||||
parentSlug: channelCtx.parentSlug,
|
||||
scope: channelCtx.isThread ? "thread" : "channel",
|
||||
});
|
||||
const memberAllowed = await ensureGuildComponentMemberAllowed({
|
||||
interaction: params.interaction,
|
||||
guildInfo,
|
||||
channelId,
|
||||
rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds,
|
||||
user,
|
||||
replyOpts,
|
||||
componentLabel: params.componentLabel,
|
||||
unauthorizedReply: params.unauthorizedReply,
|
||||
allowNameMatching,
|
||||
groupPolicy: resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: params.ctx.cfg.channels?.discord !== undefined,
|
||||
groupPolicy: params.ctx.discordConfig?.groupPolicy,
|
||||
defaultGroupPolicy: params.ctx.cfg.channels?.defaults?.groupPolicy,
|
||||
}).groupPolicy,
|
||||
});
|
||||
if (!memberAllowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commandAuthorized = resolveComponentCommandAuthorized({
|
||||
ctx: params.ctx,
|
||||
interactionCtx,
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
allowNameMatching,
|
||||
});
|
||||
|
||||
return {
|
||||
interactionCtx,
|
||||
channelCtx,
|
||||
guildInfo,
|
||||
channelConfig,
|
||||
allowNameMatching,
|
||||
commandAuthorized,
|
||||
user,
|
||||
replyOpts,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveComponentCommandAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interactionCtx: ComponentInteractionContext;
|
||||
channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
|
||||
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
||||
allowNameMatching: boolean;
|
||||
}) {
|
||||
const { ctx, interactionCtx, channelConfig, guildInfo } = params;
|
||||
if (interactionCtx.isDirectMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
|
||||
allowFrom: ctx.allowFrom,
|
||||
sender: {
|
||||
id: interactionCtx.user.id,
|
||||
name: interactionCtx.user.username,
|
||||
tag: formatDiscordUserTag(interactionCtx.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
|
||||
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
memberRoleIds: interactionCtx.memberRoleIds,
|
||||
sender: {
|
||||
id: interactionCtx.user.id,
|
||||
name: interactionCtx.user.username,
|
||||
tag: formatDiscordUserTag(interactionCtx.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false;
|
||||
const authorizers = useAccessGroups
|
||||
? [
|
||||
{ configured: ownerAllowList != null, allowed: ownerOk },
|
||||
{ configured: hasAccessRestrictions, allowed: memberAllowed },
|
||||
]
|
||||
: [{ configured: hasAccessRestrictions, allowed: memberAllowed }];
|
||||
|
||||
return resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers,
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
}
|
||||
export { resolveInteractionContextWithDmAuth } from "./agent-components-dm-auth.js";
|
||||
export {
|
||||
ensureAgentComponentInteractionAllowed,
|
||||
ensureComponentUserAllowed,
|
||||
ensureGuildComponentMemberAllowed,
|
||||
resolveAuthorizedComponentInteraction,
|
||||
resolveComponentCommandAuthorized,
|
||||
} from "./agent-components-guild-auth.js";
|
||||
|
||||
199
extensions/discord/src/monitor/agent-components-dm-auth.ts
Normal file
199
extensions/discord/src/monitor/agent-components-dm-auth.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
resolveComponentInteractionContext,
|
||||
resolveDiscordChannelContext,
|
||||
} from "./agent-components-context.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
upsertChannelPairingRequest,
|
||||
} from "./agent-components-helpers.runtime.js";
|
||||
import { replySilently } from "./agent-components-reply.js";
|
||||
import type {
|
||||
AgentComponentContext,
|
||||
AgentComponentInteraction,
|
||||
DiscordUser,
|
||||
} from "./agent-components.types.js";
|
||||
import {
|
||||
normalizeDiscordAllowList,
|
||||
resolveDiscordAllowListMatch,
|
||||
resolveGroupDmAllow,
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
|
||||
async function ensureDmComponentAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
user: DiscordUser;
|
||||
componentLabel: string;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
}) {
|
||||
const { ctx, interaction, user, componentLabel, replyOpts } = params;
|
||||
const allowFromPrefixes = ["discord:", "user:", "pk:"];
|
||||
const resolveAllowMatch = (entries: string[]) => {
|
||||
const allowList = normalizeDiscordAllowList(entries, allowFromPrefixes);
|
||||
return allowList
|
||||
? resolveDiscordAllowListMatch({
|
||||
allowList,
|
||||
candidate: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
tag: formatDiscordUserTag(user),
|
||||
},
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
|
||||
})
|
||||
: { allowed: false };
|
||||
};
|
||||
const dmPolicy = ctx.dmPolicy ?? "pairing";
|
||||
if (dmPolicy === "disabled") {
|
||||
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
|
||||
await replySilently(interaction, { content: "DM interactions are disabled.", ...replyOpts });
|
||||
return false;
|
||||
}
|
||||
if (dmPolicy === "allowlist") {
|
||||
const allowMatch = resolveAllowMatch(ctx.allowFrom ?? []);
|
||||
if (allowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const storeAllowFrom =
|
||||
dmPolicy === "open"
|
||||
? []
|
||||
: await readStoreAllowFromForDmPolicy({
|
||||
provider: "discord",
|
||||
accountId: ctx.accountId,
|
||||
dmPolicy,
|
||||
});
|
||||
const allowMatch = resolveAllowMatch([...(ctx.allowFrom ?? []), ...storeAllowFrom]);
|
||||
if (allowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dmPolicy === "pairing") {
|
||||
const pairingResult = await createChannelPairingChallengeIssuer({
|
||||
channel: "discord",
|
||||
upsertPairingRequest: async ({ id, meta }) => {
|
||||
return await upsertChannelPairingRequest({
|
||||
channel: "discord",
|
||||
id,
|
||||
accountId: ctx.accountId,
|
||||
meta,
|
||||
});
|
||||
},
|
||||
})({
|
||||
senderId: user.id,
|
||||
senderIdLine: `Your Discord user id: ${user.id}`,
|
||||
meta: {
|
||||
tag: formatDiscordUserTag(user),
|
||||
name: user.username,
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await interaction.reply({
|
||||
content: text,
|
||||
...replyOpts,
|
||||
});
|
||||
},
|
||||
});
|
||||
if (!pairingResult.created) {
|
||||
await replySilently(interaction, {
|
||||
content: "Pairing already requested. Ask the bot owner to approve your code.",
|
||||
...replyOpts,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
async function ensureGroupDmComponentAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelId: string;
|
||||
componentLabel: string;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
}) {
|
||||
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
|
||||
const groupDmEnabled = ctx.discordConfig?.dm?.groupEnabled ?? false;
|
||||
if (!groupDmEnabled) {
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
|
||||
await replySilently(interaction, {
|
||||
content: "Group DM interactions are disabled.",
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelCtx = resolveDiscordChannelContext(interaction);
|
||||
const allowed = resolveGroupDmAllow({
|
||||
channels: ctx.discordConfig?.dm?.groupChannels,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
});
|
||||
if (allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
|
||||
await replySilently(interaction, {
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveInteractionContextWithDmAuth(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
label: string;
|
||||
componentLabel: string;
|
||||
defer?: boolean;
|
||||
}) {
|
||||
const interactionCtx = await resolveComponentInteractionContext({
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
defer: params.defer,
|
||||
});
|
||||
if (!interactionCtx) {
|
||||
return null;
|
||||
}
|
||||
if (interactionCtx.isDirectMessage) {
|
||||
const authorized = await ensureDmComponentAuthorized({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
user: interactionCtx.user,
|
||||
componentLabel: params.componentLabel,
|
||||
replyOpts: interactionCtx.replyOpts,
|
||||
});
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (interactionCtx.isGroupDm) {
|
||||
const authorized = await ensureGroupDmComponentAuthorized({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
channelId: interactionCtx.channelId,
|
||||
componentLabel: params.componentLabel,
|
||||
replyOpts: interactionCtx.replyOpts,
|
||||
});
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return interactionCtx;
|
||||
}
|
||||
322
extensions/discord/src/monitor/agent-components-guild-auth.ts
Normal file
322
extensions/discord/src/monitor/agent-components-guild-auth.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import type { DiscordComponentEntry } from "../components.js";
|
||||
import { resolveDiscordChannelContext } from "./agent-components-context.js";
|
||||
import { resolveInteractionContextWithDmAuth } from "./agent-components-dm-auth.js";
|
||||
import { replySilently } from "./agent-components-reply.js";
|
||||
import type {
|
||||
AgentComponentContext,
|
||||
AgentComponentInteraction,
|
||||
ComponentInteractionContext,
|
||||
DiscordChannelContext,
|
||||
DiscordUser,
|
||||
} from "./agent-components.types.js";
|
||||
import {
|
||||
isDiscordGroupAllowedByPolicy,
|
||||
normalizeDiscordAllowList,
|
||||
resolveDiscordAllowListMatch,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
resolveDiscordGuildEntry,
|
||||
resolveDiscordMemberAccessState,
|
||||
resolveDiscordOwnerAccess,
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
|
||||
function resolveComponentRuntimeGroupPolicy(ctx: AgentComponentContext) {
|
||||
return resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: ctx.cfg.channels?.discord !== undefined,
|
||||
groupPolicy: ctx.discordConfig?.groupPolicy,
|
||||
defaultGroupPolicy: ctx.cfg.channels?.defaults?.groupPolicy,
|
||||
}).groupPolicy;
|
||||
}
|
||||
|
||||
export async function ensureGuildComponentMemberAllowed(params: {
|
||||
interaction: AgentComponentInteraction;
|
||||
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
||||
channelId: string;
|
||||
rawGuildId: string | undefined;
|
||||
channelCtx: DiscordChannelContext;
|
||||
memberRoleIds: string[];
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
allowNameMatching: boolean;
|
||||
groupPolicy: "open" | "disabled" | "allowlist";
|
||||
}) {
|
||||
const {
|
||||
interaction,
|
||||
guildInfo,
|
||||
channelId,
|
||||
rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds,
|
||||
user,
|
||||
replyOpts,
|
||||
componentLabel,
|
||||
unauthorizedReply,
|
||||
} = params;
|
||||
|
||||
if (!rawGuildId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const replyUnauthorized = async () => {
|
||||
await replySilently(interaction, { content: unauthorizedReply, ...replyOpts });
|
||||
};
|
||||
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
parentId: channelCtx.parentId,
|
||||
parentName: channelCtx.parentName,
|
||||
parentSlug: channelCtx.parentSlug,
|
||||
scope: channelCtx.isThread ? "thread" : "channel",
|
||||
});
|
||||
|
||||
if (channelConfig?.enabled === false) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
if (
|
||||
!isDiscordGroupAllowedByPolicy({
|
||||
groupPolicy: params.groupPolicy,
|
||||
guildAllowlisted: Boolean(guildInfo),
|
||||
channelAllowlistConfigured,
|
||||
channelAllowed,
|
||||
})
|
||||
) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
if (channelConfig?.allowed === false) {
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
|
||||
const { memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
memberRoleIds,
|
||||
sender: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
if (memberAllowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
|
||||
await replyUnauthorized();
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureComponentUserAllowed(params: {
|
||||
entry: DiscordComponentEntry;
|
||||
interaction: AgentComponentInteraction;
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
allowNameMatching: boolean;
|
||||
}) {
|
||||
const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
|
||||
"discord:",
|
||||
"user:",
|
||||
"pk:",
|
||||
]);
|
||||
if (!allowList) {
|
||||
return true;
|
||||
}
|
||||
const match = resolveDiscordAllowListMatch({
|
||||
allowList,
|
||||
candidate: {
|
||||
id: params.user.id,
|
||||
name: params.user.username,
|
||||
tag: formatDiscordUserTag(params.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
if (match.allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(
|
||||
`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`,
|
||||
);
|
||||
await replySilently(params.interaction, {
|
||||
content: params.unauthorizedReply,
|
||||
...params.replyOpts,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureAgentComponentInteractionAllowed(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelId: string;
|
||||
rawGuildId: string | undefined;
|
||||
memberRoleIds: string[];
|
||||
user: DiscordUser;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
}) {
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: params.rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
const memberAllowed = await ensureGuildComponentMemberAllowed({
|
||||
interaction: params.interaction,
|
||||
guildInfo,
|
||||
channelId: params.channelId,
|
||||
rawGuildId: params.rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds: params.memberRoleIds,
|
||||
user: params.user,
|
||||
replyOpts: params.replyOpts,
|
||||
componentLabel: params.componentLabel,
|
||||
unauthorizedReply: params.unauthorizedReply,
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx),
|
||||
});
|
||||
if (!memberAllowed) {
|
||||
return null;
|
||||
}
|
||||
return { parentId: channelCtx.parentId };
|
||||
}
|
||||
|
||||
export async function resolveAuthorizedComponentInteraction(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
label: string;
|
||||
componentLabel: string;
|
||||
unauthorizedReply: string;
|
||||
defer?: boolean;
|
||||
}) {
|
||||
const interactionCtx = await resolveInteractionContextWithDmAuth({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
componentLabel: params.componentLabel,
|
||||
defer: params.defer,
|
||||
});
|
||||
if (!interactionCtx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(params.ctx.discordConfig);
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
parentId: channelCtx.parentId,
|
||||
parentName: channelCtx.parentName,
|
||||
parentSlug: channelCtx.parentSlug,
|
||||
scope: channelCtx.isThread ? "thread" : "channel",
|
||||
});
|
||||
const memberAllowed = await ensureGuildComponentMemberAllowed({
|
||||
interaction: params.interaction,
|
||||
guildInfo,
|
||||
channelId,
|
||||
rawGuildId,
|
||||
channelCtx,
|
||||
memberRoleIds,
|
||||
user,
|
||||
replyOpts,
|
||||
componentLabel: params.componentLabel,
|
||||
unauthorizedReply: params.unauthorizedReply,
|
||||
allowNameMatching,
|
||||
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx),
|
||||
});
|
||||
if (!memberAllowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commandAuthorized = resolveComponentCommandAuthorized({
|
||||
ctx: params.ctx,
|
||||
interactionCtx,
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
allowNameMatching,
|
||||
});
|
||||
|
||||
return {
|
||||
interactionCtx,
|
||||
channelCtx,
|
||||
guildInfo,
|
||||
channelConfig,
|
||||
allowNameMatching,
|
||||
commandAuthorized,
|
||||
user,
|
||||
replyOpts,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveComponentCommandAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interactionCtx: ComponentInteractionContext;
|
||||
channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
|
||||
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
||||
allowNameMatching: boolean;
|
||||
}) {
|
||||
const { ctx, interactionCtx, channelConfig, guildInfo } = params;
|
||||
if (interactionCtx.isDirectMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
|
||||
allowFrom: ctx.allowFrom,
|
||||
sender: {
|
||||
id: interactionCtx.user.id,
|
||||
name: interactionCtx.user.username,
|
||||
tag: formatDiscordUserTag(interactionCtx.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
|
||||
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
memberRoleIds: interactionCtx.memberRoleIds,
|
||||
sender: {
|
||||
id: interactionCtx.user.id,
|
||||
name: interactionCtx.user.username,
|
||||
tag: formatDiscordUserTag(interactionCtx.user),
|
||||
},
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false;
|
||||
const authorizers = useAccessGroups
|
||||
? [
|
||||
{ configured: ownerAllowList != null, allowed: ownerOk },
|
||||
{ configured: hasAccessRestrictions, allowed: memberAllowed },
|
||||
]
|
||||
: [{ configured: hasAccessRestrictions, allowed: memberAllowed }];
|
||||
|
||||
return resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers,
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
}
|
||||
10
extensions/discord/src/monitor/agent-components-reply.ts
Normal file
10
extensions/discord/src/monitor/agent-components-reply.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { AgentComponentInteraction } from "./agent-components.types.js";
|
||||
|
||||
export async function replySilently(
|
||||
interaction: AgentComponentInteraction,
|
||||
params: { content: string; ephemeral?: boolean },
|
||||
) {
|
||||
try {
|
||||
await interaction.reply(params);
|
||||
} catch {}
|
||||
}
|
||||
Reference in New Issue
Block a user