Enhance command registration process and improve guild command handling

- Introduced a new `buildCommandBody` function to prevent duplicate command definitions during registration.
- Implemented `clearGuildCommandLeftovers` to remove unused guild-scoped commands, ensuring a clean command picker in Discord.
- Updated command registration logic to clear global commands when using guild-specific commands, improving command visibility and user experience.
- Enhanced logging to provide clearer insights into command registration and cleanup processes.
This commit is contained in:
smueller
2026-07-23 10:24:47 +02:00
parent 629e89f380
commit 4b6736444c
2 changed files with 58 additions and 10 deletions

View File

@@ -4,6 +4,10 @@ NODE_ENV=production
BOT_TOKEN= BOT_TOKEN=
BOT_CLIENT_ID= BOT_CLIENT_ID=
BOT_CLIENT_SECRET= BOT_CLIENT_SECRET=
# Optional: register slash commands only on this guild (instant sync for dev).
# Leave empty in production so commands are global. When set, global commands
# are cleared; when empty, leftover guild-scoped commands are cleared on startup
# so Discord does not show every command twice.
BOT_GUILD_ID= BOT_GUILD_ID=
DATABASE_URL=postgresql://nexumi:nexumi@postgres:5432/nexumi DATABASE_URL=postgresql://nexumi:nexumi@postgres:5432/nexumi
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379

View File

@@ -69,38 +69,82 @@ const commands: SlashCommand[] = [
const map = new Map(commands.map((c) => [c.data.name, c])); const map = new Map(commands.map((c) => [c.data.name, c]));
const contextMenuMap = new Map(utilityContextMenus.map((c) => [c.data.name, c])); const contextMenuMap = new Map(utilityContextMenus.map((c) => [c.data.name, c]));
function buildCommandBody(): object[] {
const seen = new Set<string>();
const body: object[] = [];
for (const command of [...commands, ...utilityContextMenus]) {
const json = command.data.toJSON() as { name: string; type?: number };
const key = `${json.type ?? 1}:${json.name}`;
if (seen.has(key)) {
logger.warn({ name: json.name, type: json.type }, 'Skipping duplicate command definition');
continue;
}
seen.add(key);
body.push(json);
}
return body;
}
/**
* Guild-scoped leftovers + global commands both appear in Discord's picker.
* Always clear the unused scope so each command shows once.
*/
async function clearGuildCommandLeftovers(rest: REST, clientId: string): Promise<number> {
const guilds = (await rest.get(Routes.userGuilds())) as Array<{ id: string }>;
let cleared = 0;
for (const guild of guilds) {
const existing = (await rest.get(
Routes.applicationGuildCommands(clientId, guild.id)
)) as unknown[];
if (existing.length === 0) {
continue;
}
await rest.put(Routes.applicationGuildCommands(clientId, guild.id), { body: [] });
cleared += 1;
}
return cleared;
}
export async function registerCommands() { export async function registerCommands() {
const rest = new REST({ version: '10' }).setToken(env.BOT_TOKEN); const rest = new REST({ version: '10' }).setToken(env.BOT_TOKEN);
const body = [ const body = buildCommandBody();
...commands.map((c) => c.data.toJSON()),
...utilityContextMenus.map((c) => c.data.toJSON())
];
// Guild commands sync instantly; global can take up to ~1h. // Guild commands sync instantly; global can take up to ~1h.
if (env.BOT_GUILD_ID) { if (env.BOT_GUILD_ID) {
await rest.put(Routes.applicationGuildCommands(env.BOT_CLIENT_ID, env.BOT_GUILD_ID), { await rest.put(Routes.applicationGuildCommands(env.BOT_CLIENT_ID, env.BOT_GUILD_ID), {
body body
}); });
// Remove global copies so the guild does not show every command twice.
await rest.put(Routes.applicationCommands(env.BOT_CLIENT_ID), { body: [] });
logger.info( logger.info(
{ {
scope: 'guild', scope: 'guild',
guildId: env.BOT_GUILD_ID, guildId: env.BOT_GUILD_ID,
count: commands.length, count: body.length,
contextMenus: utilityContextMenus.length clearedGlobal: true
}, },
'Registered guild application commands' 'Registered guild application commands and cleared global commands'
); );
return; return;
} }
await rest.put(Routes.applicationCommands(env.BOT_CLIENT_ID), { body }); await rest.put(Routes.applicationCommands(env.BOT_CLIENT_ID), { body });
// Wipe leftover guild-scoped registrations from earlier BOT_GUILD_ID / shard runs.
let clearedGuilds = 0;
try {
clearedGuilds = await clearGuildCommandLeftovers(rest, env.BOT_CLIENT_ID);
} catch (error) {
logger.warn({ error }, 'Failed to clear leftover guild application commands');
}
logger.info( logger.info(
{ {
scope: 'global', scope: 'global',
count: commands.length, count: body.length,
contextMenus: utilityContextMenus.length clearedGuilds
}, },
'Registered global application commands' 'Registered global application commands and cleared guild-scoped leftovers'
); );
} }