From cea2da70492aebdebc4265af0e038ea9580d0654 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 13:34:26 +0100 Subject: [PATCH] refactor(discord): trim interaction helper duplication --- extensions/discord/src/components.builders.ts | 64 +++----- extensions/discord/src/components.parse.ts | 39 ++--- .../src/monitor/native-command-arg-ui.ts | 98 +++++------- ...native-command-model-picker-interaction.ts | 148 ++++++------------ 4 files changed, 118 insertions(+), 231 deletions(-) diff --git a/extensions/discord/src/components.builders.ts b/extensions/discord/src/components.builders.ts index 87bc80c28cf..3ee98c71994 100644 --- a/extensions/discord/src/components.builders.ts +++ b/extensions/discord/src/components.builders.ts @@ -116,6 +116,20 @@ function createSelectComponent(params: { ) as DiscordComponentSelectType; const componentId = params.componentId ?? createShortId("sel_"); const customId = buildDiscordComponentCustomIdImpl({ componentId }); + const createEntry = ( + selectType: DiscordComponentSelectType, + label: string, + options?: DiscordComponentEntry["options"], + ): DiscordComponentEntry => ({ + id: componentId, + kind: "select", + label, + callbackData: params.spec.callbackData, + selectType, + ...(options ? { options } : {}), + allowedUsers: params.spec.allowedUsers, + }); + if (type === "string") { const options = params.spec.options ?? []; if (options.length === 0) { @@ -131,15 +145,11 @@ function createSelectComponent(params: { } return { component: new DynamicStringSelect(), - entry: { - id: componentId, - kind: "select", - label: params.spec.placeholder ?? "select", - callbackData: params.spec.callbackData, - selectType: "string", - options: options.map((option) => ({ value: option.value, label: option.label })), - allowedUsers: params.spec.allowedUsers, - }, + entry: createEntry( + "string", + params.spec.placeholder ?? "select", + options.map((option) => ({ value: option.value, label: option.label })), + ), }; } if (type === "user") { @@ -152,14 +162,7 @@ function createSelectComponent(params: { } return { component: new DynamicUserSelect(), - entry: { - id: componentId, - kind: "select", - label: params.spec.placeholder ?? "user select", - callbackData: params.spec.callbackData, - selectType: "user", - allowedUsers: params.spec.allowedUsers, - }, + entry: createEntry("user", params.spec.placeholder ?? "user select"), }; } if (type === "role") { @@ -172,14 +175,7 @@ function createSelectComponent(params: { } return { component: new DynamicRoleSelect(), - entry: { - id: componentId, - kind: "select", - label: params.spec.placeholder ?? "role select", - callbackData: params.spec.callbackData, - selectType: "role", - allowedUsers: params.spec.allowedUsers, - }, + entry: createEntry("role", params.spec.placeholder ?? "role select"), }; } if (type === "mentionable") { @@ -192,14 +188,7 @@ function createSelectComponent(params: { } return { component: new DynamicMentionableSelect(), - entry: { - id: componentId, - kind: "select", - label: params.spec.placeholder ?? "mentionable select", - callbackData: params.spec.callbackData, - selectType: "mentionable", - allowedUsers: params.spec.allowedUsers, - }, + entry: createEntry("mentionable", params.spec.placeholder ?? "mentionable select"), }; } class DynamicChannelSelect extends ChannelSelectMenu { @@ -211,14 +200,7 @@ function createSelectComponent(params: { } return { component: new DynamicChannelSelect(), - entry: { - id: componentId, - kind: "select", - label: params.spec.placeholder ?? "channel select", - callbackData: params.spec.callbackData, - selectType: "channel", - allowedUsers: params.spec.allowedUsers, - }, + entry: createEntry("channel", params.spec.placeholder ?? "channel select"), }; } diff --git a/extensions/discord/src/components.parse.ts b/extensions/discord/src/components.parse.ts index 758e425f80d..2234a90f6c0 100644 --- a/extensions/discord/src/components.parse.ts +++ b/extensions/discord/src/components.parse.ts @@ -69,6 +69,18 @@ function readOptionalNumber(value: unknown): number | undefined { return value; } +function readOptionalEmoji(value: unknown, label: string) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + const obj = value as { name?: unknown; id?: unknown; animated?: unknown }; + return { + name: readString(obj.name, `${label}.name`), + id: readOptionalString(obj.id), + animated: typeof obj.animated === "boolean" ? obj.animated : undefined, + }; +} + export function normalizeModalFieldName(value: string | undefined, index: number) { const trimmed = value?.trim(); if (trimmed) { @@ -144,20 +156,7 @@ function parseSelectOptions( label: readString(obj.label, `${label}[${index}].label`), value: readString(obj.value, `${label}[${index}].value`), description: readOptionalString(obj.description), - emoji: - typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji) - ? { - name: readString( - (obj.emoji as { name?: unknown }).name, - `${label}[${index}].emoji.name`, - ), - id: readOptionalString((obj.emoji as { id?: unknown }).id), - animated: - typeof (obj.emoji as { animated?: unknown }).animated === "boolean" - ? (obj.emoji as { animated?: boolean }).animated - : undefined, - } - : undefined, + emoji: readOptionalEmoji(obj.emoji, `${label}[${index}].emoji`), default: typeof obj.default === "boolean" ? obj.default : undefined, }; }); @@ -175,17 +174,7 @@ function parseButtonSpec(raw: unknown, label: string): DiscordComponentButtonSpe style, url, callbackData: readOptionalString(obj.callbackData), - emoji: - typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji) - ? { - name: readString((obj.emoji as { name?: unknown }).name, `${label}.emoji.name`), - id: readOptionalString((obj.emoji as { id?: unknown }).id), - animated: - typeof (obj.emoji as { animated?: unknown }).animated === "boolean" - ? (obj.emoji as { animated?: boolean }).animated - : undefined, - } - : undefined, + emoji: readOptionalEmoji(obj.emoji, `${label}.emoji`), disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined, allowedUsers: readOptionalStringArray(obj.allowedUsers, `${label}.allowedUsers`), }; diff --git a/extensions/discord/src/monitor/native-command-arg-ui.ts b/extensions/discord/src/monitor/native-command-arg-ui.ts index 6552dc6830b..551bb41787c 100644 --- a/extensions/discord/src/monitor/native-command-arg-ui.ts +++ b/extensions/discord/src/monitor/native-command-arg-ui.ts @@ -89,14 +89,13 @@ export async function handleDiscordCommandArgInteraction(params: { dispatchCommandInteraction: DispatchDiscordCommandInteraction; }) { const { interaction, data, ctx } = params; + const clearWithMessage = async (content: string) => + await params.safeInteractionCall("command arg update", () => + interaction.update({ content, components: [] }), + ); const parsed = parseDiscordCommandArgData(data); if (!parsed) { - await params.safeInteractionCall("command arg update", () => - interaction.update({ - content: "Sorry, that selection is no longer available.", - components: [], - }), - ); + await clearWithMessage("Sorry, that selection is no longer available."); return; } if (interaction.user?.id && interaction.user.id !== parsed.userId) { @@ -107,20 +106,10 @@ export async function handleDiscordCommandArgInteraction(params: { findCommandByNativeName(parsed.command, "discord") ?? listChatCommands().find((entry) => entry.key === parsed.command); if (!commandDefinition) { - await params.safeInteractionCall("command arg update", () => - interaction.update({ - content: "Sorry, that command is no longer available.", - components: [], - }), - ); + await clearWithMessage("Sorry, that command is no longer available."); return; } - const argUpdateResult = await params.safeInteractionCall("command arg update", () => - interaction.update({ - content: `✅ Selected ${parsed.value}.`, - components: [], - }), - ); + const argUpdateResult = await clearWithMessage(`✅ Selected ${parsed.value}.`); if (argUpdateResult === null) { return; } @@ -148,37 +137,42 @@ export async function handleDiscordCommandArgInteraction(params: { }); } +type DiscordCommandArgButtonParams = { + ctx: DiscordCommandArgContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; +}; + +async function runDiscordCommandArgButton( + params: DiscordCommandArgButtonParams & { + interaction: ButtonInteraction; + data: ComponentData; + }, +) { + await handleDiscordCommandArgInteraction(params); +} + class DiscordCommandArgButton extends Button { label: string; customId: string; style = ButtonStyle.Secondary; - private ctx: DiscordCommandArgContext; - private safeInteractionCall: SafeDiscordInteractionCall; - private dispatchCommandInteraction: DispatchDiscordCommandInteraction; - constructor(params: { - label: string; - customId: string; - ctx: DiscordCommandArgContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; - }) { + constructor( + params: { + label: string; + customId: string; + } & DiscordCommandArgButtonParams, + ) { super(); this.label = params.label; this.customId = params.customId; - this.ctx = params.ctx; - this.safeInteractionCall = params.safeInteractionCall; - this.dispatchCommandInteraction = params.dispatchCommandInteraction; + this.params = params; } + private params: DiscordCommandArgButtonParams; + async run(interaction: ButtonInteraction, data: ComponentData) { - await handleDiscordCommandArgInteraction({ - interaction, - data, - ctx: this.ctx, - safeInteractionCall: this.safeInteractionCall, - dispatchCommandInteraction: this.dispatchCommandInteraction, - }); + await runDiscordCommandArgButton({ ...this.params, interaction, data }); } } @@ -222,36 +216,18 @@ export function buildDiscordCommandArgMenu(params: { class DiscordCommandArgFallbackButton extends Button { label = "cmdarg"; customId = "cmdarg:seed=1"; - private ctx: DiscordCommandArgContext; - private safeInteractionCall: SafeDiscordInteractionCall; - private dispatchCommandInteraction: DispatchDiscordCommandInteraction; - constructor(params: { - ctx: DiscordCommandArgContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; - }) { + constructor(private readonly params: DiscordCommandArgButtonParams) { super(); - this.ctx = params.ctx; - this.safeInteractionCall = params.safeInteractionCall; - this.dispatchCommandInteraction = params.dispatchCommandInteraction; } async run(interaction: ButtonInteraction, data: ComponentData) { - await handleDiscordCommandArgInteraction({ - interaction, - data, - ctx: this.ctx, - safeInteractionCall: this.safeInteractionCall, - dispatchCommandInteraction: this.dispatchCommandInteraction, - }); + await runDiscordCommandArgButton({ ...this.params, interaction, data }); } } -export function createDiscordCommandArgFallbackButton(params: { - ctx: DiscordCommandArgContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; -}): Button { +export function createDiscordCommandArgFallbackButton( + params: DiscordCommandArgButtonParams, +): Button { return new DiscordCommandArgFallbackButton(params); } diff --git a/extensions/discord/src/monitor/native-command-model-picker-interaction.ts b/extensions/discord/src/monitor/native-command-model-picker-interaction.ts index 103d89edc3a..196430a8cbd 100644 --- a/extensions/discord/src/monitor/native-command-model-picker-interaction.ts +++ b/extensions/discord/src/monitor/native-command-model-picker-interaction.ts @@ -11,6 +11,7 @@ import { StringSelectMenu, type ButtonInteraction, type ComponentData, + type MessagePayload, type StringSelectMenuInteraction, } from "../internal/discord.js"; import { readDiscordModelPickerRecentModels } from "./model-picker-preferences.js"; @@ -173,6 +174,10 @@ export async function handleDiscordModelPickerInteraction(params: { allowedModelRefs, limit: 5, }); + const updatePicker = async (payload: MessagePayload) => + await params.safeInteractionCall("model picker update", () => interaction.update(payload)); + const showNotice = async (message: string) => + await updatePicker(buildDiscordModelPickerNoticePayload(message)); if (parsed.action === "recents") { const rendered = renderDiscordModelPickerRecentsView({ @@ -185,9 +190,7 @@ export async function handleDiscordModelPickerInteraction(params: { page: parsed.page, providerPage: parsed.providerPage, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } @@ -199,9 +202,7 @@ export async function handleDiscordModelPickerInteraction(params: { page: parsed.page, currentModel: currentModelRef, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } @@ -221,20 +222,14 @@ export async function handleDiscordModelPickerInteraction(params: { currentRuntime, quickModels, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } if (parsed.action === "provider") { const selectedProvider = resolveModelPickerSelectionValue(interaction) ?? parsed.provider; if (!selectedProvider || !pickerData.byProvider.has(selectedProvider)) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload("Sorry, that provider isn't available anymore."), - ), - ); + await showNotice("Sorry, that provider isn't available anymore."); return; } const rendered = renderDiscordModelPickerModelsView({ @@ -248,9 +243,7 @@ export async function handleDiscordModelPickerInteraction(params: { currentRuntime, quickModels, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } @@ -258,11 +251,7 @@ export async function handleDiscordModelPickerInteraction(params: { const selectedModel = resolveModelPickerSelectionValue(interaction); const provider = parsed.provider; if (!provider || !selectedModel) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload("Sorry, I couldn't read that model selection."), - ), - ); + await showNotice("Sorry, I couldn't read that model selection."); return; } const modelIndex = resolveDiscordModelPickerModelIndex({ @@ -271,11 +260,7 @@ export async function handleDiscordModelPickerInteraction(params: { model: selectedModel, }); if (!modelIndex) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload("Sorry, that model isn't available anymore."), - ), - ); + await showNotice("Sorry, that model isn't available anymore."); return; } const modelRef = `${provider}/${selectedModel}`; @@ -293,9 +278,7 @@ export async function handleDiscordModelPickerInteraction(params: { pendingRuntime: parsed.runtime, quickModels, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } @@ -304,11 +287,7 @@ export async function handleDiscordModelPickerInteraction(params: { resolveModelPickerSelectionValue(interaction) ?? parsed.runtime ?? "auto"; const provider = parsed.provider; if (!provider || !pickerData.byProvider.has(provider)) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload("Sorry, that provider isn't available anymore."), - ), - ); + await showNotice("Sorry, that provider isn't available anymore."); return; } const selectedModel = resolveDiscordModelPickerModelByIndex({ @@ -331,9 +310,7 @@ export async function handleDiscordModelPickerInteraction(params: { pendingRuntime: selectedRuntime, quickModels, }); - await params.safeInteractionCall("model picker update", () => - interaction.update(toDiscordModelPickerMessagePayload(rendered)), - ); + await updatePicker(toDiscordModelPickerMessagePayload(rendered)); return; } @@ -367,13 +344,7 @@ export async function handleDiscordModelPickerInteraction(params: { !parsedModelRef || !pickerData.byProvider.get(parsedModelRef.provider)?.has(parsedModelRef.model) ) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload( - "That selection expired. Please choose a model again.", - ), - ), - ); + await showNotice("That selection expired. Please choose a model again."); return; } @@ -387,19 +358,11 @@ export async function handleDiscordModelPickerInteraction(params: { runtime: runtimeOverride, }); if (!selectionCommand) { - await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload("Sorry, /model is unavailable right now."), - ), - ); + await showNotice("Sorry, /model is unavailable right now."); return; } - const updateResult = await params.safeInteractionCall("model picker update", () => - interaction.update( - buildDiscordModelPickerNoticePayload(`Applying model change to ${resolvedModelRef}...`), - ), - ); + const updateResult = await showNotice(`Applying model change to ${resolvedModelRef}...`); if (updateResult === null) { return; } @@ -440,82 +403,59 @@ export async function handleDiscordModelPickerInteraction(params: { if (parsed.action === "cancel") { const displayModel = currentModelRef ?? "default"; - await params.safeInteractionCall("model picker update", () => - interaction.update(buildDiscordModelPickerNoticePayload(`ℹ️ Model kept as ${displayModel}.`)), - ); + await showNotice(`ℹ️ Model kept as ${displayModel}.`); } } +type DiscordModelPickerFallbackParams = { + ctx: DiscordModelPickerContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; +}; + +async function runDiscordModelPickerFallback( + params: DiscordModelPickerFallbackParams & { + interaction: ButtonInteraction | StringSelectMenuInteraction; + data: ComponentData; + }, +) { + await handleDiscordModelPickerInteraction(params); +} + class DiscordModelPickerFallbackButton extends Button { label = "modelpick"; customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=btn`; - private ctx: DiscordModelPickerContext; - private safeInteractionCall: SafeDiscordInteractionCall; - private dispatchCommandInteraction: DispatchDiscordCommandInteraction; - constructor(params: { - ctx: DiscordModelPickerContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; - }) { + constructor(private readonly params: DiscordModelPickerFallbackParams) { super(); - this.ctx = params.ctx; - this.safeInteractionCall = params.safeInteractionCall; - this.dispatchCommandInteraction = params.dispatchCommandInteraction; } async run(interaction: ButtonInteraction, data: ComponentData) { - await handleDiscordModelPickerInteraction({ - interaction, - data, - ctx: this.ctx, - safeInteractionCall: this.safeInteractionCall, - dispatchCommandInteraction: this.dispatchCommandInteraction, - }); + await runDiscordModelPickerFallback({ ...this.params, interaction, data }); } } class DiscordModelPickerFallbackSelect extends StringSelectMenu { customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=sel`; options = []; - private ctx: DiscordModelPickerContext; - private safeInteractionCall: SafeDiscordInteractionCall; - private dispatchCommandInteraction: DispatchDiscordCommandInteraction; - constructor(params: { - ctx: DiscordModelPickerContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; - }) { + constructor(private readonly params: DiscordModelPickerFallbackParams) { super(); - this.ctx = params.ctx; - this.safeInteractionCall = params.safeInteractionCall; - this.dispatchCommandInteraction = params.dispatchCommandInteraction; } async run(interaction: StringSelectMenuInteraction, data: ComponentData) { - await handleDiscordModelPickerInteraction({ - interaction, - data, - ctx: this.ctx, - safeInteractionCall: this.safeInteractionCall, - dispatchCommandInteraction: this.dispatchCommandInteraction, - }); + await runDiscordModelPickerFallback({ ...this.params, interaction, data }); } } -export function createDiscordModelPickerFallbackButton(params: { - ctx: DiscordModelPickerContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; -}): Button { +export function createDiscordModelPickerFallbackButton( + params: DiscordModelPickerFallbackParams, +): Button { return new DiscordModelPickerFallbackButton(params); } -export function createDiscordModelPickerFallbackSelect(params: { - ctx: DiscordModelPickerContext; - safeInteractionCall: SafeDiscordInteractionCall; - dispatchCommandInteraction: DispatchDiscordCommandInteraction; -}): StringSelectMenu { +export function createDiscordModelPickerFallbackSelect( + params: DiscordModelPickerFallbackParams, +): StringSelectMenu { return new DiscordModelPickerFallbackSelect(params); }