diff --git a/extensions/discord/src/actions/handle-action.ts b/extensions/discord/src/actions/handle-action.ts index a1b9caf3b93..4beb7d76de4 100644 --- a/extensions/discord/src/actions/handle-action.ts +++ b/extensions/discord/src/actions/handle-action.ts @@ -10,7 +10,7 @@ import { resolveReactionMessageId } from "../../../../src/channels/plugins/actio import type { ChannelMessageActionContext } from "../../../../src/channels/plugins/types.js"; import { normalizeInteractiveReply } from "../../../../src/interactive/payload.js"; import { readBooleanParam } from "../../../../src/plugin-sdk/boolean-param.js"; -import { buildDiscordInteractiveComponents } from "../components.js"; +import { buildDiscordInteractiveComponents } from "../shared-interactive.js"; import { resolveDiscordChannelId } from "../targets.js"; import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js"; diff --git a/extensions/discord/src/components.ts b/extensions/discord/src/components.ts index 6725ad49a4d..27d29c0dbd7 100644 --- a/extensions/discord/src/components.ts +++ b/extensions/discord/src/components.ts @@ -25,8 +25,6 @@ import { type TopLevelComponents, } from "@buape/carbon"; import { ButtonStyle, MessageFlags, TextInputStyle } from "discord-api-types/v10"; -import { reduceInteractiveReply } from "../../../src/channels/plugins/outbound/interactive.js"; -import type { InteractiveButtonStyle, InteractiveReply } from "../../../src/interactive/payload.js"; export const DISCORD_COMPONENT_CUSTOM_ID_KEY = "occomp"; export const DISCORD_MODAL_CUSTOM_ID_KEY = "ocmodal"; @@ -213,69 +211,7 @@ export type DiscordComponentBuildResult = { entries: DiscordComponentEntry[]; modals: DiscordModalEntry[]; }; - -function resolveDiscordInteractiveButtonStyle( - style?: InteractiveButtonStyle, -): DiscordComponentButtonStyle | undefined { - return style ?? "secondary"; -} - -const DISCORD_INTERACTIVE_BUTTON_ROW_SIZE = 5; - -export function buildDiscordInteractiveComponents( - interactive?: InteractiveReply, -): DiscordComponentMessageSpec | undefined { - const blocks = reduceInteractiveReply( - interactive, - [] as NonNullable, - (state, block) => { - if (block.type === "text") { - const text = block.text.trim(); - if (text) { - state.push({ type: "text", text }); - } - return state; - } - if (block.type === "buttons") { - if (block.buttons.length === 0) { - return state; - } - for ( - let index = 0; - index < block.buttons.length; - index += DISCORD_INTERACTIVE_BUTTON_ROW_SIZE - ) { - state.push({ - type: "actions", - buttons: block.buttons - .slice(index, index + DISCORD_INTERACTIVE_BUTTON_ROW_SIZE) - .map((button) => ({ - label: button.label, - style: resolveDiscordInteractiveButtonStyle(button.style), - callbackData: button.value, - })), - }); - } - return state; - } - if (block.type === "select" && block.options.length > 0) { - state.push({ - type: "actions", - select: { - type: "string", - placeholder: block.placeholder, - options: block.options.map((option) => ({ - label: option.label, - value: option.value, - })), - }, - }); - } - return state; - }, - ); - return blocks.length > 0 ? { blocks } : undefined; -} +export { buildDiscordInteractiveComponents } from "./shared-interactive.js"; const BLOCK_ALIASES = new Map([ ["row", "actions"], diff --git a/extensions/discord/src/outbound-adapter.ts b/extensions/discord/src/outbound-adapter.ts index 09796a7b0b3..1c6e0111869 100644 --- a/extensions/discord/src/outbound-adapter.ts +++ b/extensions/discord/src/outbound-adapter.ts @@ -8,7 +8,6 @@ import type { OpenClawConfig } from "../../../src/config/config.js"; import type { OutboundIdentity } from "../../../src/infra/outbound/identity.js"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import type { DiscordComponentMessageSpec } from "./components.js"; -import { buildDiscordInteractiveComponents } from "./components.js"; import { getThreadBindingManager, type ThreadBindingRecord } from "./monitor/thread-bindings.js"; import { normalizeDiscordOutboundTarget } from "./normalize.js"; import { @@ -17,6 +16,7 @@ import { sendPollDiscord, sendWebhookMessageDiscord, } from "./send.js"; +import { buildDiscordInteractiveComponents } from "./shared-interactive.js"; function resolveDiscordOutboundTarget(params: { to: string; diff --git a/extensions/discord/src/shared-interactive.test.ts b/extensions/discord/src/shared-interactive.test.ts index 827ad1126a8..33ce8f68ec1 100644 --- a/extensions/discord/src/shared-interactive.test.ts +++ b/extensions/discord/src/shared-interactive.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildDiscordInteractiveComponents } from "./components.js"; +import { buildDiscordInteractiveComponents } from "./shared-interactive.js"; describe("buildDiscordInteractiveComponents", () => { it("maps shared buttons and selects into Discord component blocks", () => { diff --git a/extensions/discord/src/shared-interactive.ts b/extensions/discord/src/shared-interactive.ts new file mode 100644 index 00000000000..d99f964f5c9 --- /dev/null +++ b/extensions/discord/src/shared-interactive.ts @@ -0,0 +1,66 @@ +import { reduceInteractiveReply } from "../../../src/channels/plugins/outbound/interactive.js"; +import type { InteractiveButtonStyle, InteractiveReply } from "../../../src/interactive/payload.js"; +import type { DiscordComponentButtonStyle, DiscordComponentMessageSpec } from "./components.js"; + +function resolveDiscordInteractiveButtonStyle( + style?: InteractiveButtonStyle, +): DiscordComponentButtonStyle | undefined { + return style ?? "secondary"; +} + +const DISCORD_INTERACTIVE_BUTTON_ROW_SIZE = 5; + +export function buildDiscordInteractiveComponents( + interactive?: InteractiveReply, +): DiscordComponentMessageSpec | undefined { + const blocks = reduceInteractiveReply( + interactive, + [] as NonNullable, + (state, block) => { + if (block.type === "text") { + const text = block.text.trim(); + if (text) { + state.push({ type: "text", text }); + } + return state; + } + if (block.type === "buttons") { + if (block.buttons.length === 0) { + return state; + } + for ( + let index = 0; + index < block.buttons.length; + index += DISCORD_INTERACTIVE_BUTTON_ROW_SIZE + ) { + state.push({ + type: "actions", + buttons: block.buttons + .slice(index, index + DISCORD_INTERACTIVE_BUTTON_ROW_SIZE) + .map((button) => ({ + label: button.label, + style: resolveDiscordInteractiveButtonStyle(button.style), + callbackData: button.value, + })), + }); + } + return state; + } + if (block.type === "select" && block.options.length > 0) { + state.push({ + type: "actions", + select: { + type: "string", + placeholder: block.placeholder, + options: block.options.map((option) => ({ + label: option.label, + value: option.value, + })), + }, + }); + } + return state; + }, + ); + return blocks.length > 0 ? { blocks } : undefined; +}