Before you continueโ
Assuming you've followed the guide so far, your project directory should look something like this:
discord-bot/
โโโ node_modules/
โโโ config.js
โโโ index.js
โโโ package-lock.json
โโโ package.json
Following this guide, create a src/ folder and move index.js and config.js into it.
Your new project directory should look like this:
discord-bot/
โโโ node_modules/
โโโ src/
โ โโโ config.js
โ โโโ index.js
โโโ package-lock.json
โโโ package.json
Make sure to update the "main" field in your package.json from "index.js" to "src/index.js".
This page requires a valid main file. Select the tab below for the type of commands you want to create.
For fully functional commands, three pieces need to be in place:
- Individual command files with their definitions and functionality.
- A command handler that dynamically reads and executes those files.
- A deployment script to register slash commands with Discord.
These can be completed in any order. All three are required before slash commands are functional. For message commands, only the first two are required. This page covers step 1.
- Slash commands
- Message commands
Individual command filesโ
Create a commands/music/ folder inside src/. This is where your command files will live. You'll use SlashCommandBuilder to define each command.
Each command file exports two things:
dataโ the command definition registered with Discord.execute()โ the function that runs when the command is used.
A slash command must always respond to the interaction. Discord enforces this to ensure a good user experience โ failing to respond will cause Discord to show the command as failed.
Create src/commands/music/play.js:
- src/commands/music/play.js
const { LoadTypes, StateTypes } = require("magmastream");
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("play")
.setDescription("Play a song or playlist.")
.addStringOption((option) =>
option
.setName("query")
.setDescription("The search query for the song or playlist")
.setRequired(true)
),
async execute(client, interaction) {
// Check if the user is in a voice channel
if (!interaction.member.voice.channel) {
return await interaction
.reply("You need to be in a voice channel to use this command.")
.catch((error) => console.log(`[ERROR] Failed to reply: ${error.message}`));
}
// Get the current player
let player = client.manager.getPlayer(interaction.guild.id);
// Check if the user is in the same voice channel as the bot
if (player && interaction.member.voice.channel.id !== player.voiceChannelId) {
return await interaction
.reply("You need to be in the same voice channel as the bot.")
.catch((error) => console.log(`[ERROR] Failed to reply: ${error.message}`));
}
// Defer the reply
await interaction.deferReply().catch((error) => console.log(`[ERROR] Failed to defer: ${error.message}`));
// Search for the query
const query = interaction.options.getString("query");
let res = await client.manager.search(query, interaction.member.user);
// Handle empty or errored results
if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
return await interaction
.editReply("No results found.")
.catch((error) => console.log(`[ERROR] Failed to edit reply: ${error.message}`));
}
// Create the player if it doesn't exist
try {
player = client.manager.create({
guildId: interaction.guild.id,
voiceChannelId: interaction.member.voice.channel.id,
textChannelId: interaction.channel.id,
selfDeafen: true,
volume: 100,
});
if (player.state !== StateTypes.Connected) player.connect();
} catch (error) {
return await interaction
.editReply(`An error occurred while creating the player: ${error.message}`)
.catch((error) => console.log(`[ERROR] Failed to edit reply: ${error.message}`));
}
// Handle the search result
switch (res.loadType) {
case LoadTypes.Track:
case LoadTypes.Search: {
const track = res.tracks[0];
player.queue.add(track);
if (!player.playing && !player.paused && (await player.queue.totalSize()) === 1) {
await player.play();
}
await interaction
.editReply(`Added \`${track.author} - ${track.title}\` to the queue.`)
.catch((error) => console.log(`[ERROR] Failed to edit reply: ${error.message}`));
break;
}
case LoadTypes.Playlist: {
const tracks = res.playlist.tracks;
player.queue.add(tracks);
if (!player.playing && !player.paused && (await player.queue.totalSize()) === tracks.length) {
await player.play();
}
await interaction
.editReply(`Added playlist \`${res.playlist.name}\` with \`${tracks.length}\` songs to the queue.`)
.catch((error) => console.log(`[ERROR] Failed to edit reply: ${error.message}`));
break;
}
}
},
};
Your project directory should now look like this:
discord-bot/
โโโ node_modules/
โโโ src/
โ โโโ commands/
โ โ โโโ music/
โ โ โโโ play.js
โ โโโ config.js
โ โโโ index.js
โโโ package-lock.json
โโโ package.json
Message commands use a prefix (e.g. !play) instead of Discord's slash command system. They require the MessageContent privileged intent โ make sure it is enabled in the Discord Developer Portal and added to your client in index.js:
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.MessageContent,
],
});
Also add a prefix field to your config.js:
module.exports = {
token: "[TOKEN]",
prefix: "!",
nodes: [ ... ],
};
Individual command filesโ
Each command file exports a message() function alongside the slash command's data and execute(). This way a single file handles both interaction and message variants of the same command.
Create src/commands/music/play.js:
- src/commands/music/play.js
const { LoadTypes, StateTypes } = require("magmastream");
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: {} // ... slash command implementation (see Slash commands tab)
async execute(client, interaction) {
// ... slash command implementation (see Slash commands tab)
},
async message(client, message, args) {
if (!args.length) return message.reply("Please provide a search query.");
if (!message.member.voice.channel) return message.reply("You need to be in a voice channel to use this command.");
let player = client.manager.getPlayer(message.guild.id);
if (player && message.member.voice.channel.id !== player.voiceChannelId)
return message.reply("You need to be in the same voice channel as the bot to use this command.");
const query = args.join(" ");
let res = await client.manager.search(query, message.member.user);
if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
return await message
.reply("No results found.")
.catch((error) => console.log(`[ERROR] Failed to send message ${error.message} to channel: ${message.channel.id}`));
}
try {
player = client.manager.create({
guildId: message.guild.id,
voiceChannelId: message.member.voice.channel.id,
textChannelId: message.channel.id,
selfDeafen: true,
volume: 100,
});
// Connect to the voice channel if the player is not already connected
if (player.state !== StateTypes.Connected) player.connect();
} catch (error) {
return await message
.reply(`An error occurred while creating the player: ${error.message}`)
.catch((error) => console.log(`[ERROR] Failed to send message ${error.message} to channel: ${message.channel.id}`));
}
switch (res.loadType) {
case LoadTypes.Track:
case LoadTypes.Search:
// Add the track to the queue
const track = res.tracks[0];
player.queue.add(track);
// Play the track if the queue is empty
if (!player.playing && !player.paused && player.queue.totalSize() === 1) await player.play();
// Reply with a success message
await message
.reply(`Successfully added \`${track.author} - ${track.title}\` to the queue.`)
.catch((error) => console.log(`[ERROR] Failed to send message ${error.message} to channel: ${message.channel.id}`));
break;
case LoadTypes.Playlist:
// Add the playlist tracks to the queue
res.tracks = res.playlist.tracks;
player.queue.add(res.tracks);
// Play the first track if the queue is empty
if (!player.playing && !player.paused && player.queue.totalSize() === res.tracks.length) await player.play();
// Reply with a success message
await message
.reply(`Successfully added \`${res.playlist.name}\` playlist with \`${res.tracks.length + 1} songs\` to the queue.`)
.catch((error) => console.log(`[ERROR] Failed to send message ${error.message} to channel: ${message.channel.id}`));
break;
}
},
};
Your project directory should now look like this:
discord-bot/
โโโ node_modules/
โโโ src/
โ โโโ commands/
โ โ โโโ music/
โ โ โโโ play.js
โ โโโ events/
โ โ โโโ interactionCreate.js
โ โ โโโ messageCreate.js
โ โโโ config.js
โ โโโ index.js
โโโ package-lock.json
โโโ package.json
Next stepsโ
Add more commands by creating new files inside src/commands/music/. Next, move on to command handling to load your files and respond to interactions.
Resulting codeโ
You can review the complete example on the GitHub repository here .