refactor: extract discord shared interactive mapper

This commit is contained in:
Peter Steinberger
2026-03-16 05:35:21 +00:00
parent 7bea559166
commit ff558862f0
5 changed files with 70 additions and 68 deletions

View File

@@ -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";

View File

@@ -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<DiscordComponentMessageSpec["blocks"]>,
(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<string, DiscordComponentBlock["type"]>([
["row", "actions"],

View File

@@ -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;

View File

@@ -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", () => {

View File

@@ -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<DiscordComponentMessageSpec["blocks"]>,
(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;
}