fix(discord): thread runtime config through guild actions

This commit is contained in:
Claw Kowalski
2026-04-22 10:30:48 -04:00
committed by Peter Steinberger
parent e9d16cbd8c
commit 43366cd541
5 changed files with 356 additions and 386 deletions

View File

@@ -56,33 +56,32 @@ export const discordGuildActionRuntime = {
uploadStickerDiscord,
};
type DiscordRoleMutation = (params: {
guildId: string;
userId: string;
roleId: string;
}) => Promise<unknown>;
type DiscordRoleMutationWithAccount = (
type DiscordRoleMutationOpts = { cfg: OpenClawConfig; accountId?: string };
type DiscordRoleMutation = (
params: {
guildId: string;
userId: string;
roleId: string;
},
options: { accountId: string },
options: DiscordRoleMutationOpts,
) => Promise<unknown>;
async function runRoleMutation(params: {
cfgOptions: { cfg: OpenClawConfig };
accountId?: string;
values: Record<string, unknown>;
mutate: DiscordRoleMutation & DiscordRoleMutationWithAccount;
mutate: DiscordRoleMutation;
}) {
const guildId = readStringParam(params.values, "guildId", { required: true });
const userId = readStringParam(params.values, "userId", { required: true });
const roleId = readStringParam(params.values, "roleId", { required: true });
if (params.accountId) {
await params.mutate({ guildId, userId, roleId }, { accountId: params.accountId });
return;
}
await params.mutate({ guildId, userId, roleId });
await params.mutate(
{ guildId, userId, roleId },
{
...params.cfgOptions,
...(params.accountId ? { accountId: params.accountId } : {}),
},
);
}
function readChannelPermissionTarget(params: Record<string, unknown>) {
@@ -100,6 +99,15 @@ export async function handleDiscordGuildAction(
options?: { mediaLocalRoots?: readonly string[] },
): Promise<AgentToolResult<unknown>> {
const accountId = readStringParam(params, "accountId");
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,
});
switch (action) {
case "memberInfo": {
if (!isActionEnabled("memberInfo")) {
@@ -111,13 +119,13 @@ export async function handleDiscordGuildAction(
const userId = readStringParam(params, "userId", {
required: true,
});
const effectiveAccountId =
accountId ?? (cfg ? resolveDefaultDiscordAccountId(cfg) : undefined);
const effectiveAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
const member = effectiveAccountId
? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, {
...cfgOptions,
accountId: effectiveAccountId,
})
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId);
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, cfgOptions);
const presence = getPresence(effectiveAccountId, userId);
const activities = presence?.activities ?? undefined;
const status = presence?.status ?? undefined;
@@ -130,9 +138,7 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", {
required: true,
});
const roles = accountId
? await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, { accountId })
: await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId);
const roles = await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, withOpts());
return jsonResult({ ok: true, roles });
}
case "emojiList": {
@@ -142,9 +148,7 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", {
required: true,
});
const emojis = accountId
? await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listGuildEmojisDiscord(guildId);
const emojis = await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, withOpts());
return jsonResult({ ok: true, emojis });
}
case "emojiUpload": {
@@ -159,22 +163,15 @@ export async function handleDiscordGuildAction(
required: true,
});
const roleIds = readStringArrayParam(params, "roleIds");
const emoji = accountId
? await discordGuildActionRuntime.uploadEmojiDiscord(
{
guildId,
name,
mediaUrl,
roleIds: roleIds?.length ? roleIds : undefined,
},
{ accountId },
)
: await discordGuildActionRuntime.uploadEmojiDiscord({
guildId,
name,
mediaUrl,
roleIds: roleIds?.length ? roleIds : undefined,
});
const emoji = await discordGuildActionRuntime.uploadEmojiDiscord(
{
guildId,
name,
mediaUrl,
roleIds: roleIds?.length ? roleIds : undefined,
},
withOpts(),
);
return jsonResult({ ok: true, emoji });
}
case "stickerUpload": {
@@ -192,24 +189,16 @@ export async function handleDiscordGuildAction(
const mediaUrl = readStringParam(params, "mediaUrl", {
required: true,
});
const sticker = accountId
? await discordGuildActionRuntime.uploadStickerDiscord(
{
guildId,
name,
description,
tags,
mediaUrl,
},
{ accountId },
)
: await discordGuildActionRuntime.uploadStickerDiscord({
guildId,
name,
description,
tags,
mediaUrl,
});
const sticker = await discordGuildActionRuntime.uploadStickerDiscord(
{
guildId,
name,
description,
tags,
mediaUrl,
},
withOpts(),
);
return jsonResult({ ok: true, sticker });
}
case "roleAdd": {
@@ -217,6 +206,7 @@ export async function handleDiscordGuildAction(
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({
cfgOptions,
accountId,
values: params,
mutate: discordGuildActionRuntime.addRoleDiscord,
@@ -228,6 +218,7 @@ export async function handleDiscordGuildAction(
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({
cfgOptions,
accountId,
values: params,
mutate: discordGuildActionRuntime.removeRoleDiscord,
@@ -241,9 +232,10 @@ export async function handleDiscordGuildAction(
const channelId = readStringParam(params, "channelId", {
required: true,
});
const channel = accountId
? await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId, { accountId })
: await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId);
const channel = await discordGuildActionRuntime.fetchChannelInfoDiscord(
channelId,
withOpts(),
);
return jsonResult({ ok: true, channel });
}
case "channelList": {
@@ -253,9 +245,10 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", {
required: true,
});
const channels = accountId
? await discordGuildActionRuntime.listGuildChannelsDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listGuildChannelsDiscord(guildId);
const channels = await discordGuildActionRuntime.listGuildChannelsDiscord(
guildId,
withOpts(),
);
return jsonResult({ ok: true, channels });
}
case "voiceStatus": {
@@ -268,11 +261,11 @@ export async function handleDiscordGuildAction(
const userId = readStringParam(params, "userId", {
required: true,
});
const voice = accountId
? await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId, {
accountId,
})
: await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId);
const voice = await discordGuildActionRuntime.fetchVoiceStatusDiscord(
guildId,
userId,
withOpts(),
);
return jsonResult({ ok: true, voice });
}
case "eventList": {
@@ -282,9 +275,10 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", {
required: true,
});
const events = accountId
? await discordGuildActionRuntime.listScheduledEventsDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listScheduledEventsDiscord(guildId);
const events = await discordGuildActionRuntime.listScheduledEventsDiscord(
guildId,
withOpts(),
);
return jsonResult({ ok: true, events });
}
case "eventCreate": {
@@ -321,11 +315,11 @@ export async function handleDiscordGuildAction(
image,
privacy_level: 2,
};
const event = accountId
? await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload, {
accountId,
})
: await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload);
const event = await discordGuildActionRuntime.createScheduledEventDiscord(
guildId,
payload,
withOpts(),
);
return jsonResult({ ok: true, event });
}
case "channelCreate": {
@@ -339,28 +333,18 @@ export async function handleDiscordGuildAction(
const topic = readStringParam(params, "topic");
const position = readNumberParam(params, "position", { integer: true });
const nsfw = params.nsfw as boolean | undefined;
const channel = accountId
? await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
type: type ?? undefined,
parentId: parentId ?? undefined,
topic: topic ?? undefined,
position: position ?? undefined,
nsfw,
},
{ accountId },
)
: await discordGuildActionRuntime.createChannelDiscord({
guildId,
name,
type: type ?? undefined,
parentId: parentId ?? undefined,
topic: topic ?? undefined,
position: position ?? undefined,
nsfw,
});
const channel = await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
type: type ?? undefined,
parentId: parentId ?? undefined,
topic: topic ?? undefined,
position: position ?? undefined,
nsfw,
},
withOpts(),
);
return jsonResult({ ok: true, channel });
}
case "channelEdit": {
@@ -397,9 +381,7 @@ export async function handleDiscordGuildAction(
autoArchiveDuration: autoArchiveDuration ?? undefined,
availableTags,
};
const channel = accountId
? await discordGuildActionRuntime.editChannelDiscord(editPayload, { accountId })
: await discordGuildActionRuntime.editChannelDiscord(editPayload);
const channel = await discordGuildActionRuntime.editChannelDiscord(editPayload, withOpts());
return jsonResult({ ok: true, channel });
}
case "channelDelete": {
@@ -409,9 +391,7 @@ export async function handleDiscordGuildAction(
const channelId = readStringParam(params, "channelId", {
required: true,
});
const result = accountId
? await discordGuildActionRuntime.deleteChannelDiscord(channelId, { accountId })
: await discordGuildActionRuntime.deleteChannelDiscord(channelId);
const result = await discordGuildActionRuntime.deleteChannelDiscord(channelId, withOpts());
return jsonResult(result);
}
case "channelMove": {
@@ -424,24 +404,15 @@ export async function handleDiscordGuildAction(
});
const parentId = readDiscordParentIdParam(params);
const position = readNumberParam(params, "position", { integer: true });
if (accountId) {
await discordGuildActionRuntime.moveChannelDiscord(
{
guildId,
channelId,
parentId,
position: position ?? undefined,
},
{ accountId },
);
} else {
await discordGuildActionRuntime.moveChannelDiscord({
await discordGuildActionRuntime.moveChannelDiscord(
{
guildId,
channelId,
parentId,
position: position ?? undefined,
});
}
},
withOpts(),
);
return jsonResult({ ok: true });
}
case "categoryCreate": {
@@ -451,22 +422,15 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { required: true });
const name = readStringParam(params, "name", { required: true });
const position = readNumberParam(params, "position", { integer: true });
const channel = accountId
? await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
type: 4,
position: position ?? undefined,
},
{ accountId },
)
: await discordGuildActionRuntime.createChannelDiscord({
guildId,
name,
type: 4,
position: position ?? undefined,
});
const channel = await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
type: 4,
position: position ?? undefined,
},
withOpts(),
);
return jsonResult({ ok: true, category: channel });
}
case "categoryEdit": {
@@ -478,20 +442,14 @@ export async function handleDiscordGuildAction(
});
const name = readStringParam(params, "name");
const position = readNumberParam(params, "position", { integer: true });
const channel = accountId
? await discordGuildActionRuntime.editChannelDiscord(
{
channelId: categoryId,
name: name ?? undefined,
position: position ?? undefined,
},
{ accountId },
)
: await discordGuildActionRuntime.editChannelDiscord({
channelId: categoryId,
name: name ?? undefined,
position: position ?? undefined,
});
const channel = await discordGuildActionRuntime.editChannelDiscord(
{
channelId: categoryId,
name: name ?? undefined,
position: position ?? undefined,
},
withOpts(),
);
return jsonResult({ ok: true, category: channel });
}
case "categoryDelete": {
@@ -501,9 +459,7 @@ export async function handleDiscordGuildAction(
const categoryId = readStringParam(params, "categoryId", {
required: true,
});
const result = accountId
? await discordGuildActionRuntime.deleteChannelDiscord(categoryId, { accountId })
: await discordGuildActionRuntime.deleteChannelDiscord(categoryId);
const result = await discordGuildActionRuntime.deleteChannelDiscord(categoryId, withOpts());
return jsonResult(result);
}
case "channelPermissionSet": {
@@ -517,26 +473,16 @@ export async function handleDiscordGuildAction(
const targetType = targetTypeRaw === "member" ? 1 : 0;
const allow = readStringParam(params, "allow");
const deny = readStringParam(params, "deny");
if (accountId) {
await discordGuildActionRuntime.setChannelPermissionDiscord(
{
channelId,
targetId,
targetType,
allow: allow ?? undefined,
deny: deny ?? undefined,
},
{ accountId },
);
} else {
await discordGuildActionRuntime.setChannelPermissionDiscord({
await discordGuildActionRuntime.setChannelPermissionDiscord(
{
channelId,
targetId,
targetType,
allow: allow ?? undefined,
deny: deny ?? undefined,
});
}
},
withOpts(),
);
return jsonResult({ ok: true });
}
case "channelPermissionRemove": {
@@ -544,13 +490,11 @@ export async function handleDiscordGuildAction(
throw new Error("Discord channel management is disabled.");
}
const { channelId, targetId } = readChannelPermissionTarget(params);
if (accountId) {
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId, {
accountId,
});
} else {
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId);
}
await discordGuildActionRuntime.removeChannelPermissionDiscord(
channelId,
targetId,
withOpts(),
);
return jsonResult({ ok: true });
}
default:

View File

@@ -1,5 +1,5 @@
import { PermissionFlagsBits } from "discord-api-types/v10";
import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordActionConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
discordModerationActionRuntime,
@@ -13,6 +13,11 @@ const timeoutMemberDiscord = vi.fn(async () => ({ id: "user-1" }));
const hasAnyGuildPermissionDiscord = vi.fn(async () => false);
const enableAllActions = (_key: keyof DiscordActionConfig, _defaultValue = true) => true;
const DISCORD_TEST_CFG = {} as OpenClawConfig;
function handleModerationAction(action: string, params: Record<string, unknown>) {
return handleDiscordModerationAction(action, params, enableAllActions, DISCORD_TEST_CFG);
}
describe("discord moderation sender authorization", () => {
beforeEach(() => {
@@ -29,22 +34,18 @@ describe("discord moderation sender authorization", () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false);
await expect(
handleDiscordModerationAction(
"ban",
{
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
},
enableAllActions,
),
handleModerationAction("ban", {
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
}),
).rejects.toThrow("required permissions");
expect(hasAnyGuildPermissionDiscord).toHaveBeenCalledWith(
"guild-1",
"sender-1",
[PermissionFlagsBits.BanMembers],
undefined,
{ cfg: DISCORD_TEST_CFG },
);
expect(banMemberDiscord).not.toHaveBeenCalled();
});
@@ -53,22 +54,18 @@ describe("discord moderation sender authorization", () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false);
await expect(
handleDiscordModerationAction(
"kick",
{
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
},
enableAllActions,
),
handleModerationAction("kick", {
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
}),
).rejects.toThrow("required permissions");
expect(hasAnyGuildPermissionDiscord).toHaveBeenCalledWith(
"guild-1",
"sender-1",
[PermissionFlagsBits.KickMembers],
undefined,
{ cfg: DISCORD_TEST_CFG },
);
expect(kickMemberDiscord).not.toHaveBeenCalled();
});
@@ -77,23 +74,19 @@ describe("discord moderation sender authorization", () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false);
await expect(
handleDiscordModerationAction(
"timeout",
{
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
durationMinutes: 60,
},
enableAllActions,
),
handleModerationAction("timeout", {
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
durationMinutes: 60,
}),
).rejects.toThrow("required permissions");
expect(hasAnyGuildPermissionDiscord).toHaveBeenCalledWith(
"guild-1",
"sender-1",
[PermissionFlagsBits.ModerateMembers],
undefined,
{ cfg: DISCORD_TEST_CFG },
);
expect(timeoutMemberDiscord).not.toHaveBeenCalled();
});
@@ -102,51 +95,46 @@ describe("discord moderation sender authorization", () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(true);
kickMemberDiscord.mockResolvedValueOnce({ ok: true });
await handleDiscordModerationAction(
"kick",
{
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
reason: "rule violation",
},
enableAllActions,
);
await handleModerationAction("kick", {
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
reason: "rule violation",
});
expect(hasAnyGuildPermissionDiscord).toHaveBeenCalledWith(
"guild-1",
"sender-1",
[PermissionFlagsBits.KickMembers],
undefined,
{ cfg: DISCORD_TEST_CFG },
);
expect(kickMemberDiscord).toHaveBeenCalledWith(
{
guildId: "guild-1",
userId: "user-1",
reason: "rule violation",
},
{ cfg: DISCORD_TEST_CFG },
);
expect(kickMemberDiscord).toHaveBeenCalledWith({
guildId: "guild-1",
userId: "user-1",
reason: "rule violation",
});
});
it("forwards accountId into permission check and moderation execution", async () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(true);
timeoutMemberDiscord.mockResolvedValueOnce({ id: "user-1" });
await handleDiscordModerationAction(
"timeout",
{
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
accountId: "ops",
durationMinutes: 5,
},
enableAllActions,
);
await handleModerationAction("timeout", {
guildId: "guild-1",
userId: "user-1",
senderUserId: "sender-1",
accountId: "ops",
durationMinutes: 5,
});
expect(hasAnyGuildPermissionDiscord).toHaveBeenCalledWith(
"guild-1",
"sender-1",
[PermissionFlagsBits.ModerateMembers],
{ accountId: "ops" },
{ cfg: DISCORD_TEST_CFG, accountId: "ops" },
);
expect(timeoutMemberDiscord).toHaveBeenCalledWith(
{
@@ -156,7 +144,7 @@ describe("discord moderation sender authorization", () => {
until: undefined,
reason: undefined,
},
{ accountId: "ops" },
{ cfg: DISCORD_TEST_CFG, accountId: "ops" },
);
});
});

View File

@@ -4,6 +4,7 @@ import {
jsonResult,
readStringParam,
type DiscordActionConfig,
type OpenClawConfig,
} from "../runtime-api.js";
import {
banMemberDiscord,
@@ -29,6 +30,7 @@ async function verifySenderModerationPermission(params: {
senderUserId?: string;
requiredPermission: bigint;
accountId?: string;
cfgOptions: { cfg: OpenClawConfig };
}) {
// CLI/manual flows may not have sender context; enforce only when present.
if (!params.senderUserId) {
@@ -38,7 +40,10 @@ async function verifySenderModerationPermission(params: {
params.guildId,
params.senderUserId,
[params.requiredPermission],
params.accountId ? { accountId: params.accountId } : undefined,
{
...params.cfgOptions,
...(params.accountId ? { accountId: params.accountId } : {}),
},
);
if (!hasPermission) {
throw new Error("Sender does not have required permissions for this moderation action.");
@@ -49,6 +54,7 @@ export async function handleDiscordModerationAction(
action: string,
params: Record<string, unknown>,
isActionEnabled: ActionGate<DiscordActionConfig>,
cfg?: OpenClawConfig,
): Promise<AgentToolResult<unknown>> {
if (!isDiscordModerationAction(action)) {
throw new Error(`Unknown action: ${action}`);
@@ -56,75 +62,59 @@ export async function handleDiscordModerationAction(
if (!isActionEnabled("moderation", false)) {
throw new Error("Discord moderation is disabled.");
}
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 senderUserId = readStringParam(params, "senderUserId");
const withOpts = () => ({
...cfgOptions,
...(accountId ? { accountId } : {}),
});
await verifySenderModerationPermission({
guildId: command.guildId,
senderUserId,
requiredPermission: requiredGuildPermissionForModerationAction(command.action),
accountId,
cfgOptions,
});
switch (command.action) {
case "timeout": {
const member = accountId
? await discordModerationActionRuntime.timeoutMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
durationMinutes: command.durationMinutes,
until: command.until,
reason: command.reason,
},
{ accountId },
)
: await discordModerationActionRuntime.timeoutMemberDiscord({
guildId: command.guildId,
userId: command.userId,
durationMinutes: command.durationMinutes,
until: command.until,
reason: command.reason,
});
const member = await discordModerationActionRuntime.timeoutMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
durationMinutes: command.durationMinutes,
until: command.until,
reason: command.reason,
},
withOpts(),
);
return jsonResult({ ok: true, member });
}
case "kick": {
if (accountId) {
await discordModerationActionRuntime.kickMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
reason: command.reason,
},
{ accountId },
);
} else {
await discordModerationActionRuntime.kickMemberDiscord({
await discordModerationActionRuntime.kickMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
reason: command.reason,
});
}
},
withOpts(),
);
return jsonResult({ ok: true });
}
case "ban": {
if (accountId) {
await discordModerationActionRuntime.banMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
reason: command.reason,
deleteMessageDays: command.deleteMessageDays,
},
{ accountId },
);
} else {
await discordModerationActionRuntime.banMemberDiscord({
await discordModerationActionRuntime.banMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
reason: command.reason,
deleteMessageDays: command.deleteMessageDays,
});
}
},
withOpts(),
);
return jsonResult({ ok: true });
}
}

View File

@@ -97,6 +97,25 @@ function handleMessagingAction(
return handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg);
}
function handleGuildAction(
action: string,
params: Record<string, unknown>,
isActionEnabled: (key: keyof DiscordActionConfig) => boolean,
cfg: OpenClawConfig = DISCORD_TEST_CFG,
options?: { mediaLocalRoots?: readonly string[] },
) {
return handleDiscordGuildAction(action, params, isActionEnabled, cfg, options);
}
function handleModerationAction(
action: string,
params: Record<string, unknown>,
isActionEnabled: (key: keyof DiscordActionConfig, defaultValue?: boolean) => boolean,
cfg: OpenClawConfig = DISCORD_TEST_CFG,
) {
return handleDiscordModerationAction(action, params, isActionEnabled, cfg);
}
const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions";
const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo";
const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation";
@@ -516,26 +535,28 @@ describe("handleDiscordGuildAction", () => {
user: { id: "U1" },
})) as never;
const result = await handleDiscordGuildAction(
const cfg = {
channels: {
discord: {
defaultAccount: "work",
accounts: {
work: { token: "token-work" },
},
},
},
} as OpenClawConfig;
const result = await handleGuildAction(
"memberInfo",
{
guildId: "G1",
userId: "U1",
},
enableAllActions,
{
channels: {
discord: {
defaultAccount: "work",
accounts: {
work: { token: "token-work" },
},
},
},
} as OpenClawConfig,
cfg,
);
expect(discordGuildActionRuntime.fetchMemberInfoDiscord).toHaveBeenCalledWith("G1", "U1", {
cfg,
accountId: "work",
});
expect(result.details).toEqual(
@@ -557,7 +578,7 @@ describe("handleDiscordGuildAction - channel management", () => {
});
it("creates a channel", async () => {
const result = await handleDiscordGuildAction(
const result = await handleGuildAction(
"channelCreate",
{
guildId: "G1",
@@ -567,35 +588,37 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(createChannelDiscord).toHaveBeenCalledWith({
guildId: "G1",
name: "test-channel",
type: 0,
parentId: undefined,
topic: "Test topic",
position: undefined,
nsfw: undefined,
});
expect(createChannelDiscord).toHaveBeenCalledWith(
{
guildId: "G1",
name: "test-channel",
type: 0,
parentId: undefined,
topic: "Test topic",
position: undefined,
nsfw: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
expect(result.details).toMatchObject({ ok: true });
});
it("respects channel gating for channelCreate", async () => {
await expect(
handleDiscordGuildAction("channelCreate", { guildId: "G1", name: "test" }, channelsDisabled),
handleGuildAction("channelCreate", { guildId: "G1", name: "test" }, channelsDisabled),
).rejects.toThrow(/Discord channel management is disabled/);
});
it("forwards accountId for channelList", async () => {
await handleDiscordGuildAction(
"channelList",
{ guildId: "G1", accountId: "ops" },
channelInfoEnabled,
);
expect(listGuildChannelsDiscord).toHaveBeenCalledWith("G1", { accountId: "ops" });
await handleGuildAction("channelList", { guildId: "G1", accountId: "ops" }, channelInfoEnabled);
expect(listGuildChannelsDiscord).toHaveBeenCalledWith("G1", {
cfg: DISCORD_TEST_CFG,
accountId: "ops",
});
});
it("edits a channel", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelEdit",
{
channelId: "C1",
@@ -604,22 +627,25 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(editChannelDiscord).toHaveBeenCalledWith({
channelId: "C1",
name: "new-name",
topic: "new topic",
position: undefined,
parentId: undefined,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: undefined,
locked: undefined,
autoArchiveDuration: undefined,
});
expect(editChannelDiscord).toHaveBeenCalledWith(
{
channelId: "C1",
name: "new-name",
topic: "new topic",
position: undefined,
parentId: undefined,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: undefined,
locked: undefined,
autoArchiveDuration: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it("forwards thread edit fields", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelEdit",
{
channelId: "C1",
@@ -629,25 +655,28 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(editChannelDiscord).toHaveBeenCalledWith({
channelId: "C1",
name: undefined,
topic: undefined,
position: undefined,
parentId: undefined,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: true,
locked: false,
autoArchiveDuration: 1440,
});
expect(editChannelDiscord).toHaveBeenCalledWith(
{
channelId: "C1",
name: undefined,
topic: undefined,
position: undefined,
parentId: undefined,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: true,
locked: false,
autoArchiveDuration: 1440,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it.each([
["parentId is null", { parentId: null }],
["clearParent is true", { clearParent: true }],
])("clears the channel parent when %s", async (_label, payload) => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelEdit",
{
channelId: "C1",
@@ -655,27 +684,30 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(editChannelDiscord).toHaveBeenCalledWith({
channelId: "C1",
name: undefined,
topic: undefined,
position: undefined,
parentId: null,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: undefined,
locked: undefined,
autoArchiveDuration: undefined,
});
expect(editChannelDiscord).toHaveBeenCalledWith(
{
channelId: "C1",
name: undefined,
topic: undefined,
position: undefined,
parentId: null,
nsfw: undefined,
rateLimitPerUser: undefined,
archived: undefined,
locked: undefined,
autoArchiveDuration: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it("deletes a channel", async () => {
await handleDiscordGuildAction("channelDelete", { channelId: "C1" }, channelsEnabled);
expect(deleteChannelDiscord).toHaveBeenCalledWith("C1");
await handleGuildAction("channelDelete", { channelId: "C1" }, channelsEnabled);
expect(deleteChannelDiscord).toHaveBeenCalledWith("C1", { cfg: DISCORD_TEST_CFG });
});
it("moves a channel", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelMove",
{
guildId: "G1",
@@ -685,19 +717,22 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(moveChannelDiscord).toHaveBeenCalledWith({
guildId: "G1",
channelId: "C1",
parentId: "P1",
position: 5,
});
expect(moveChannelDiscord).toHaveBeenCalledWith(
{
guildId: "G1",
channelId: "C1",
parentId: "P1",
position: 5,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it.each([
["parentId is null", { parentId: null }],
["clearParent is true", { clearParent: true }],
])("clears the channel parent on move when %s", async (_label, payload) => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelMove",
{
guildId: "G1",
@@ -706,44 +741,53 @@ describe("handleDiscordGuildAction - channel management", () => {
},
channelsEnabled,
);
expect(moveChannelDiscord).toHaveBeenCalledWith({
guildId: "G1",
channelId: "C1",
parentId: null,
position: undefined,
});
expect(moveChannelDiscord).toHaveBeenCalledWith(
{
guildId: "G1",
channelId: "C1",
parentId: null,
position: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it("creates a category with type=4", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"categoryCreate",
{ guildId: "G1", name: "My Category" },
channelsEnabled,
);
expect(createChannelDiscord).toHaveBeenCalledWith({
guildId: "G1",
name: "My Category",
type: 4,
position: undefined,
});
expect(createChannelDiscord).toHaveBeenCalledWith(
{
guildId: "G1",
name: "My Category",
type: 4,
position: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it("edits a category", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"categoryEdit",
{ categoryId: "CAT1", name: "Renamed Category" },
channelsEnabled,
);
expect(editChannelDiscord).toHaveBeenCalledWith({
channelId: "CAT1",
name: "Renamed Category",
position: undefined,
});
expect(editChannelDiscord).toHaveBeenCalledWith(
{
channelId: "CAT1",
name: "Renamed Category",
position: undefined,
},
{ cfg: DISCORD_TEST_CFG },
);
});
it("deletes a category", async () => {
await handleDiscordGuildAction("categoryDelete", { categoryId: "CAT1" }, channelsEnabled);
expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1");
await handleGuildAction("categoryDelete", { categoryId: "CAT1" }, channelsEnabled);
expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1", { cfg: DISCORD_TEST_CFG });
});
it.each([
@@ -781,23 +825,27 @@ describe("handleDiscordGuildAction - channel management", () => {
},
},
])("sets channel permissions for $name", async ({ params, expected }) => {
await handleDiscordGuildAction("channelPermissionSet", params, channelsEnabled);
expect(setChannelPermissionDiscord).toHaveBeenCalledWith(expected);
await handleGuildAction("channelPermissionSet", params, channelsEnabled);
expect(setChannelPermissionDiscord).toHaveBeenCalledWith(expected, {
cfg: DISCORD_TEST_CFG,
});
});
it("removes channel permissions", async () => {
await handleDiscordGuildAction(
await handleGuildAction(
"channelPermissionRemove",
{ channelId: "C1", targetId: "R1" },
channelsEnabled,
);
expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1");
expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1", {
cfg: DISCORD_TEST_CFG,
});
});
});
describe("handleDiscordModerationAction", () => {
it("forwards accountId for timeout", async () => {
await handleDiscordModerationAction(
await handleModerationAction(
"timeout",
{
guildId: "G1",
@@ -813,7 +861,7 @@ describe("handleDiscordModerationAction", () => {
userId: "U1",
durationMinutes: 5,
}),
{ accountId: "ops" },
{ cfg: DISCORD_TEST_CFG, accountId: "ops" },
);
});
});
@@ -836,7 +884,7 @@ describe("handleDiscordAction per-account gating", () => {
);
expect(timeoutMemberDiscord).toHaveBeenCalledWith(
expect.objectContaining({ guildId: "G1", userId: "U1" }),
{ accountId: "ops" },
{ cfg, accountId: "ops" },
);
});
@@ -921,7 +969,7 @@ describe("handleDiscordAction per-account gating", () => {
expect(createChannelDiscord).toHaveBeenCalledWith(
expect.objectContaining({ guildId: "G1", name: "alerts" }),
{ accountId: "ops" },
{ cfg, accountId: "ops" },
);
});
});

View File

@@ -72,7 +72,7 @@ export async function handleDiscordAction(
return await handleDiscordGuildAction(action, params, isActionEnabled, cfg, options);
}
if (moderationActions.has(action)) {
return await handleDiscordModerationAction(action, params, isActionEnabled);
return await handleDiscordModerationAction(action, params, isActionEnabled, cfg);
}
if (presenceActions.has(action)) {
return await handleDiscordPresenceAction(action, params, isActionEnabled);