Skip to content

Commit a6fdbfc

Browse files
committed
feat: add Discord settings launcher button interactions
1 parent 4327f34 commit a6fdbfc

File tree

1 file changed

+102
-8
lines changed

1 file changed

+102
-8
lines changed

packages/ims/discord/client.ts

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Client, GatewayIntentBits, Partials } from "discord.js";
1+
import {
2+
ActionRowBuilder,
3+
ButtonBuilder,
4+
ButtonStyle,
5+
Client,
6+
GatewayIntentBits,
7+
Partials,
8+
} from "discord.js";
29
import { createCoreRuntime } from "@/core/runtime";
310
import type { IMAdapter } from "@/core/types";
411
import { createAgentAdapter } from "@/agents/adapter";
@@ -176,8 +183,18 @@ function getLocalSettingsUrl(): string {
176183
return `http://${getWebHost()}:${getWebPort()}/local-setting`;
177184
}
178185

186+
type LauncherCommand = "setting" | "channel" | "gh";
187+
188+
function getResolvedChannelId(target: any): string {
189+
const channel = target?.channel;
190+
if (channel?.isThread?.()) {
191+
return channel.parentId ?? target.channelId;
192+
}
193+
return target.channelId;
194+
}
195+
179196
function buildLauncherCommandText(params: {
180-
command: "setting" | "channel" | "gh";
197+
command: LauncherCommand;
181198
userId: string;
182199
channelId: string;
183200
}): string {
@@ -198,6 +215,77 @@ function buildLauncherCommandText(params: {
198215
return `Open settings: ${settingsUrl}`;
199216
}
200217

218+
function buildSettingsLinkRow(): ActionRowBuilder<ButtonBuilder> {
219+
return new ActionRowBuilder<ButtonBuilder>().addComponents(
220+
new ButtonBuilder()
221+
.setStyle(ButtonStyle.Link)
222+
.setLabel("Open settings")
223+
.setURL(getLocalSettingsUrl())
224+
);
225+
}
226+
227+
function buildSettingsChooserRows(channelId: string): ActionRowBuilder<ButtonBuilder>[] {
228+
return [
229+
new ActionRowBuilder<ButtonBuilder>().addComponents(
230+
new ButtonBuilder()
231+
.setCustomId(`ode:launcher:setting:${channelId}`)
232+
.setStyle(ButtonStyle.Secondary)
233+
.setLabel("General setting"),
234+
new ButtonBuilder()
235+
.setCustomId(`ode:launcher:channel:${channelId}`)
236+
.setStyle(ButtonStyle.Secondary)
237+
.setLabel("Channel setting"),
238+
new ButtonBuilder()
239+
.setCustomId(`ode:launcher:gh:${channelId}`)
240+
.setStyle(ButtonStyle.Secondary)
241+
.setLabel("GitHub info")
242+
),
243+
buildSettingsLinkRow(),
244+
];
245+
}
246+
247+
function buildLauncherReplyPayload(params: {
248+
command: LauncherCommand;
249+
userId: string;
250+
channelId: string;
251+
}): { content: string; components: ActionRowBuilder<ButtonBuilder>[] } {
252+
const { command, userId, channelId } = params;
253+
if (command === "setting") {
254+
return {
255+
content: "Choose which settings page to open.",
256+
components: buildSettingsChooserRows(channelId),
257+
};
258+
}
259+
260+
return {
261+
content: buildLauncherCommandText({ command, userId, channelId }),
262+
components: [buildSettingsLinkRow()],
263+
};
264+
}
265+
266+
async function handleLauncherButtonInteraction(interaction: any): Promise<void> {
267+
const customId = String(interaction.customId ?? "");
268+
if (!customId.startsWith("ode:launcher:")) return;
269+
270+
const [, , commandRaw, channelIdRaw] = customId.split(":", 4);
271+
const command = commandRaw as LauncherCommand;
272+
if (!["setting", "channel", "gh"].includes(command)) return;
273+
274+
const channelId = channelIdRaw || getResolvedChannelId(interaction);
275+
const payload = buildLauncherReplyPayload({
276+
command,
277+
userId: interaction.user.id,
278+
channelId,
279+
});
280+
281+
if (interaction.deferred || interaction.replied) {
282+
await interaction.followUp({ ...payload, ephemeral: true });
283+
return;
284+
}
285+
286+
await interaction.reply({ ...payload, ephemeral: true });
287+
}
288+
201289
async function registerDiscordCommands(client: Client): Promise<void> {
202290
try {
203291
const guilds = await client.guilds.fetch();
@@ -325,19 +413,25 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
325413

326414
client.on("interactionCreate", async (interaction: any) => {
327415
try {
416+
if (interaction.isButton && interaction.isButton()) {
417+
await handleLauncherButtonInteraction(interaction);
418+
return;
419+
}
420+
328421
if (!interaction.isChatInputCommand || !interaction.isChatInputCommand()) return;
329422
const commandName = String(interaction.commandName || "").toLowerCase();
330423
if (!["setting", "channel", "gh"].includes(commandName)) return;
331424

332-
const channel = interaction.channel;
333-
const resolvedChannelId = channel?.isThread?.() ? (channel.parentId ?? interaction.channelId) : interaction.channelId;
334-
const text = buildLauncherCommandText({
335-
command: commandName as "setting" | "channel" | "gh",
425+
const payload = buildLauncherReplyPayload({
426+
command: commandName as LauncherCommand,
336427
userId: interaction.user.id,
337-
channelId: resolvedChannelId,
428+
channelId: getResolvedChannelId(interaction),
338429
});
339430

340-
await interaction.reply({ content: text, ephemeral: true });
431+
await interaction.reply({
432+
...payload,
433+
ephemeral: true,
434+
});
341435
} catch (error) {
342436
log.error("Discord interaction handler failed", { error: String(error) });
343437
}

0 commit comments

Comments
 (0)