Build A Bot (DiscordJS) — Better Logging And A Persistent Bot Config
Last time we left off, we had turned our simple bot into a bot factory, allowing us to spawn multiple bots with different configs. Those configs though were still pretty simple and not persistent. The user could not make any changes unless he made them directly to the config files.
Today we will spend a little bit of time on a prettier logger and then allow our bot to read and write his own config file on the server.
As always, the link to the finished code in GitHub is at the end of the article.
Credits: Today’s session will include code influenced and partly taken from the Liora Bot Project. Feel free to look at their code for more inspiration.
To start today’s session, we will implement a prettier solution for our console logs using Winston for the logging and chalk for the pretty colours.
You know the drill, grab what we need from npm and then let’s get busy.
npm i -S winston chalk
Winston is working with log levels and colours so let’s start by setting up some sensible defaults. Right now we will mostly work with error, warn and info but later on, those other levels will be used too.
Then we create a new logger instance with the basic setup and formating. Within the printf function, we can format our desired logout format. We want a timestamp here along with the log level and of course the logged message.
What’s left to do now is to wire it up with our bot object, finally getting rid of that
… and apply it in the places where we used the old and too simple logger and add our desired log levels and use chalk to paint the message where we see fit.
When you are done, your console logging should now look like this. If you want to see my choice of colours, check out this commit.
One thing that we can now get rid of is putting the tag everywhere by hand. We can let Winston handle that for us. Change the line where we assigned the
winston.createLogger() result and turn it into a fat arrow function that passes in the tag and returns the logger. This way we can include the tag in our printf output via
Now we need to add the tag (including a sensible default) to our log assignment and we’re done.
The difference in the visual output is minimal but in our code, we just removed a lot of redundancy.
Before we move on to the config, we still need to clean up a bit. There are still useless tags scattered throughout our code.
Read & Write Configs
Some of the tools we’re going to use for our config are prebaked in Node but in addition to those, we will need a way to work with json files, a way to create directories and to open files.
npm i -S jsonfile mkdirp opn
Let’s start by adding our new tools to the imports and defining a useful small sanitise function to radically clean up user input. We’ll use this later to create directories for the bots’ config files and we don’t want any funny characters in those directory names.
As we are going to implement proper configs now, let’s put some work in here and define a more detailed config schema. We can replace our old configSchema with this.
I’m using this schema to define what type of data the config accepts. This way we can run a basic check later to make sure every attribute resembles our requirements and we can include defaults in case the user has not set an attribute. Anything not in this list or of a wrong type will be discarded from the user input or old copies of the bot’s config. This way we can make sure that the current config is always compatible.
One advice, don’t put your token into the configSchema by hand. Include it in the initialConfig on bot start, as we had set it up last time. You would not want to hard code your bot’s token (or upload it to a public repository in any case!) as it better sits in the non-versioned .env file or environment config of your hosted project.
You should also add 2 lines to the rules in out .eslintrc file because we will need them soon to not get bugged by the linter about stuff that is working as intended / we want it to be.
1) Setting the config directory
We will need a way to keep track of config file paths to a certain directory. We simply store those in our bot object.
2) Run it once initially
Here we are using the sanitise function we defined earlier to take the bot name and use it to create a directory for each bot. If you run the script on your own PC during test and development, the config files will be written to your home/user directory instead of the server’s respective directory. Simply check for files starting with
.discord- followed by your bot's name.
3) Open generated config files for proofreading
Furthermore, I want to be able to open the files our script has created on the first run so that the user can check if his values have been merged correctly.
For this we will use something node provides us with,
opn and if one of the bots had his config generated for the first time, we will open the generated file exit the process. On the next run of our script, all bots will connect regularly.
4) Check the configSchema
We also need a function to validate the user-supplied config and merge it with our schema to generate the new bot config. We’ll go through our schema step by step, compare the existence and type of the respective attribute in the bot config and either delete or overwrite it depending on our checks. For objects, it will call itself recursively layer by layer.
5) The big one, loadConfig
This is the place where it all comes together. I broke it down into 5 subsections that we will go through piece by piece.
Our new loadConfig function will do a lot of things so I stripped it down to the shell and some comments to give you the outlines.
First of all, check for the existence of a config file. We will need this in a moment.
If no old config is found, we simply create a new config.json in our chosen location using
mkdirp, a small package resembling the desktop command
mkdir -p, and prepare it with the most basic and important fields from what we are passing in on project start; discordToken, Prefix and
Next step, we load the config file, no matter if it’s an old one or we just created it.
Now call our configIterator with the config we read from the disk and compare it to our schema. As previously written, this makes sure that no old or mismatched values remain in the config once we decide to change the schema in the future.
Write the checked and clean config back to the server.
Last but not least, reload the config from the directory and check one last time. If everything is fine, execute the callback to continue and otherwise abort with an error.
If you want to make sure you got everything, have a look at the finished function in all it’s glory and complexity.
Using nodeJS for the first time to access and work with files can be a daunting task so depending on where you are/were with your experience, I hope I was able to keep it nice and basic and understandable.
Our Bot(s) can now be started by creating a new or loading an existing config file. Next time we will add some commands that let the users with the right roles and permissions change the config on the fly, add new tags and maybe even access those from a dashboard… stay tuned.
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:
- React Quick Start with Typescript, Redux and Router
- 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.