Skip to content

Instantly share code, notes, and snippets.

@AllGistsEqual
Created January 25, 2021 21:29
Show Gist options
  • Save AllGistsEqual/0a612604b96bafd14e84f6334997bed0 to your computer and use it in GitHub Desktop.
Save AllGistsEqual/0a612604b96bafd14e84f6334997bed0 to your computer and use it in GitHub Desktop.

Build A Bot (DiscordJS) - A scalebale setup with command modules

Last week on "Build A Bot"

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.

Cleaning up

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.

https://gist.github.com/30b6b0a4bf70acae7852bd5b85a06954

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.

https://gist.github.com/dd55471632476e24fdfbdc6dbbf689d5

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.

https://gist.github.com/797e73e110355126399663f65a8367bc

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:

  • imports
  • setup
  • functions
  • event handlers
  • the call to the load function

Running npm start on the command line will boot our bot like it did last time. So far so good.

GitHub Commit

Extracting our command logic

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.

https://gist.github.com/33d922d05e70250f1f7dec5aa9c1e806

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.

https://gist.github.com/118fffcd74696cd5c48b2d2ed675e23c

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.

https://gist.github.com/94bd32d3dc42da586b0ebdab282d994b

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 new discord.Collection().

https://gist.github.com/dfb7f6dea1d7ac86275324cfdd80d0ce

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.

https://gist.github.com/99df338d4a0591dcd83c67d714010a2b

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.

https://gist.github.com/d2e8c1bc4f0ecc3f70b443a1595a8c83

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 !ping...

Adding a default config

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.

https://gist.github.com/265c7bf9eca97f4ac3bdbc932635969f

With this done, simply add the bot to the command handler execution.

https://gist.github.com/14538b05998c8ee45000c67489db107a

Finally, a new command - roll the dice

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.

https://gist.github.com/b87b4889981779ee16d04096a1f4cf31

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.

https://gist.github.com/98af2a7fd8a2460656f7b7180a1f88a2

This command allows the user to use different combinations of the command and arguments. The following 4 patterns are valid:

  • !dice
  • !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.

https://gist.github.com/bf22ea27e4291114dd34ac3263363cf3

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.

https://gist.github.com/602976d053ca2b1d0ddd5ad1cec7ecf8

Wrapping up

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment