Command handlingâ
Unless your bot project is small, it's not a very good idea to have a single file with a giant if
/else
if chain for commands. If you want to implement features into your bot and make your development process a lot less painful, you'll want to implement a command handler. Let's get started on that!
đĄ For fully functional slash commands, there are three important pieces of code that need to be written. They are:
- The
individual command files
, containing their definitions and functionality. - The command handler, which dynamically reads the files and executes the commands.
- The
command deployment script
, to register your slash commands with Discord so they appear in the interface.
These steps can be done in any order, but all are required before the commands are fully functional.
This page details how to complete Step 2. Make sure to also complete the other pages linked above!
Loading command filesâ
âšī¸ This page requires you to have a valid main file and have at least created one
command following these steps.
â ī¸ Please make sure to select below the command creation "step" you need. The section below is divided into "Handling interaction commands" and "Handling message commands".
- Handling interaction commands
- Handling message commands
Now that your command files have been created, your bot needs to load these files on startup.
In your index.js
file, make these additions to the base template:
const fs = require("node:fs");
const path = require("node:path");
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require("discord.js");
const { Manager, UseNodeOptions, SearchPlatform } = require("magmastream");
const config = require("./config.js");
// Create a new client instance
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent],
});
// Assign Manager to the client
client.manager = new Manager({
autoPlay: true, // Optional! - Recommanded to be true
usePriority: false, // Optional! - Recommanded if you have more than 1 node
replaceYouTubeCredentials: true, // Optional! - Recommanded to be true
lastFmApiKey: config.lastFmApiKey, // Optional!
trackPartial: ["pluginInfo", "title", "author", "duration", "uri", "requester", "artworkUrl", "sourceName", "identifier", "artistUrl"], // Optional!
useNode: UseNodeOptions.LeastLoad, // Optional!
defaultSearchPlatform: SearchPlatform.YouTube, // Optional! - Assuming YouTube is enabled on Lavalink
autoPlaySearchPlatform: SearchPlatform.Spotify, // Optional! - Assuming Spotify is enabled on Lavalink
nodes: config.nodes,
send: async (id, payload) => {
const guild = client.guilds.cache.get(id);
if (guild) await guild.shard.send(payload);
},
});
// Store commands
client.commands = new Collection();
// Log in the client
client.login(config.token);
We recommend attaching a .commands
property to your client instance so that you can access your commands in other files. The rest of the examples in this guide will follow this convention. For TypeScript users, we recommend extending the base Client class to add this property, casting , or augmenting the module type .
- The
fs
module is Node's native file system module.fs
is used to read thecommands
directory and identify our command files. - The
path
module is Node's native path utility module.path
helps construct paths to access files and directories. One of the advantages of the path module is that it automatically detects the operating system and uses the appropriate joiners. - The
Collection
class extends JavaScript's nativeMap
class, and includes more extensive, useful functionality.Collection
is used to store and efficiently retrieve commands for execution.
Next, using the modules imported above, dynamically retrieve your command files with a few more additions to the index.js
file:
client.commands = new Collection();
const foldersPath = path.join(__dirname, "commands");
const loadCommands = (folderPath) => {
const commandFiles = fs.readdirSync(folderPath);
for (const file of commandFiles) {
const filePath = path.join(folderPath, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// If it's a directory, recursively load commands from it
loadCommands(filePath);
} else if (file.endsWith(".js")) {
const command = require(filePath);
// Set a new item in the Collection with the key as the command name and the value as the exported module
if ("data" in command && "execute" in command) {
console.log(`[INFO] Loaded command: ${command.data.name}`);
client.commands.set(command.data.name, command);
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
};
// Load commands from the main commands folder
loadCommands(foldersPath);
loadCommands()
recursively scans thecommands
folder and its subfolders usingfs.readdirSync
andfs.statSync
, checking if a file is a directory (to recurse deeper) or a.js
file (to process as a command).- For each
.js
file, it imports the command module, checks if it has the requireddata
(command metadata) andexecute
(function to run) properties, and adds valid commands to theclient.commands
Collection. - Valid commands are logged with their name, while invalid ones trigger a warning about missing properties, ensuring only properly structured commands are loaded.
Receiving command interactionsâ
You will receive an interaction for every slash command executed. To respond to a command, you need to create a listener for the Client#interactionCreate
event that will execute code when your application receives an interaction. Place the code below in the index.js
file you created earlier.
client.on(Events.InteractionCreate, interaction => {
console.log(interaction);
});
Not every interaction is a slash command (e.g. MessageComponent
interactions). Make sure to only handle slash commands in this function by making use of the BaseInteraction#isChatInputCommand()
method to exit the handler if another type is encountered. This method also provides typeguarding for TypeScript users, narrowing the type from BaseInteraction
to ChatInputCommandInteraction
.
client.on(Events.InteractionCreate, interaction => {
if (!interaction.isChatInputCommand()) return;
console.log(interaction);
});
Executing slash commandsâ
When your bot receives a Client#interactionCreate
event, the interaction object contains all the information you need to dynamically retrieve and execute your commands!
Let's take a look at the play
command again. Note the execute() function that will reply to the interaction with "Playing your song/playlist!".
module.exports = {
data: new SlashCommandBuilder()
.setName("play")
.setDescription("Play a song/playlist.")
.addStringOption((option) => option.setName("query").setDescription("The search query for the song/playlist").setRequired(true)),
async execute(client, interaction) {
await interaction.reply('Playing your song/playlist!')
}
}
First, you need to get the matching command from the client.commands
Collection based on the interaction.commandName
. If no matching command is found, log an error to the console and ignore the event. Then we pass your Client
instance as parameter into the execute() function.
client.on(Events.InteractionCreate, async interaction => {
// Only handle chat input commands
if (!interaction.isChatInputCommand()) return;
// Get the command from the command collection
const command = client.commands.get(interaction.commandName);
// If no command is found, log an error
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
// Execute the command
await command.execute(client, interaction);
} catch (error) {
// Log the error
console.error(error);
// Reply with an ephemeral message if the interaction has already been replied to
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral });
}
}
})
Next stepsâ
- We'll create an event handler to clean up our
index.js
file. - We'll create a [deployment script] for our slash commands.
Resulting codeâ
If you want to compare your code to the final result after completing all the steps in this guide, you can review it on the GitHub repository here .