refactor(discord): trim interaction helper duplication

This commit is contained in:
Peter Steinberger
2026-04-29 13:34:26 +01:00
parent f0adbd48e8
commit cea2da7049
4 changed files with 118 additions and 231 deletions

View File

@@ -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"),
};
}

View File

@@ -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`),
};

View File

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

View File

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