Discord: move action runtime into extension

This commit is contained in:
Gustavo Madeira Santana 2026-03-18 02:07:01 +00:00
parent 4c36436fb4
commit 9df3e9b617
No known key found for this signature in database
15 changed files with 363 additions and 195 deletions

View File

@ -1,4 +1,7 @@
export * from "./src/audit.js";
export * from "./src/actions/runtime.js";
export * from "./src/actions/runtime.moderation-shared.js";
export * from "./src/actions/runtime.shared.js";
export * from "./src/channel-actions.js";
export * from "./src/directory-live.js";
export * from "./src/monitor.js";

View File

@ -5,12 +5,12 @@ import {
readStringArrayParam,
readStringParam,
} from "openclaw/plugin-sdk/agent-runtime";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
import { handleDiscordAction } from "./runtime.js";
import {
isDiscordModerationAction,
readDiscordModerationCommand,
} from "openclaw/plugin-sdk/agent-runtime";
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
} from "./runtime.moderation-shared.js";
type Ctx = Pick<
ChannelMessageActionContext,

View File

@ -4,8 +4,6 @@ import {
readStringArrayParam,
readStringParam,
} from "openclaw/plugin-sdk/agent-runtime";
import { readDiscordParentIdParam } from "openclaw/plugin-sdk/agent-runtime";
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
@ -13,6 +11,8 @@ import { normalizeInteractiveReply } from "openclaw/plugin-sdk/channel-runtime";
import { buildDiscordInteractiveComponents } from "../shared-interactive.js";
import { resolveDiscordChannelId } from "../targets.js";
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
import { handleDiscordAction } from "./runtime.js";
import { readDiscordParentIdParam } from "./runtime.shared.js";
const providerId = "discord";

View File

@ -1,5 +1,14 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import {
type ActionGate,
jsonResult,
parseAvailableTags,
readNumberParam,
readStringArrayParam,
readStringParam,
} from "../../../../src/agents/tools/common.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import { getPresence } from "../monitor/presence-cache.js";
import {
addRoleDiscord,
createChannelDiscord,
@ -19,17 +28,29 @@ import {
setChannelPermissionDiscord,
uploadEmojiDiscord,
uploadStickerDiscord,
} from "../../plugin-sdk/discord.js";
import { getPresence } from "../../plugin-sdk/discord.js";
import {
type ActionGate,
jsonResult,
parseAvailableTags,
readNumberParam,
readStringArrayParam,
readStringParam,
} from "./common.js";
import { readDiscordParentIdParam } from "./discord-actions-shared.js";
} from "../send.js";
import { readDiscordParentIdParam } from "./runtime.shared.js";
export const discordGuildActionRuntime = {
addRoleDiscord,
createChannelDiscord,
createScheduledEventDiscord,
deleteChannelDiscord,
editChannelDiscord,
fetchChannelInfoDiscord,
fetchMemberInfoDiscord,
fetchRoleInfoDiscord,
fetchVoiceStatusDiscord,
listGuildChannelsDiscord,
listGuildEmojisDiscord,
listScheduledEventsDiscord,
moveChannelDiscord,
removeChannelPermissionDiscord,
removeRoleDiscord,
setChannelPermissionDiscord,
uploadEmojiDiscord,
uploadStickerDiscord,
};
type DiscordRoleMutation = (params: {
guildId: string;
@ -85,8 +106,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const member = accountId
? await fetchMemberInfoDiscord(guildId, userId, { accountId })
: await fetchMemberInfoDiscord(guildId, userId);
? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, { accountId })
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId);
const presence = getPresence(accountId, userId);
const activities = presence?.activities ?? undefined;
const status = presence?.status ?? undefined;
@ -100,8 +121,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const roles = accountId
? await fetchRoleInfoDiscord(guildId, { accountId })
: await fetchRoleInfoDiscord(guildId);
? await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, { accountId })
: await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId);
return jsonResult({ ok: true, roles });
}
case "emojiList": {
@ -112,8 +133,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const emojis = accountId
? await listGuildEmojisDiscord(guildId, { accountId })
: await listGuildEmojisDiscord(guildId);
? await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listGuildEmojisDiscord(guildId);
return jsonResult({ ok: true, emojis });
}
case "emojiUpload": {
@ -129,7 +150,7 @@ export async function handleDiscordGuildAction(
});
const roleIds = readStringArrayParam(params, "roleIds");
const emoji = accountId
? await uploadEmojiDiscord(
? await discordGuildActionRuntime.uploadEmojiDiscord(
{
guildId,
name,
@ -138,7 +159,7 @@ export async function handleDiscordGuildAction(
},
{ accountId },
)
: await uploadEmojiDiscord({
: await discordGuildActionRuntime.uploadEmojiDiscord({
guildId,
name,
mediaUrl,
@ -162,7 +183,7 @@ export async function handleDiscordGuildAction(
required: true,
});
const sticker = accountId
? await uploadStickerDiscord(
? await discordGuildActionRuntime.uploadStickerDiscord(
{
guildId,
name,
@ -172,7 +193,7 @@ export async function handleDiscordGuildAction(
},
{ accountId },
)
: await uploadStickerDiscord({
: await discordGuildActionRuntime.uploadStickerDiscord({
guildId,
name,
description,
@ -185,14 +206,22 @@ export async function handleDiscordGuildAction(
if (!isActionEnabled("roles", false)) {
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({ accountId, values: params, mutate: addRoleDiscord });
await runRoleMutation({
accountId,
values: params,
mutate: discordGuildActionRuntime.addRoleDiscord,
});
return jsonResult({ ok: true });
}
case "roleRemove": {
if (!isActionEnabled("roles", false)) {
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({ accountId, values: params, mutate: removeRoleDiscord });
await runRoleMutation({
accountId,
values: params,
mutate: discordGuildActionRuntime.removeRoleDiscord,
});
return jsonResult({ ok: true });
}
case "channelInfo": {
@ -203,8 +232,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const channel = accountId
? await fetchChannelInfoDiscord(channelId, { accountId })
: await fetchChannelInfoDiscord(channelId);
? await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId, { accountId })
: await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId);
return jsonResult({ ok: true, channel });
}
case "channelList": {
@ -215,8 +244,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const channels = accountId
? await listGuildChannelsDiscord(guildId, { accountId })
: await listGuildChannelsDiscord(guildId);
? await discordGuildActionRuntime.listGuildChannelsDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listGuildChannelsDiscord(guildId);
return jsonResult({ ok: true, channels });
}
case "voiceStatus": {
@ -230,8 +259,10 @@ export async function handleDiscordGuildAction(
required: true,
});
const voice = accountId
? await fetchVoiceStatusDiscord(guildId, userId, { accountId })
: await fetchVoiceStatusDiscord(guildId, userId);
? await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId, {
accountId,
})
: await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId);
return jsonResult({ ok: true, voice });
}
case "eventList": {
@ -242,8 +273,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const events = accountId
? await listScheduledEventsDiscord(guildId, { accountId })
: await listScheduledEventsDiscord(guildId);
? await discordGuildActionRuntime.listScheduledEventsDiscord(guildId, { accountId })
: await discordGuildActionRuntime.listScheduledEventsDiscord(guildId);
return jsonResult({ ok: true, events });
}
case "eventCreate": {
@ -274,8 +305,10 @@ export async function handleDiscordGuildAction(
privacy_level: 2,
};
const event = accountId
? await createScheduledEventDiscord(guildId, payload, { accountId })
: await createScheduledEventDiscord(guildId, payload);
? await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload, {
accountId,
})
: await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload);
return jsonResult({ ok: true, event });
}
case "channelCreate": {
@ -290,7 +323,7 @@ export async function handleDiscordGuildAction(
const position = readNumberParam(params, "position", { integer: true });
const nsfw = params.nsfw as boolean | undefined;
const channel = accountId
? await createChannelDiscord(
? await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
@ -302,7 +335,7 @@ export async function handleDiscordGuildAction(
},
{ accountId },
)
: await createChannelDiscord({
: await discordGuildActionRuntime.createChannelDiscord({
guildId,
name,
type: type ?? undefined,
@ -348,8 +381,8 @@ export async function handleDiscordGuildAction(
availableTags,
};
const channel = accountId
? await editChannelDiscord(editPayload, { accountId })
: await editChannelDiscord(editPayload);
? await discordGuildActionRuntime.editChannelDiscord(editPayload, { accountId })
: await discordGuildActionRuntime.editChannelDiscord(editPayload);
return jsonResult({ ok: true, channel });
}
case "channelDelete": {
@ -360,8 +393,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const result = accountId
? await deleteChannelDiscord(channelId, { accountId })
: await deleteChannelDiscord(channelId);
? await discordGuildActionRuntime.deleteChannelDiscord(channelId, { accountId })
: await discordGuildActionRuntime.deleteChannelDiscord(channelId);
return jsonResult(result);
}
case "channelMove": {
@ -375,7 +408,7 @@ export async function handleDiscordGuildAction(
const parentId = readDiscordParentIdParam(params);
const position = readNumberParam(params, "position", { integer: true });
if (accountId) {
await moveChannelDiscord(
await discordGuildActionRuntime.moveChannelDiscord(
{
guildId,
channelId,
@ -385,7 +418,7 @@ export async function handleDiscordGuildAction(
{ accountId },
);
} else {
await moveChannelDiscord({
await discordGuildActionRuntime.moveChannelDiscord({
guildId,
channelId,
parentId,
@ -402,7 +435,7 @@ export async function handleDiscordGuildAction(
const name = readStringParam(params, "name", { required: true });
const position = readNumberParam(params, "position", { integer: true });
const channel = accountId
? await createChannelDiscord(
? await discordGuildActionRuntime.createChannelDiscord(
{
guildId,
name,
@ -411,7 +444,7 @@ export async function handleDiscordGuildAction(
},
{ accountId },
)
: await createChannelDiscord({
: await discordGuildActionRuntime.createChannelDiscord({
guildId,
name,
type: 4,
@ -429,7 +462,7 @@ export async function handleDiscordGuildAction(
const name = readStringParam(params, "name");
const position = readNumberParam(params, "position", { integer: true });
const channel = accountId
? await editChannelDiscord(
? await discordGuildActionRuntime.editChannelDiscord(
{
channelId: categoryId,
name: name ?? undefined,
@ -437,7 +470,7 @@ export async function handleDiscordGuildAction(
},
{ accountId },
)
: await editChannelDiscord({
: await discordGuildActionRuntime.editChannelDiscord({
channelId: categoryId,
name: name ?? undefined,
position: position ?? undefined,
@ -452,8 +485,8 @@ export async function handleDiscordGuildAction(
required: true,
});
const result = accountId
? await deleteChannelDiscord(categoryId, { accountId })
: await deleteChannelDiscord(categoryId);
? await discordGuildActionRuntime.deleteChannelDiscord(categoryId, { accountId })
: await discordGuildActionRuntime.deleteChannelDiscord(categoryId);
return jsonResult(result);
}
case "channelPermissionSet": {
@ -468,7 +501,7 @@ export async function handleDiscordGuildAction(
const allow = readStringParam(params, "allow");
const deny = readStringParam(params, "deny");
if (accountId) {
await setChannelPermissionDiscord(
await discordGuildActionRuntime.setChannelPermissionDiscord(
{
channelId,
targetId,
@ -479,7 +512,7 @@ export async function handleDiscordGuildAction(
{ accountId },
);
} else {
await setChannelPermissionDiscord({
await discordGuildActionRuntime.setChannelPermissionDiscord({
channelId,
targetId,
targetType,
@ -495,9 +528,11 @@ export async function handleDiscordGuildAction(
}
const { channelId, targetId } = readChannelPermissionTarget(params);
if (accountId) {
await removeChannelPermissionDiscord(channelId, targetId, { accountId });
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId, {
accountId,
});
} else {
await removeChannelPermissionDiscord(channelId, targetId);
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId);
}
return jsonResult({ ok: true });
}

View File

@ -1,7 +1,19 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/config.js";
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { withNormalizedTimestamp } from "../../../../src/agents/date-time.js";
import { assertMediaNotDataUrl } from "../../../../src/agents/sandbox-paths.js";
import {
type ActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "../../../../src/agents/tools/common.js";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import { resolvePollMaxSelections } from "../../../../src/polls.js";
import { readDiscordComponentSpec } from "../components.js";
import {
createThreadDiscord,
deleteMessageDiscord,
@ -23,20 +35,34 @@ import {
sendStickerDiscord,
sendVoiceMessageDiscord,
unpinMessageDiscord,
} from "../../plugin-sdk/discord.js";
import type { DiscordSendComponents, DiscordSendEmbeds } from "../../plugin-sdk/discord.js";
import { readDiscordComponentSpec, resolveDiscordChannelId } from "../../plugin-sdk/discord.js";
import { resolvePollMaxSelections } from "../../polls.js";
import { withNormalizedTimestamp } from "../date-time.js";
import { assertMediaNotDataUrl } from "../sandbox-paths.js";
import {
type ActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "./common.js";
} from "../send.js";
import type { DiscordSendComponents, DiscordSendEmbeds } from "../send.shared.js";
import { resolveDiscordChannelId } from "../targets.js";
export const discordMessagingActionRuntime = {
createThreadDiscord,
deleteMessageDiscord,
editMessageDiscord,
fetchChannelPermissionsDiscord,
fetchMessageDiscord,
fetchReactionsDiscord,
listPinsDiscord,
listThreadsDiscord,
pinMessageDiscord,
reactMessageDiscord,
readDiscordComponentSpec,
readMessagesDiscord,
removeOwnReactionsDiscord,
removeReactionDiscord,
resolveDiscordChannelId,
searchMessagesDiscord,
sendDiscordComponentMessage,
sendMessageDiscord,
sendPollDiscord,
sendStickerDiscord,
sendVoiceMessageDiscord,
unpinMessageDiscord,
};
function parseDiscordMessageLink(link: string) {
const normalized = link.trim();
@ -65,7 +91,7 @@ export async function handleDiscordMessagingAction(
cfg?: OpenClawConfig,
): Promise<AgentToolResult<unknown>> {
const resolveChannelId = () =>
resolveDiscordChannelId(
discordMessagingActionRuntime.resolveDiscordChannelId(
readStringParam(params, "channelId", {
required: true,
}),
@ -95,28 +121,45 @@ export async function handleDiscordMessagingAction(
});
if (remove) {
if (accountId) {
await removeReactionDiscord(channelId, messageId, emoji, {
await discordMessagingActionRuntime.removeReactionDiscord(channelId, messageId, emoji, {
...cfgOptions,
accountId,
});
} else {
await removeReactionDiscord(channelId, messageId, emoji, cfgOptions);
await discordMessagingActionRuntime.removeReactionDiscord(
channelId,
messageId,
emoji,
cfgOptions,
);
}
return jsonResult({ ok: true, removed: emoji });
}
if (isEmpty) {
const removed = accountId
? await removeOwnReactionsDiscord(channelId, messageId, { ...cfgOptions, accountId })
: await removeOwnReactionsDiscord(channelId, messageId, cfgOptions);
? await discordMessagingActionRuntime.removeOwnReactionsDiscord(channelId, messageId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.removeOwnReactionsDiscord(
channelId,
messageId,
cfgOptions,
);
return jsonResult({ ok: true, removed: removed.removed });
}
if (accountId) {
await reactMessageDiscord(channelId, messageId, emoji, {
await discordMessagingActionRuntime.reactMessageDiscord(channelId, messageId, emoji, {
...cfgOptions,
accountId,
});
} else {
await reactMessageDiscord(channelId, messageId, emoji, cfgOptions);
await discordMessagingActionRuntime.reactMessageDiscord(
channelId,
messageId,
emoji,
cfgOptions,
);
}
return jsonResult({ ok: true, added: emoji });
}
@ -129,11 +172,15 @@ export async function handleDiscordMessagingAction(
required: true,
});
const limit = readNumberParam(params, "limit");
const reactions = await fetchReactionsDiscord(channelId, messageId, {
...cfgOptions,
...(accountId ? { accountId } : {}),
limit,
});
const reactions = await discordMessagingActionRuntime.fetchReactionsDiscord(
channelId,
messageId,
{
...cfgOptions,
...(accountId ? { accountId } : {}),
limit,
},
);
return jsonResult({ ok: true, reactions });
}
case "sticker": {
@ -146,7 +193,7 @@ export async function handleDiscordMessagingAction(
required: true,
label: "stickerIds",
});
await sendStickerDiscord(to, stickerIds, {
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, {
...cfgOptions,
...(accountId ? { accountId } : {}),
content,
@ -169,7 +216,7 @@ export async function handleDiscordMessagingAction(
const allowMultiselect = readBooleanParam(params, "allowMultiselect");
const durationHours = readNumberParam(params, "durationHours");
const maxSelections = resolvePollMaxSelections(answers.length, allowMultiselect);
await sendPollDiscord(
await discordMessagingActionRuntime.sendPollDiscord(
to,
{ question, options: answers, maxSelections, durationHours },
{ ...cfgOptions, ...(accountId ? { accountId } : {}), content },
@ -182,8 +229,11 @@ export async function handleDiscordMessagingAction(
}
const channelId = resolveChannelId();
const permissions = accountId
? await fetchChannelPermissionsDiscord(channelId, { ...cfgOptions, accountId })
: await fetchChannelPermissionsDiscord(channelId, cfgOptions);
? await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, cfgOptions);
return jsonResult({ ok: true, permissions });
}
case "fetchMessage": {
@ -206,8 +256,11 @@ export async function handleDiscordMessagingAction(
);
}
const message = accountId
? await fetchMessageDiscord(channelId, messageId, { ...cfgOptions, accountId })
: await fetchMessageDiscord(channelId, messageId, cfgOptions);
? await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, cfgOptions);
return jsonResult({
ok: true,
message: normalizeMessage(message),
@ -228,8 +281,11 @@ export async function handleDiscordMessagingAction(
around: readStringParam(params, "around"),
};
const messages = accountId
? await readMessagesDiscord(channelId, query, { ...cfgOptions, accountId })
: await readMessagesDiscord(channelId, query, cfgOptions);
? await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, cfgOptions);
return jsonResult({
ok: true,
messages: messages.map((message) => normalizeMessage(message)),
@ -245,7 +301,7 @@ export async function handleDiscordMessagingAction(
const rawComponents = params.components;
const componentSpec =
rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents)
? readDiscordComponentSpec(rawComponents)
? discordMessagingActionRuntime.readDiscordComponentSpec(rawComponents)
: null;
const components: DiscordSendComponents | undefined =
Array.isArray(rawComponents) || typeof rawComponents === "function"
@ -279,16 +335,20 @@ export async function handleDiscordMessagingAction(
const payload = componentSpec.text
? componentSpec
: { ...componentSpec, text: normalizedContent };
const result = await sendDiscordComponentMessage(to, payload, {
...cfgOptions,
...(accountId ? { accountId } : {}),
silent,
replyTo: replyTo ?? undefined,
sessionKey: sessionKey ?? undefined,
agentId: agentId ?? undefined,
mediaUrl: mediaUrl ?? undefined,
filename: filename ?? undefined,
});
const result = await discordMessagingActionRuntime.sendDiscordComponentMessage(
to,
payload,
{
...cfgOptions,
...(accountId ? { accountId } : {}),
silent,
replyTo: replyTo ?? undefined,
sessionKey: sessionKey ?? undefined,
agentId: agentId ?? undefined,
mediaUrl: mediaUrl ?? undefined,
filename: filename ?? undefined,
},
);
return jsonResult({ ok: true, result, components: true });
}
@ -305,7 +365,7 @@ export async function handleDiscordMessagingAction(
);
}
assertMediaNotDataUrl(mediaUrl);
const result = await sendVoiceMessageDiscord(to, mediaUrl, {
const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, {
...cfgOptions,
...(accountId ? { accountId } : {}),
replyTo,
@ -314,7 +374,7 @@ export async function handleDiscordMessagingAction(
return jsonResult({ ok: true, result, voiceMessage: true });
}
const result = await sendMessageDiscord(to, content ?? "", {
const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", {
...cfgOptions,
...(accountId ? { accountId } : {}),
mediaUrl,
@ -338,8 +398,18 @@ export async function handleDiscordMessagingAction(
required: true,
});
const message = accountId
? await editMessageDiscord(channelId, messageId, { content }, { ...cfgOptions, accountId })
: await editMessageDiscord(channelId, messageId, { content }, cfgOptions);
? await discordMessagingActionRuntime.editMessageDiscord(
channelId,
messageId,
{ content },
{ ...cfgOptions, accountId },
)
: await discordMessagingActionRuntime.editMessageDiscord(
channelId,
messageId,
{ content },
cfgOptions,
);
return jsonResult({ ok: true, message });
}
case "deleteMessage": {
@ -351,9 +421,12 @@ export async function handleDiscordMessagingAction(
required: true,
});
if (accountId) {
await deleteMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await deleteMessageDiscord(channelId, messageId, cfgOptions);
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, cfgOptions);
}
return jsonResult({ ok: true });
}
@ -375,8 +448,11 @@ export async function handleDiscordMessagingAction(
appliedTags: appliedTags ?? undefined,
};
const thread = accountId
? await createThreadDiscord(channelId, payload, { ...cfgOptions, accountId })
: await createThreadDiscord(channelId, payload, cfgOptions);
? await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, cfgOptions);
return jsonResult({ ok: true, thread });
}
case "threadList": {
@ -391,7 +467,7 @@ export async function handleDiscordMessagingAction(
const before = readStringParam(params, "before");
const limit = readNumberParam(params, "limit");
const threads = accountId
? await listThreadsDiscord(
? await discordMessagingActionRuntime.listThreadsDiscord(
{
guildId,
channelId,
@ -401,7 +477,7 @@ export async function handleDiscordMessagingAction(
},
{ ...cfgOptions, accountId },
)
: await listThreadsDiscord(
: await discordMessagingActionRuntime.listThreadsDiscord(
{
guildId,
channelId,
@ -423,13 +499,17 @@ export async function handleDiscordMessagingAction(
});
const mediaUrl = readStringParam(params, "mediaUrl");
const replyTo = readStringParam(params, "replyTo");
const result = await sendMessageDiscord(`channel:${channelId}`, content, {
...cfgOptions,
...(accountId ? { accountId } : {}),
mediaUrl,
mediaLocalRoots: options?.mediaLocalRoots,
replyTo,
});
const result = await discordMessagingActionRuntime.sendMessageDiscord(
`channel:${channelId}`,
content,
{
...cfgOptions,
...(accountId ? { accountId } : {}),
mediaUrl,
mediaLocalRoots: options?.mediaLocalRoots,
replyTo,
},
);
return jsonResult({ ok: true, result });
}
case "pinMessage": {
@ -441,9 +521,12 @@ export async function handleDiscordMessagingAction(
required: true,
});
if (accountId) {
await pinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await pinMessageDiscord(channelId, messageId, cfgOptions);
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, cfgOptions);
}
return jsonResult({ ok: true });
}
@ -456,9 +539,12 @@ export async function handleDiscordMessagingAction(
required: true,
});
if (accountId) {
await unpinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await unpinMessageDiscord(channelId, messageId, cfgOptions);
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, cfgOptions);
}
return jsonResult({ ok: true });
}
@ -468,8 +554,11 @@ export async function handleDiscordMessagingAction(
}
const channelId = resolveChannelId();
const pins = accountId
? await listPinsDiscord(channelId, { ...cfgOptions, accountId })
: await listPinsDiscord(channelId, cfgOptions);
? await discordMessagingActionRuntime.listPinsDiscord(channelId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.listPinsDiscord(channelId, cfgOptions);
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
}
case "searchMessages": {
@ -490,7 +579,7 @@ export async function handleDiscordMessagingAction(
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
const results = accountId
? await searchMessagesDiscord(
? await discordMessagingActionRuntime.searchMessagesDiscord(
{
guildId,
content,
@ -500,7 +589,7 @@ export async function handleDiscordMessagingAction(
},
{ ...cfgOptions, accountId },
)
: await searchMessagesDiscord(
: await discordMessagingActionRuntime.searchMessagesDiscord(
{
guildId,
content,

View File

@ -1,5 +1,5 @@
import { PermissionFlagsBits } from "discord-api-types/v10";
import { readNumberParam, readStringParam } from "./common.js";
import { readNumberParam, readStringParam } from "../../../../src/agents/tools/common.js";
export type DiscordModerationAction = "timeout" | "kick" | "ban";

View File

@ -1,25 +1,30 @@
import { PermissionFlagsBits } from "discord-api-types/v10";
import { describe, expect, it, vi } from "vitest";
import type { DiscordActionConfig } from "../../config/config.js";
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import {
discordModerationActionRuntime,
handleDiscordModerationAction,
} from "./runtime.moderation.js";
const discordSendMocks = vi.hoisted(() => ({
banMemberDiscord: vi.fn(async () => ({ ok: true })),
kickMemberDiscord: vi.fn(async () => ({ ok: true })),
timeoutMemberDiscord: vi.fn(async () => ({ id: "user-1" })),
hasAnyGuildPermissionDiscord: vi.fn(async () => false),
}));
const { banMemberDiscord, kickMemberDiscord, timeoutMemberDiscord, hasAnyGuildPermissionDiscord } =
discordSendMocks;
vi.mock("../../../extensions/discord/src/send.js", () => ({
...discordSendMocks,
}));
const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime };
const banMemberDiscord = vi.fn(async () => ({ ok: true }));
const kickMemberDiscord = vi.fn(async () => ({ ok: true }));
const timeoutMemberDiscord = vi.fn(async () => ({ id: "user-1" }));
const hasAnyGuildPermissionDiscord = vi.fn(async () => false);
const enableAllActions = (_key: keyof DiscordActionConfig, _defaultValue = true) => true;
describe("discord moderation sender authorization", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.assign(discordModerationActionRuntime, originalDiscordModerationActionRuntime, {
banMemberDiscord,
kickMemberDiscord,
timeoutMemberDiscord,
hasAnyGuildPermissionDiscord,
});
});
it("rejects ban when sender lacks BAN_MEMBERS", async () => {
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false);

View File

@ -1,17 +1,28 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import {
type ActionGate,
jsonResult,
readStringParam,
} from "../../../../src/agents/tools/common.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import {
banMemberDiscord,
hasAnyGuildPermissionDiscord,
kickMemberDiscord,
timeoutMemberDiscord,
} from "../../plugin-sdk/discord.js";
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
} from "../send.js";
import {
isDiscordModerationAction,
readDiscordModerationCommand,
requiredGuildPermissionForModerationAction,
} from "./discord-actions-moderation-shared.js";
} from "./runtime.moderation-shared.js";
export const discordModerationActionRuntime = {
banMemberDiscord,
hasAnyGuildPermissionDiscord,
kickMemberDiscord,
timeoutMemberDiscord,
};
async function verifySenderModerationPermission(params: {
guildId: string;
@ -23,7 +34,7 @@ async function verifySenderModerationPermission(params: {
if (!params.senderUserId) {
return;
}
const hasPermission = await hasAnyGuildPermissionDiscord(
const hasPermission = await discordModerationActionRuntime.hasAnyGuildPermissionDiscord(
params.guildId,
params.senderUserId,
[params.requiredPermission],
@ -57,7 +68,7 @@ export async function handleDiscordModerationAction(
switch (command.action) {
case "timeout": {
const member = accountId
? await timeoutMemberDiscord(
? await discordModerationActionRuntime.timeoutMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
@ -67,7 +78,7 @@ export async function handleDiscordModerationAction(
},
{ accountId },
)
: await timeoutMemberDiscord({
: await discordModerationActionRuntime.timeoutMemberDiscord({
guildId: command.guildId,
userId: command.userId,
durationMinutes: command.durationMinutes,
@ -78,7 +89,7 @@ export async function handleDiscordModerationAction(
}
case "kick": {
if (accountId) {
await kickMemberDiscord(
await discordModerationActionRuntime.kickMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
@ -87,7 +98,7 @@ export async function handleDiscordModerationAction(
{ accountId },
);
} else {
await kickMemberDiscord({
await discordModerationActionRuntime.kickMemberDiscord({
guildId: command.guildId,
userId: command.userId,
reason: command.reason,
@ -97,7 +108,7 @@ export async function handleDiscordModerationAction(
}
case "ban": {
if (accountId) {
await banMemberDiscord(
await discordModerationActionRuntime.banMemberDiscord(
{
guildId: command.guildId,
userId: command.userId,
@ -107,7 +118,7 @@ export async function handleDiscordModerationAction(
{ accountId },
);
} else {
await banMemberDiscord({
await discordModerationActionRuntime.banMemberDiscord({
guildId: command.guildId,
userId: command.userId,
reason: command.reason,

View File

@ -1,12 +1,9 @@
import type { GatewayPlugin } from "@buape/carbon/gateway";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
clearGateways,
registerGateway,
} from "../../../extensions/discord/src/monitor/gateway-registry.js";
import type { DiscordActionConfig } from "../../config/config.js";
import type { ActionGate } from "./common.js";
import { handleDiscordPresenceAction } from "./discord-actions-presence.js";
import type { ActionGate } from "../../../../src/agents/tools/common.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import { clearGateways, registerGateway } from "../monitor/gateway-registry.js";
import { handleDiscordPresenceAction } from "./runtime.presence.js";
const mockUpdatePresence = vi.fn();

View File

@ -1,8 +1,12 @@
import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import { getGateway } from "../../plugin-sdk/discord.js";
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
import {
type ActionGate,
jsonResult,
readStringParam,
} from "../../../../src/agents/tools/common.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import { getGateway } from "../monitor/gateway-registry.js";
const ACTIVITY_TYPE_MAP: Record<string, number> = {
playing: 0,

View File

@ -1,4 +1,4 @@
import { readStringParam } from "./common.js";
import { readStringParam } from "../../../../src/agents/tools/common.js";
export function readDiscordParentIdParam(
params: Record<string, unknown>,

View File

@ -1,11 +1,22 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DiscordActionConfig, OpenClawConfig } from "../../config/config.js";
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
import { handleDiscordAction } from "./discord-actions.js";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
import { discordGuildActionRuntime, handleDiscordGuildAction } from "./runtime.guild.js";
import { handleDiscordAction } from "./runtime.js";
import {
discordMessagingActionRuntime,
handleDiscordMessagingAction,
} from "./runtime.messaging.js";
import {
discordModerationActionRuntime,
handleDiscordModerationAction,
} from "./runtime.moderation.js";
const discordSendMocks = vi.hoisted(() => ({
const originalDiscordMessagingActionRuntime = { ...discordMessagingActionRuntime };
const originalDiscordGuildActionRuntime = { ...discordGuildActionRuntime };
const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime };
const discordSendMocks = {
banMemberDiscord: vi.fn(async () => ({})),
createChannelDiscord: vi.fn(async () => ({
id: "new-channel",
@ -42,7 +53,7 @@ const discordSendMocks = vi.hoisted(() => ({
setChannelPermissionDiscord: vi.fn(async () => ({ ok: true })),
timeoutMemberDiscord: vi.fn(async () => ({})),
unpinMessageDiscord: vi.fn(async () => ({})),
}));
};
const {
createChannelDiscord,
@ -67,21 +78,28 @@ const {
timeoutMemberDiscord,
} = discordSendMocks;
vi.mock("../../../extensions/discord/src/send.js", () => ({
...discordSendMocks,
}));
const enableAllActions = () => true;
const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions";
const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo";
const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation";
describe("handleDiscordMessagingAction", () => {
beforeEach(() => {
vi.clearAllMocks();
});
beforeEach(() => {
vi.clearAllMocks();
Object.assign(
discordMessagingActionRuntime,
originalDiscordMessagingActionRuntime,
discordSendMocks,
);
Object.assign(discordGuildActionRuntime, originalDiscordGuildActionRuntime, discordSendMocks);
Object.assign(
discordModerationActionRuntime,
originalDiscordModerationActionRuntime,
discordSendMocks,
);
});
describe("handleDiscordMessagingAction", () => {
it.each([
{
name: "without account",

View File

@ -1,11 +1,11 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import { createDiscordActionGate } from "../../plugin-sdk/discord.js";
import { readStringParam } from "./common.js";
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
import { handleDiscordPresenceAction } from "./discord-actions-presence.js";
import { readStringParam } from "../../../../src/agents/tools/common.js";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import { createDiscordActionGate } from "../accounts.js";
import { handleDiscordGuildAction } from "./runtime.guild.js";
import { handleDiscordMessagingAction } from "./runtime.messaging.js";
import { handleDiscordModerationAction } from "./runtime.moderation.js";
import { handleDiscordPresenceAction } from "./runtime.presence.js";
const messagingActions = new Set([
"react",

View File

@ -1,4 +1,5 @@
import {
createLegacyMessageToolDiscoveryMethods,
createDiscordMessageToolComponentsSchema,
createUnionActionGate,
listTokenSourcedAccounts,
@ -132,6 +133,7 @@ function describeDiscordMessageTool({
export const discordMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeDiscordMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeDiscordMessageTool),
extractToolSend: ({ args }) => {
const action = typeof args.action === "string" ? args.action.trim() : "";
if (action === "sendMessage") {

View File

@ -16,14 +16,18 @@ export * from "../agents/provider-id.js";
export * from "../agents/schema/typebox.js";
export * from "../agents/sglang-defaults.js";
export * from "../agents/tools/common.js";
export * from "../agents/tools/discord-actions-shared.js";
export * from "../agents/tools/discord-actions.js";
export * from "../agents/tools/telegram-actions.js";
export * from "../agents/tools/web-guarded-fetch.js";
export * from "../agents/tools/web-shared.js";
export * from "../agents/tools/discord-actions-moderation-shared.js";
export * from "../agents/tools/web-fetch-utils.js";
export * from "../agents/vllm-defaults.js";
// Intentional public runtime surface: channel plugins use ingress agent helpers directly.
export * from "../agents/agent-command.js";
export * from "../tts/tts.js";
// Legacy channel action runtime re-exports. New bundled plugin code should use
// local extension-owned modules instead of adding more public SDK surface here.
export {
handleDiscordAction,
readDiscordParentIdParam,
isDiscordModerationAction,
readDiscordModerationCommand,
} from "../../extensions/discord/runtime-api.js";