The making of the Resonite sessions Discord bot
If you are a Resonite player and are in the Discord guild, you might be familiar with the #active-sessions
channel, in which a bot displays the 10 most popular sessions on Resonite as well as some stats.
What you might not know, is that I’m the author of this bot, that I originally started as just a small oneshot project to have some fun.
For some background, when Neos was still a thing (technically still is, but in what state), a bot like this was in the Discord guild, showing sessions and game stats like server metrics and session counts.
When Resonite was released, the channel was there, however, no metrics or posts were ever made, saying that the bot would be revived at some point in the future(TM).
At the time, I was a bit bored and wanted a new project, so I decided to start experimenting with .NET Discord bots and in term set myself the objective to re-create the original bot.
Why .NET? One reason being that I use .NET all the time as it’s fairly easy to use, the other one being that most of the Resonite community also knows .NET due to being used to make mods and whatnot.
The bot itself is fairly simple and divided in multiple parts built around the .NET Hosted Services:
- Discord client service - handles the connectivity to Discord
- Interaction service - provides the command handler for the few commands the bot has
- Startup service - sets up some bot must-haves like the logger and other data
- Message service - the core functionality of the bot that runs all the logic that makes the magic happen
- Healthcheck service - this one is optional, but important when hosting the bot in Docker
Let’s go through all of those in detail and see why they were made that way.
The Discord-related services#
I’m gonna group those together as they belong to the same component really: handling Discord-related shenanigans.
The bot has a few commands:
/info
which shows some info about the bot/setchannel
which sets the channel in which you want the notifications/which
which shows which channel is set as the notification one/unregister
which unsets the notification channel/setting
allows you to toggle some settings like if you want thumbnails or not
All of those commands (with the exception of /info
) are admin-only.
This part is honestly fairly boring and straightforward, the rest just passes a Discord bot token, connects to the chat service and tries to log in.
await _client.LoginAsync(TokenType.Bot, token);
await _client.StartAsync();
The message service#
Now this is the juicy stuff.
This part handles:
- Retrieving the info from the Resonite API
- Formats it neatly into a message
- Generates embeds from the session info
- Storing which messages were sent where in a DB
- Checking if messages can be updated
- Updating said messages with the new data
First off, the bot uses a relatively simple SQLite DB to avoid everything being hardcoded. The first versions were using direct channel IDs, but this is far from ideal if you want something modular without having to host multiple copies.
The DB basically stores the guild ID, channel ID and settings for the bot to send the updates in the right place, containing what we want.
Speaking of settings, there is only one so far: show the session thumbnails or not. The reason for this is a difference between the Discord & Resonite ToS. While nipples aren’t considered sexual on Resonite, they are on Discord, meaning having a session thumbnail showing nipples on Discord without gating the channel to 18+ users would be violating the ToS of the platform.
The message sending, updating and checking logic works this way:
One thing I am quite proud of is how stable the bot it, nowadays it rarely, if ever, crashes alone, which it used to do quite often.
The bot is made to handle errors gracefully and never shut down or crash the program unless something really, really warrants it. When running this bot, all the errors that would normally crash it are instead logged with an error message and stacktrace to make it easy to debug.
Another thing to note is that the database schema hasn’t been updated since the bot basically released and touching it is considered a very last resort thing. Having the DB break during an upgrade would be disastrous, requiring all admins to re-set the notifications channel. As they say, if it ain’t broken, don’t fix it.
Out of all the variables in the mix, Discord is the most unstable one, having lots of outages, sometimes lasting hours at a time, just being slow for no reason whatsoever or thinking existing things (such as channels or messages) don’t exist even though they do.
This is why the whole checking logic exists, it will first check if the channel exists, if it does will check if the message exists, and if it does, try to update it. If it fails at any point, it will try again for a while then delete the message, try to re-send it, and ultimately, if this fails, delete the channel from the DB and the admin will have to re-set the notification channel again.
The re-try was implemented after some issues raised from Discord:
- The bot would forget about the message (because Discord said it didn’t exist anymore) and would send a second one in the same channel or over and over until restarted
- Sometimes the checks would fail on first try and delete everything gracefully without notifying anybody
- Bot would crash because everything “ceased” to exist
On the Resonite side, if an error happens while contacting the API, the bot will just skip this cycle and try updating the next time (one minute by default). This used to crash the bot (whoops) in the early days.
The latest addition made was the Docker healthcheck, given recently the bot crashed in the main Resonite guild (GH-3521) and no monitoring was triggered.
Now the bot has a small HTTP server running, simply returning the date that a curl
instance will check every 30 seconds.
The CI/CD#
It’s no secret that I love GitLab as a software. I also work daily on and with it in my day-to-day job.
The CI/CD used in this project is extensive, but classic:
- Secret detection
- SAST scan
- Dependency scanning
- NuGet build
- NuGet deployment
- ARM64 and x86 Docker builds
- Release publishing
The first three are kinda explicit, and will warn if any secrets have been pushed, if any unsafe codepaths are detected or if any dependencies needs updating.
Now the most important thing to highlight are the separated Docker builds for the two architectures. I originally tried combining the builds into a single one as you would do by specifying multiple architectures in buildx, however this did not work.
An error when building ARM on x86 (with virtualization) and vice versa would always arise, though the same command would work for other projects.
To avoid errors when doing things manually, the release process is also automated, triggering when a tag is detected. It will basically build as usual with the version tag and then publish the release following a markdown template. It will also automatically fill-in some details like image tags, etc from the changelog.
Fun fact, the bot is Open-Source under the MIT license, meaning you are welcome to host your own version.
Some stats about the project so far:
- 47 pull requests merged
- 97 commits (PRs are squashed)
- 16 releases
- 6+ months of existence
- 10k (roughly) of users served in the Resonite Discord alone
- Way too many hours sunk into overengineering this
What’s to come? Nothing much really, this bot has basically achieved its goal of having the active session list published somewhere.
The project is basically in maintenance mode, housed under the Crystalline Shard development group which I am one of the founders.
Now for the self-criticism: if I had to restart the project from scratch, I would probably opt into an even less complex design. Some services are really huge files that only increased in complexity with time.
Currently nothing too bad, but I think a refactor would be warranted to decrease complexity and make it more maintainable.
I wouldn’t touch the language though, since the bot’s footprint is really small, only about 30Mb to 50Mb of RAM used total during normal runtime.
In the end, this bot is a really fun project to make and maintain, and I’m extremely happy that it got canonized and used in an official manner.
For a quick roadmap:
- GL-33 have someone completely review the codebase
- GL-39 disable unneeded intents for the bot (on hold for evaluation)
- GL-48 solve a mystery crash that randomly happens… sometimes
Let’s see in about 6 more months to see how everything evolved with even more bugs being squashed.