deploy #1

Merged
smueller merged 30 commits from deploy into main 2026-07-24 08:41:09 +00:00
2 changed files with 58 additions and 10 deletions
Showing only changes of commit 4b6736444c - Show all commits

View File

@@ -4,6 +4,10 @@ NODE_ENV=production
BOT_TOKEN=
BOT_CLIENT_ID=
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=
DATABASE_URL=postgresql://nexumi:nexumi@postgres:5432/nexumi
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 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() {
const rest = new REST({ version: '10' }).setToken(env.BOT_TOKEN);
const body = [
...commands.map((c) => c.data.toJSON()),
...utilityContextMenus.map((c) => c.data.toJSON())
];
const body = buildCommandBody();
// Guild commands sync instantly; global can take up to ~1h.
if (env.BOT_GUILD_ID) {
await rest.put(Routes.applicationGuildCommands(env.BOT_CLIENT_ID, env.BOT_GUILD_ID), {
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(
{
scope: 'guild',
guildId: env.BOT_GUILD_ID,
count: commands.length,
contextMenus: utilityContextMenus.length
count: body.length,
clearedGlobal: true
},
'Registered guild application commands'
'Registered guild application commands and cleared global commands'
);
return;
}
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(
{
scope: 'global',
count: commands.length,
contextMenus: utilityContextMenus.length
count: body.length,
clearedGuilds
},
'Registered global application commands'
'Registered global application commands and cleared guild-scoped leftovers'
);
}