diff --git a/extensions/discord/src/actions/runtime.guild.ts b/extensions/discord/src/actions/runtime.guild.ts index a5c159c5e85..2ea98fdfcf7 100644 --- a/extensions/discord/src/actions/runtime.guild.ts +++ b/extensions/discord/src/actions/runtime.guild.ts @@ -56,33 +56,32 @@ export const discordGuildActionRuntime = { uploadStickerDiscord, }; -type DiscordRoleMutation = (params: { - guildId: string; - userId: string; - roleId: string; -}) => Promise; -type DiscordRoleMutationWithAccount = ( +type DiscordRoleMutationOpts = { cfg: OpenClawConfig; accountId?: string }; +type DiscordRoleMutation = ( params: { guildId: string; userId: string; roleId: string; }, - options: { accountId: string }, + options: DiscordRoleMutationOpts, ) => Promise; async function runRoleMutation(params: { + cfgOptions: { cfg: OpenClawConfig }; accountId?: string; values: Record; - 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) { @@ -100,6 +99,15 @@ export async function handleDiscordGuildAction( options?: { mediaLocalRoots?: readonly string[] }, ): Promise> { 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) => ({ + ...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: diff --git a/extensions/discord/src/actions/runtime.moderation.authz.test.ts b/extensions/discord/src/actions/runtime.moderation.authz.test.ts index bf787552b66..fe1dd15c617 100644 --- a/extensions/discord/src/actions/runtime.moderation.authz.test.ts +++ b/extensions/discord/src/actions/runtime.moderation.authz.test.ts @@ -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) { + 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" }, ); }); }); diff --git a/extensions/discord/src/actions/runtime.moderation.ts b/extensions/discord/src/actions/runtime.moderation.ts index 0d3a51c2c55..23dc1db4b6a 100644 --- a/extensions/discord/src/actions/runtime.moderation.ts +++ b/extensions/discord/src/actions/runtime.moderation.ts @@ -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, isActionEnabled: ActionGate, + cfg?: OpenClawConfig, ): Promise> { 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 }); } } diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 7206169727c..f050ca3ca17 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -97,6 +97,25 @@ function handleMessagingAction( return handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg); } +function handleGuildAction( + action: string, + params: Record, + 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, + 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" }, ); }); }); diff --git a/extensions/discord/src/actions/runtime.ts b/extensions/discord/src/actions/runtime.ts index a4f9da17541..838e9b64d4b 100644 --- a/extensions/discord/src/actions/runtime.ts +++ b/extensions/discord/src/actions/runtime.ts @@ -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);