deploy #1
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user