Skip to content

Commit 292d605

Browse files
committed
feat: add Discord settings and GitHub launcher commands
1 parent 533e48d commit 292d605

File tree

1 file changed

+131
-1
lines changed

1 file changed

+131
-1
lines changed

packages/ims/discord/client.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,32 @@ import { createCoreRuntime } from "@/core/runtime";
33
import type { IMAdapter } from "@/core/types";
44
import { createAgentAdapter } from "@/agents/adapter";
55
import type { OpenCodeMessageContext } from "@/agents";
6-
import { getChannelSystemMessage, getDiscordBotTokens, getDiscordTargetChannels, getGitHubInfoForUser } from "@/config";
6+
import {
7+
getChannelSystemMessage,
8+
getDiscordBotTokens,
9+
getDiscordTargetChannels,
10+
getGitHubInfoForUser,
11+
getWebHost,
12+
getWebPort,
13+
} from "@/config";
714
import { isThreadActive, markThreadActive } from "@/config/local/settings";
815
import { log } from "@/utils";
916

1017
const DISCORD_MESSAGE_LIMIT = 2000;
18+
const DISCORD_LAUNCHER_COMMANDS = [
19+
{
20+
name: "setting",
21+
description: "Open Ode settings",
22+
},
23+
{
24+
name: "channel",
25+
description: "Open channel settings",
26+
},
27+
{
28+
name: "gh",
29+
description: "Check GitHub token status",
30+
},
31+
] as const;
1132

1233
let discordClient: Client | null = null;
1334
const statusMessageThreadMap = new Map<string, string>();
@@ -143,6 +164,63 @@ function isStopCommand(text: string): boolean {
143164
return text.trim().toLowerCase() === "stop";
144165
}
145166

167+
function parseLauncherCommand(text: string): "setting" | "channel" | "gh" | null {
168+
const trimmed = text.trim().toLowerCase();
169+
if (/^\/setting\b/.test(trimmed)) return "setting";
170+
if (/^\/channel\b/.test(trimmed)) return "channel";
171+
if (/^\/gh\b/.test(trimmed)) return "gh";
172+
return null;
173+
}
174+
175+
function getLocalSettingsUrl(): string {
176+
return `http://${getWebHost()}:${getWebPort()}/local-setting`;
177+
}
178+
179+
function buildLauncherCommandText(params: {
180+
command: "setting" | "channel" | "gh";
181+
userId: string;
182+
channelId: string;
183+
}): string {
184+
const { command, userId, channelId } = params;
185+
const settingsUrl = getLocalSettingsUrl();
186+
187+
if (command === "gh") {
188+
const hasToken = Boolean(getGitHubInfoForUser(userId)?.token);
189+
return hasToken
190+
? `GitHub token is set for your account. You can update it at ${settingsUrl}.`
191+
: `No GitHub token set yet. Add it at ${settingsUrl}.`;
192+
}
193+
194+
if (command === "channel") {
195+
return `Open channel settings for channel ${channelId}: ${settingsUrl}`;
196+
}
197+
198+
return `Open settings: ${settingsUrl}`;
199+
}
200+
201+
async function postLauncherCommandReply(params: {
202+
channel: any;
203+
command: "setting" | "channel" | "gh";
204+
userId: string;
205+
channelId: string;
206+
}): Promise<void> {
207+
const text = buildLauncherCommandText(params);
208+
await params.channel.send(text);
209+
}
210+
211+
async function registerDiscordCommands(client: Client): Promise<void> {
212+
try {
213+
const guilds = await client.guilds.fetch();
214+
for (const [, guildPreview] of guilds) {
215+
const guild = await client.guilds.fetch(guildPreview.id);
216+
await guild.commands.set([...DISCORD_LAUNCHER_COMMANDS]);
217+
}
218+
log.info("Discord slash commands registered", { count: DISCORD_LAUNCHER_COMMANDS.length });
219+
} catch (error) {
220+
log.warn("Failed to register Discord slash commands", { error: String(error) });
221+
}
222+
}
223+
146224
export async function startDiscordRuntime(reason: string): Promise<boolean> {
147225
if (discordClient) return true;
148226
const configuredTokens = getDiscordBotTokens();
@@ -176,6 +254,16 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
176254

177255
const threadId = message.channel.id;
178256
const text = message.content.trim();
257+
const launcherCommand = parseLauncherCommand(text);
258+
if (launcherCommand) {
259+
await postLauncherCommandReply({
260+
channel: message.channel,
261+
command: launcherCommand,
262+
userId: message.author.id,
263+
channelId: parentId,
264+
});
265+
return;
266+
}
179267
const mentioned = message.mentions.users.has(client.user.id);
180268
const active = isThreadActive(parentId, threadId);
181269
if (!mentioned && !active) return;
@@ -203,10 +291,31 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
203291
const parentId = message.channel.id;
204292
if (configuredChannels && !configuredChannels.includes(parentId)) return;
205293

294+
const parentLauncherCommand = parseLauncherCommand(message.content);
295+
if (parentLauncherCommand) {
296+
await postLauncherCommandReply({
297+
channel: message.channel,
298+
command: parentLauncherCommand,
299+
userId: message.author.id,
300+
channelId: parentId,
301+
});
302+
return;
303+
}
304+
206305
const isMentioned = message.mentions.users.has(client.user.id);
207306
if (!isMentioned) return;
208307

209308
const cleaned = cleanBotMention(message.content, client.user.id);
309+
const cleanedLauncherCommand = parseLauncherCommand(cleaned);
310+
if (cleanedLauncherCommand) {
311+
await postLauncherCommandReply({
312+
channel: message.channel,
313+
command: cleanedLauncherCommand,
314+
userId: message.author.id,
315+
channelId: parentId,
316+
});
317+
return;
318+
}
210319
if (!cleaned) {
211320
await message.reply("Please include a request after mentioning me.");
212321
return;
@@ -230,7 +339,28 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
230339
}
231340
});
232341

342+
client.on("interactionCreate", async (interaction: any) => {
343+
try {
344+
if (!interaction.isChatInputCommand || !interaction.isChatInputCommand()) return;
345+
const commandName = String(interaction.commandName || "").toLowerCase();
346+
if (!["setting", "channel", "gh"].includes(commandName)) return;
347+
348+
const channel = interaction.channel;
349+
const resolvedChannelId = channel?.isThread?.() ? (channel.parentId ?? interaction.channelId) : interaction.channelId;
350+
const text = buildLauncherCommandText({
351+
command: commandName as "setting" | "channel" | "gh",
352+
userId: interaction.user.id,
353+
channelId: resolvedChannelId,
354+
});
355+
356+
await interaction.reply({ content: text, ephemeral: true });
357+
} catch (error) {
358+
log.error("Discord interaction handler failed", { error: String(error) });
359+
}
360+
});
361+
233362
await client.login(token);
363+
await registerDiscordCommands(client);
234364
discordClient = client;
235365
log.info("Discord runtime started", { reason, botUserId: client.user?.id ?? "unknown" });
236366
return true;

0 commit comments

Comments
 (0)