Additional features

TIP

This page is a follow-up and bases its code off of the previous page.

The command handler you've been building so far doesn't do much aside from dynamically load and execute commands. Those two things alone are great, but definitely not the only things you want. Before diving into it, let's do some quick refactoring in preparation.

	const args = message.content.slice(prefix.length).split(/ +/);
-	const command = args.shift().toLowerCase();
+	const commandName = args.shift().toLowerCase();

-	if (!client.commands.has(command)) return;
+	if (!client.commands.has(commandName)) return;
+
+	const command = client.commands.get(commandName);

	try {
-		client.commands.get(command).execute(message, args);
+		command.execute(message, args);
	}

In this short (but necessary) refactor, you:

  1. Renamed the original command variable to commandName (to be descriptive about what it actually is, and so that you can name one of our variables as command later).
  2. Changed the following if statement appropriately.
  3. Made a variable named command which is the actual command object.
  4. Changed the line inside the try/catch statement to use the command variable instead.

Now you can start adding features!

Required arguments

For this section, we'll be using the args-info.js command as an example. If you chose to keep it, it should look like this now:

module.exports = {
	name: 'args-info',
	description: 'Information about the arguments provided.',
	execute(message, args) {
		if (!args.length) {
			return message.channel.send(`You didn't provide any arguments, ${message.author}!`);
		} else if (args[0] === 'foo') {
			return message.channel.send('bar');
		}

		message.channel.send(`Arguments: ${args}\nArguments length: ${args.length}`);
	},
};

This is fine if you only have a few commands that require arguments. However, if you plan on making a lot of commands and don't want to copy & paste that if statement each time, it'd be a smart idea to change that check into something simpler.

Here are the changes you'll be making:

+	args: true,
	execute(message, args) {
-		if (!args.length) {
-			return message.channel.send(`You didn't provide any arguments, ${message.author}!`);
-		}
-		else if (args[0] === 'foo') {
+		if (args[0] === 'foo') {
			return message.channel.send('bar');
		}
		
		message.channel.send(`Arguments: ${args}\nArguments length: ${args.length}`);
	},

And then in your main file:

	const command = client.commands.get(commandName);
+
+	if (command.args && !args.length) {
+		return message.channel.send(`You didn't provide any arguments, ${message.author}!`);
+	}
+

Now whenever you set args to true in one of your command files, it'll perform this check and supply feedback if necessary.

Expected command usage

It's good UX (user experience) to let the user know that a command requires arguments when they don't provide any (it also prevents your code from breaking). Letting them know what kind of arguments are expected is even better.

Here's a simple implementation of such a thing. For this example, pretend you have a !role command, where the first argument is the user to give the role, and the second argument is the name of the role to give them.

In your role.js file:

	args: true,
+	usage: '<user> <role>',
	execute(message, args) {

In your main file:

	if (command.args && !args.length) {
-		return message.channel.send(`You didn't provide any arguments, ${message.author}!`);
+		let reply = `You didn't provide any arguments, ${message.author}!`;
+
+		if (command.usage) {
+			reply += `\nThe proper usage would be: \`${prefix}${command.name} ${command.usage}\``;
+		}
+
+		return message.channel.send(reply);
	}

Use an if statement to check if the usage property exists (and is truthy) first, so that you don't accidentally end up with undefined in the reply string (in the case that you forget to properly supply the property in your command file, or some similar incident). A simple precaution such as this can greatly improve the user experience.

Guild only commands

Some commands are meant to be used only inside servers and won't work whatsoever in DMs. A prime example of this would be a kick command. You can add a property to the necessary commands to determine whether or not it should be only available outside of servers.

In the kick command you created in an earlier chapter, make the following changes:

module.exports = {
	name: 'kick',
	description: 'Kick a user from the server.',
+	guildOnly: true,

And in your main file, above the args checking line, add this in:

if (command.guildOnly && message.channel.type !== 'text') {
	return message.reply('I can\'t execute that command inside DMs!');
}

Now when you try to use the kick command inside a DM, you'll get the appropriate response which will also prevent your bot from throwing an error.

User
User 02/01/2019
!kick
Tutorial Bot
Tutorial Bot Bot 02/01/2019
I can't execute that command inside DMs!

Cooldowns

Spam is something you generally want to avoid - especially if one of your commands requires calls to other APIs, or takes a bit of time to build/send. This is also a very common feature bot developers want to integrate into their projects, so let's get started on that!

You'll be using the ping command to test this on. Add in the following bit of code:

module.exports = {
	name: 'ping',
+	cooldown: 5,
	execute(message) {
		message.channel.send('Pong.');
	},
};

This is the amount (in seconds) that the user will have to wait before being able to properly use that command again. You'll be using Collections again to store what you need.

In your main file, add in this line (preferably somewhere above your ready event):

const cooldowns = new Discord.Collection();

Again in your main file, directly above the try/catch, add in the following:

if (!cooldowns.has(command.name)) {
	cooldowns.set(command.name, new Discord.Collection());
}

const now = Date.now();
const timestamps = cooldowns.get(command.name);
const cooldownAmount = (command.cooldown || 3) * 1000;

if (timestamps.has(message.author.id)) {
	// ...
}

You check if the cooldowns Collection has the command set in it yet. If not, then add it in. Next, 3 variables are created:

  1. A variable with the current timestamp.
  2. A variable that .get()s the Collection for the triggered command.
  3. A variable that gets the necessary cooldown amount. If you don't supply it in your command file, it'll default to 3. Afterwards, convert it to the proper amount of milliseconds.

After that, create a simple if statement to check if the timestamps Collection has the author ID in it yet.

Continuing with your current setup, this is the complete if statement:

if (timestamps.has(message.author.id)) {
	const expirationTime = timestamps.get(message.author.id) + cooldownAmount;

	if (now < expirationTime) {
		const timeLeft = (expirationTime - now) / 1000;
		return message.reply(`please wait ${timeLeft.toFixed(1)} more second(s) before reusing the \`${command.name}\` command.`);
	}
}

Since the timestamps Collection has the author ID in it, you .get() it and then sum it up with the cooldownAmount variable, in order to get the correct expiration timestamp. You then check to see if it's actually expired or not.

If the expirationTime has not passed, you return a message letting the user know how much time is left until they can use that command again. As you will see shortly, the author ID should be deleted from the timestamps Collection upon expiration, but you should take this extra precaution to avoid potential UX problems.

Finally, if the timestamps Collection doesn't have the message author's ID (or if the author ID did not get deleted as planned), .set() the author ID with the current timestamp and create a setTimeout() to automatically delete it after the cooldown period has passed:

timestamps.set(message.author.id, now);
setTimeout(() => timestamps.delete(message.author.id), cooldownAmount);

Command aliases

It's a good idea to allow users to trigger your commands in more than one way; it gives them the freedom of choosing what to send and may even make some command names easier to remember. Luckily, setting up aliases for your commands is quite simple.

For this bit of the guide, we'll be using the avatar command as a target. Around Discord, your profile picture is referred to as an "avatar" - however, not everyone calls it that. Some people prefer "icon" or "pfp" (profile picture). With that in mind, let's update the avatar command to allow all 3 of those triggers.

Open your avatar.js file and add in the following line:

module.exports = {
	name: 'avatar',
+	aliases: ['icon', 'pfp'],

The aliases property should always contain an array of strings. In your main file, here are the changes you'll need to make:

-	if (!client.commands.has(commandName)) return;
-
-	const command = client.commands.get(commandName);
+	const command = client.commands.get(commandName)
+		|| client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
+
+	if (!command) return;

Making those two small changes, you get this:

User
User 02/01/2019
!avatar @User
Tutorial Bot
Tutorial Bot Bot 02/01/2019
User's avatar: https://cdn.discordapp.com/avatars/328037144868290560/1cc0a3b14aec3499632225c708451d67.png?size=2084
User
User 02/01/2019
!icon @User
Tutorial Bot
Tutorial Bot Bot 02/01/2019
User's avatar: https://cdn.discordapp.com/avatars/328037144868290560/1cc0a3b14aec3499632225c708451d67.png?size=2084

A dynamic help command

If you don't use a framework or command handler for your projects, you'll have a tough time setting up an always up-to-date help command. Luckily, that's not the case here. Start by creating a new command file inside your commands folder and populate it as you normally would.

module.exports = {
	name: 'help',
	description: 'List all of my commands or info about a specific command.',
	aliases: ['commands'],
	usage: '[command name]',
	cooldown: 5,
	execute(message, args) {
		// ...
	},
};

You're going to need your prefix variable a couple times inside this command, so make sure to require that at the very top of the file (outside of the module.exports bit).

const { prefix } = require('../config.json');

Inside the execute() function, set up some variables and an if/else statement to determine whether it should display a list of all the command names or only information about a specific command.

const data = [];
const { commands } = message.client;

if (!args.length) {
	// ...
}

// ...

You can use .push() on the data variable to append the info you want and then DM it to the message author once you're done.

Inside the if statement, this is what you'll need:

data.push('Here\'s a list of all my commands:');
data.push(commands.map(command => command.name).join(', '));
data.push(`\nYou can send \`${prefix}help [command name]\` to get info on a specific command!`);

return message.author.send(data, { split: true })
	.then(() => {
		if (message.channel.type === 'dm') return;
		message.reply('I\'ve sent you a DM with all my commands!');
	})
	.catch(error => {
		console.error(`Could not send help DM to ${message.author.tag}.\n`, error);
		message.reply('it seems like I can\'t DM you! Do you have DMs disabled?');
	});

There's nothing really complex here; all you do is append some strings, .map() over the commands Collection, and add an additional string to let the user know how to trigger information about a specific command.

Since help messages can get messy, you'll be DMing it to the message author instead of posting it in the requested channel. However, there is something very important you should consider: the possibility of not being able to DM the user, whether it be that they have DMs disabled on that server or overall, or they have the bot blocked. For that reason, you should .catch() it and let them know.

TIP

If you weren't already aware, .send() takes 2 parameters: the content to send, and the message options to pass in. You can read about the MessageOptions type here. Using split: true here will automatically split our help message into 2 or more messages in the case that it exceeds the 2,000 character limit.

TIP

Because the data variable is an array, you can take advantage of discord.js' functionality where it will .join() any array sent with a \n character. If you prefer to not rely on that in the chance that it changes in the future, you can simply append .join('\n') to the end of that yourself.

Below the if (!args.length) statement is where you'll send the help message for the command they specified.

const name = args[0].toLowerCase();
const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name));

if (!command) {
	return message.reply('that\'s not a valid command!');
}

data.push(`**Name:** ${command.name}`);

if (command.aliases) data.push(`**Aliases:** ${command.aliases.join(', ')}`);
if (command.description) data.push(`**Description:** ${command.description}`);
if (command.usage) data.push(`**Usage:** ${prefix}${command.name} ${command.usage}`);

data.push(`**Cooldown:** ${command.cooldown || 3} second(s)`);

message.channel.send(data, { split: true });

Once you get the command based off the name or alias they gave, you can start .push()ing what you need into the data variable. Not all commands will have descriptions, aliases, or usage strings, so you use an if statement for each of those to append them conditionally. After that, send back all the relevant information.

At the end of it all, you should be getting this as a result:

User
User 02/01/2019
!help
Tutorial Bot
Tutorial Bot Bot 02/01/2019
Here's a list of all my commands: args-info, avatar, beep, help, kick, ping, prune, server, user-info
You can send `!help [command name]` to get info on a specific command!
User
User 02/01/2019
!help avatar
Tutorial Bot
Tutorial Bot Bot 02/01/2019
**Name:** avatar
**Aliases:** icon,pfp
**Description:** Get the avatar URL of the tagged user(s), or your own avatar.
**Cooldown:** 3 second(s)

No more manually editing your help command! If you aren't completely satisfied with how it looks, you can always adjust it to your liking later.

Conclusion

At this point of the guide, you should now have a command handler with some very basic (but useful) features! If you see fit, you can expand upon the current structure to make something even better and easier for you to use in the future.

Resulting code

If you want to compare your code to the code we've constructed so far, you can review it over on the GitHub repository here .

Last Updated: 1/4/2019, 8:26:39 AM