Commands live in a module's commands-dir (typically commands/). Each .js file is one slash command. The bot
collects all command files and syncs them with Discord at startup.
// modules/example/commands/ping.js
module.exports.config = {
name: 'ping',
description: 'Replies with pong.'
};
module.exports.run = async (interaction) => {
await interaction.reply({content: 'Pong!', ephemeral: true});
};Two exports:
config- the slash command definition Discord registers.name,description, optionaloptions, optionaldefaultMemberPermissions.run- async function called when a user invokes the command. Receives theChatInputCommandInteraction.
const {ChannelType} = require('discord.js');
module.exports.config = {
name: 'archive',
description: 'Archive a channel.',
options: [
{
type: 'CHANNEL',
name: 'channel',
description: 'Channel to archive.',
required: true,
channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement]
},
{
type: 'STRING',
name: 'reason',
description: 'Why are you archiving it?',
required: false
}
]
};Supported type strings: STRING, INTEGER, BOOLEAN, USER, CHANNEL, ROLE, MENTIONABLE, NUMBER,
ATTACHMENT, SUB_COMMAND, SUB_COMMAND_GROUP. (These are mapped to ApplicationCommandOptionType internally.)
Read option values inside run with interaction.options.getString('reason'), getChannel('channel', true),
getInteger(...), etc.
Use SUB_COMMAND options and export a subcommands map keyed by subcommand name:
module.exports.subcommands = {
'add': async (interaction) => { /* ... */ },
'remove': async (interaction) => { /* ... */ },
'list': async (interaction) => { /* ... */ }
};
module.exports.config = {
name: 'role',
description: 'Manage self-assignable roles.',
options: [
{
type: 'SUB_COMMAND',
name: 'add',
description: 'Add a role.',
options: [{type: 'ROLE', name: 'role', description: 'Role to add.', required: true}]
},
{
type: 'SUB_COMMAND',
name: 'remove',
description: 'Remove a role.',
options: [{type: 'ROLE', name: 'role', description: 'Role to remove.', required: true}]
},
{
type: 'SUB_COMMAND',
name: 'list',
description: 'List configured roles.'
}
]
};When subcommands is exported, the loader dispatches to the matching key automatically - you don't need a top-level
run. (You may still export run as a fallback for commands that have both subcommands and a no-subcommand
invocation.)
For STRING / INTEGER / NUMBER options with autocomplete: true, export an autocomplete function:
module.exports.config = {
name: 'play',
description: 'Play a sound.',
options: [
{
type: 'STRING',
name: 'sound',
description: 'Which sound to play.',
required: true,
autocomplete: true
}
]
};
module.exports.autocomplete = async (interaction) => {
const focused = interaction.options.getFocused();
const sounds = client.configurations['sounds']['catalog']
.filter(s => s.name.toLowerCase().includes(focused.toLowerCase()))
.slice(0, 25);
await interaction.respond(sounds.map(s => ({name: s.name, value: s.id})));
};Restrict who can use a command at the Discord level with defaultMemberPermissions:
const {PermissionFlagsBits} = require('discord.js');
module.exports.config = {
name: 'kick',
description: 'Kick a member.',
defaultMemberPermissions: PermissionFlagsBits.KickMembers.toString(),
options: [/* ... */]
};For finer-grained checks (role-based, configurable per-server), do the check inside run:
module.exports.run = async (interaction) => {
const staffRoles = interaction.client.configurations['my-module']['config']['staffRoles'];
if (!interaction.member.roles.cache.some(r => staffRoles.includes(r.id))) {
return interaction.reply({content: '⚠️ Staff only.', ephemeral: true});
}
// ...
};Use localize() for both descriptions and replies - see localization.md. Descriptions are
evaluated at command registration time, so they always render in client.locale:
const {localize} = require('../../../src/functions/localize');
module.exports.config = {
name: 'help',
description: localize('help', 'command-description')
};Discord requires a response within 3 seconds. If your command does anything slow (database lookups, API calls, file I/O), defer immediately:
module.exports.run = async (interaction) => {
await interaction.deferReply({ephemeral: true});
const result = await someSlowThing();
await interaction.editReply({content: result});
};Commands are registered as guild commands for the guild configured in config/config.json. Global registration is
not supported - this bot is single-guild by design. Reloading happens automatically at startup; new commands appear
within seconds. To force a re-sync without restart, run /reload.