Build A Bot (DiscordJS) — A scalable setup with command modules
In our last session, we have created a functional discord bot with some basic commands, a small config and linked everything to our discord application/bot setup in the discord developer portal using a generated token.
Today we will clean up our central index.js file, make it more readable and scaleable and move all our existing commands to a separate folder for import. When all else is done, we will also start expanding the functionality of our bot by adding a more complex command to play with on our test server and give you a better understanding of the wide range of functionality, tools and commands possible with discord bots.
If you want to grab or compare with the code from the last session, here’s the GitHub link to the respective tag.
First of all, we will replace our simple bot client instance with a more elaborate bot object. Within this new object, we will mirror our discord.Client() as the client and as we are planning to expand our logging in the future, we are hiding our interim console.log behind bot.log with the comment to disable eslint for the no-console rule as before. That way we can use this for our logging and when we later introduce a better logger, we can do it right there.
For comparison, I’ve included the diff to our old file. At the end of each step, you will find a GitHub link to the commit/changes to compare with your own code.
Next thing on our list is to add some functions that will be triggered by the event handlers as be the backbone of our bot. Right now this might seem to be “overkill” or premature optimisation but if we do this now, the code will be easier to read AND easier to extend and build on.
This is basically nothing new, it’s just our load() function and “on ready” event listener from last week, using our new structure.
We will do the same with our “on message” event listener code. Right now we won’t change a single line of code within this section but we will wrap it in a function before we bind it to the actual event listeners.
As you see we are using simple log calls for all sorts of error states and issues while we bind our onConnect and onMessage function to their respective event handlers.
The last line is really important as that is the line that actually calls our bot once everything else is defined and set up.
For a cleaner separation in our file we now have the following order:
- event handlers
- the call to the load function
npm start on the command line will boot our bot like it did last time. So far so good.
As you see, even with the basic setup, our index file is already close to 100 lines long and we should try to keep our files both as short as possible AND as focused as possible. With every new command that we add to the bot, this file would get more and more verbose so let’s move all those existing commands to a new folder and import them from there.
Under src/ create a new folder called “commands” and add new, empty files for our commands and a central index.js file.
The ping is, again, the easiest case. Simply create a module.exports object with name, description and the execution of our command. https://gist.github.com/37710ee875d0556e42974d5cc0756705
Moving on to our “who” command, we run into the first issue. We need to import the config again to have access to the name variable.
Importing to export
Repeat the same process for the “whois” command and then open the new src/commands/index.js file. We need to import all our modules and combine them in one object that we will use in our main bot code.
With this in place, we can now import all commands in our main file and add them to our bot. To do so, we will create a new collection from via
In our bot.load function we will add a new step before logging our bot into the discord servers and create a new set in our collection for each command we have.
The last thing to do in this step is to replace the old commands in our onMessage function and add our new and shiny collection to it. There is a minor caveat (or change) right now but I’ll explain it after you had a look at the code.
What is all this code, you might ask? Well, let’s see. First of all, we still check for our prefix. Then we split the message into an array and store that as our args. This will be handy later on when we build commands such as
!tag add <tag name> <tag message>.
Then we shift() the first part out of that array as our command (mutating our args array), strip it from the prefix. If we can’t find the command in our command list, we can exit directly. Otherwise, we can attempt to execute the command from the collection and to be extra safe here, we wrap that in a try/catch.
When writing this part of the tutorial I ran into the issue of the missing “name” for the !who command and luckily the try/catch error directly helped me identify the issue and still keep the bot running. I would otherwise have seen a very angry node error message about an unhandled exception.
What was the caveat?
Our ping will now also require the prefix. There would have been multiple possible solutions for this issue but none of them felt clean and as I do not have this bot deployed anywhere yet, I can simply change this right now.
ping is now
Previously, when we added the ping and who/whois commands, we only use the message parameter. We’ve just added the “args” array too but for allowing our functions to be more flexible and have better integration with discord, let’s add our bot object to the command handler as well.
Why? Because we can define stuff like our default colours for user feedback (success, error etc.), variables like the bot “name” field we were missing earlier and much more in a config attribute and access those values where we need them. This will help us make adjustments later and prevent redundant code and settings by keeping those values in a central place.
So let’s make another change to the src/index.js by adding default colours to the bot settings and adjusting our command execution call to pass in the bot object as well.
With this done, simply add the bot to the command handler execution.
As a fun exercise, we will add a
!dice command that will let the user choose a number and type of dice and have the bot roll them.
I’ve previously written a dice function called
getDiceResult() as an exercise. I've included and adjusted it to generate the results and texts we need to send a nice and well-formatted message into the chat. For reference, here is the schema of the return value of said function.
The really interesting part in the new command is the embedded message provided by discordJS. There is a lot of stuff you can add to an embed and there are even multiple ways to achieve the same result when defining the fields ( read the official docs) but for now, we will restrict ourselves to the title, colour and content fields.
This command allows the user to use different combinations of the command and arguments. The following 4 patterns are valid:
- !dice [1–10]
- !dice [1–10]d[2, 3, 4, 6, 8, 10, 12, 20, 100]
- !dice [1–10]d[2, 3, 4, 6, 8, 10, 12, 20, 100] “optional message”
Let’s look at the getDiceResult function in detail. We pass in the args and receive an object with strings but what happens inside? If you read the comments below, you will see that we try to get the count of “rolls” and type of “sides” of the command with some defaults, check them for our ruleset and then calculate the result.
If the user passes in an invalid argument, we generate an error response and cancel the execution.
To check if our bot handles all cases as expected, here are a few variations and their results.
Tracing back our steps
With this we are done with the new command (I know, we skipped the !help part today) but with the new config we made for the last part, we can return once again to the
!who command file and make ONE final edit, getting rid of the additional import and instead using the bot param from the execution call.
We’ve cleaned up our central index file, created a clear separation of code sections based on their intent and introduced a command collection to handle all user input based on a set of imported commands from separate files. Furthermore, we’ve added a new config and prepared our user messages in a way that allows us to easily scan for keywords and parameters.
Next time I will guide you through the process of writing a scaleable and self-updating help command as well as adding our first user management/administration commands to make the bot a bit more useful.
Some words about me:
If you want to see more of my work and progress, feel free to follow me and check out my other articles. If you clap feverishly for the articles you like most, it will be easier for me to decide which directions to pursue in following articles so use your ability to cast a vote for future content.
I’m also currently working on other series covering complex React Native Setups using Typescript and scalable apps with Redux, where I’ll go into details about how and why I do stuff the way I do as well as some articles on my experience in building games for web and mobile with React.
Here are some of my recent topics:
- Linting/Prettier with Typescript
- Redux + Toolkit with Typescript
- clean and simple Redux, explained
- Game Theory behind Incremental Games
- Custom and flexible UI Frames in React Native
And if you feel really supportive right now, you can always support me on patreon, thus allowing me to continue to write tutorials and offer support in the comments section.