Over the last year, Mozilla has decided to shut down the IRC network and replace it with a more modern platform. To my greatest delight, the Matrix ecosystem has been selected among all the possible replacements. For those who might not know Matrix, it's a modern, decentralized protocol, using plain HTTP JSON-formatted endpoints, well-documented, and it implements both features that are common in recent messaging systems (e.g. file attachments, message edits and deletions), as well as those needed to handle large groups (e.g. moderation tools, private rooms, invite-only rooms).
In this post I reflect on my personal history of writing chat bots, and then present a panel of features that the bot has, some user-facing ones, some others that embody what I esteem to be a sane, well-behaved Matrix bot.
Over time, it slowly transformed into an IRC bot framework, with modules you could attach and configure at startup, setting which rooms the bot would join, what should be the cooldowns for message sending (more on this later), and so much more! Hence it was renamed meta-bot.
I find making bots a fun activity, since once you've passed the step of connecting and sending messages, the rest is mostly easy (cough cough regular expressions cough cough) and creative work. And it's unfortunately easy to be reckless too.
At this time, I never considered the potentially bad effects of quoting text from a random source, viz. fetching tweets from the @horsejs account. If the source would return a message that was inconsiderate, rude, or even worse, aggressive, then the bot would replicate this behavior. It is a real issue because although the bot doesn't think by itself and doesn't mean any harm, its programmers can do better, and they should try to avoid these issues at all costs. A chat bot replicates the culture of the engineers who made it on one hand, but also contributes to propagating this culture in the chat rooms it participates in, normalizing it to the chat participants.
My bot happened to be well-behaved most of the time... until one time where it was not. After noticing the incident and expressing my deepest apologies, I deactivated the module and went through the whole list of modules, to make sure none could cause any harm, in any possible way. I should have known better in the first place! I am really not trying to signal my own virtue, since I failed in a way that should have been predictable. I hope by writing this that other people may reflect about the actions of their bots as well, in case they could be misbehaving like this.
When we moved over to Matrix, the bots unfortunately became outdated, since the communication protocol (IRC) they were using was different. We could have ported them to the Matrix protocol, but the Not-Invented-Here syndrom was strong with this one: I've been making bots for a while, and I was personally interested in the Matrix protocol and trying out the JS facilities offered by the Matrix ecosystem.
So I've decided to write Botzilla, a successor in spirit to meta-bot and mrgiggles, written in TypeScript. This is a very unofficial bot, tailored for Mozilla's needs but probably useful in other contexts. I've worked on it informally as a side-project, on my copious spare time. Crafting tools that show useful to other people has been sufficient a reward to motivate me to work on it, so it's been quite fun!
Botzilla's logo, courtesy of Nical
Let's take a look at all the features that the bot offers, at this point.
This was a feature of Firebot, and easy enough to replicate, so this was the
test feature for the Matrix bot. When saying
!uuid, the bot will
automatically generate a unique id (using uuid v4), guaranteed GMO-free and
usable in any context that would require it. This was the first module,
designed to test the framework.
Mozilla developers tend to interact a lot with the continuous integration
trees, because code is sometimes landed, sometimes backed out (sorry/thank you
sheriffs!), sometimes merged across branches. This leads to the integration
trees being closed. Before we had the feature to automatically land patch
stacks when the trees reopened, it was useful to be able to get the open/close
status of a tree. Asking
!treestatus will answer with a list of the status of
some common trees. It is also possible to request the status of a particular
tree, e.g. for the "mozilla-central" tree, by asking
mozilla-central (or just
central, as a handy shortcut).
If you have ever interacted with Mozilla's code, there's chances that you've
used Bugzilla, and mentioned bug numbers in conversations. The bot caches any
bug XXX and will respond with a link to this bug, the
nickname of the person assigned to this bug if there's one, and the summary of
this bug, if it's public. This is by far the most used and useful module, since
it doesn't require a special incantation, but will react automatically to a lot
of messages written with no particular intent (see below where it's explained
how to not be spammy, though).
This was a very nice feature that mrgiggles had: ask for potential reviewers
for a particular file in the Gecko source tree and get a list of most recent
reviewers. Botzilla replicates this, when seeing the trigger:
who can review
js/src/wasm/WasmJS.cpp?. The list of potential reviewers is extracted from
Mercurial logs, looking for the N last reviewers of this particular file.
As a bonus, there's no need to pass the full path to the file, if the file's
name is unique in the tree's source code. Botzilla will trigger a search in
Searchfox, and will use the unique name in the result list, if there's such a
unique result. The previous example thus can be shortened to
who can review
WasmJS.cpp? since the file's name is unique in the whole code base.
It is possible for a room administrator to "connect" a given Matrix room to a
Github repository. Later on, any mention of issues or pull requests by their
#1234, will make Botzilla react with the summary and a link to
the issue/PR at stake.
This also works for Gitlab repositories, with slight differences: the
administrator has to precise what's the root URL of the Gitlab instance (since
Gitlab can be selfhosted). Issues are caught when numbers follows a
while merge requests are caught when the numbers follow a
An administrator can configure a room to tie it up to a Twitter (respectively
Mastodon) user account, using API tokens. Then, any person with an
administrative role can post messages with
!tweet something shocking for the
!toot something heartful for the mammoth site). This
makes it possible to allow other people to post on these social networks
without the need to give them the account's password.
Unfortunately, the Twitter module hasn't ever been tested, since when I've tried to create a developer account, Twitter accepted it after a few days but then never displayed the API tokens on the interface. The support also never answered when I asked for help. Thankfully Mastodon can be self-hosted and thus it is easier to test. I'm happy to report that it works quite well!
It is quite common in teams to set up regular standup meetings, where everyone in the team announces what they've been working on in the last few days or week. It also strikes me as important for personal recognition, including towards management, to be able to show off (just a bit!) what you've accomplished recently, and to remember this when times are harder (see also Julia Evans' blog post on the topic).
There's a Botzilla module for this. Every time someone starts a message with
confession:, then everything after the colon will be saved in a database
(...wait for it!). Then, all the confessions are displayed on the
Histoire 2 website, with one
message feed per user. Note it is possible to send confessions privately to
Botzilla (that doesn't affect the frontend though, which is open and public to
all!), or in a public channel. Public channels somehow equate to team members,
so channels also get their own pages on the frontend.
Now the fun/cursed part is how all of this works. This was implemented in mrgiggles, and I liked it a lot, since it required no kind of backend or frontend server. How so? By (ab)using Github files as the database and Github pages as the frontend. Sending a confession will trigger a request to a Github endpoint to find a database file segregated by time, then it will trigger another request to create/modify it with the content of the confession. The frontend then uses other requests to public Github APIs to read the confessions before dynamically rendering those. Astute readers will notice that under a lot of confession activity, the bot would be a bit slowed down by Github's API use rates. In this case, there's some exponential backoff behavior before trying to re-send unsaved confessions to Github. Overall it works great, and API limitation rates have never quite been a problem.
In addition to all the user-facing features, the bot has a few other interesting attributes that are more relevant to consider from a framework point of view. Hopefully some of these ideas can be useful for other bot authors!
Every time the bot is invited to a channel, be it public or private, it will join the channel, making it easy to use in general. It was implemented for free by the JS framework I've been using, and it is a definitive improvement over the IRC version of the bot.
Sometimes Matrix rooms are upgraded to a new version of the room. The bot will try to join the upgraded room if it can, keeping all its room settings intact during the transition.
To avoid spamming the channel, especially for modules that are reactions to other messages (think: bug numbers, issues/pull requests mentions), the bot has had to learn how to keep quiet. There are two rules triggering the quieting behavior:
- if the bot has already reacted less than N minutes ago (where N is a configurable amount) in the same room,
- or if it has already reacted to some entity in a message, and there's been fewer than M messages in between the last reaction and the last message mentioning the same entity in the same room (M is also configurable)
If any of these two criteria is met, then the bot will keep quiet and it will not react to another similar message. The combination of these two has proven over time to be quite solid in my experience, based on observing the bot's behavior and public reactions to its behavior.
Some similar mechanism is used for the confession module: on a first confession, the bot will answer with a message saying it has seen the confession, including a link to where it is going to be posted, and will add an emoji "eyes" reaction to the message. Posting this long form message could be quite spammy, if there's a lot of confessions around the same time. Under the same criteria, it will just react with an "eyes" emoji to other confessions. Later on, it'll resend the full message, once both criterias aren't blocking it from doing so.
The bot can be administrated, by discussing with it using the
This can happen in both a private conversation with it, or in public channels,
yet it is recommended to do so in private channels. To confirm that an admin
action has succeeded, it'll use the thumbs-up emoji on the message doing the
To have a single administrator for the bot would be quite the burden, and it is not resilient to people switching roles, leaving the company, etc. Normally you'd solve this by implementing your own access control lists. Fortunately, Matrix already has a concept of power levels that assigns roles to users, among which there are the administrator and moderator roles.
The bot will rely on this to decide to which requests it will answer. Somebody
marked as an administrator or a moderator of a room can administrate Botzilla
in this particular room, using the
commands. There's still a
super-admin role, that must be defined in the configuration, in case things go
awry. While administrators only have power over the current room, a super-admin
can use its super-powers to change anything in any room. This decentralization
of the administrative roles makes it easy to have different settings for
different rooms, and to rely a bit less on single individuals.
In general, the bot contains a key-value store implemented in an sqlite database, making it easy to migrate and add context that's preserved across restarts of the bot. This is used to store private information like user repository information and settings for most rooms. Conceptually, each pair of room and module has its own key-value store, so that there's no risk of confusion between different rooms and modules. There's also a key-value per-module store that's applicable to all the rooms, to represent global settings. If there's some non-global (per room) settings for a room, these are preferred over the global settings.
Each chat module is implemented as a ECMAScript module and must export an help
string along the main reaction function. This is then captured and aggregated
as part of an
!help command, that can be used to request help about usage of
the bot. The main help message will display the list of all the enabled
modules, and help about a specific module may be queried with e.g.
If I were to start again, I'd do a few things differently:
- use a real single-page app framework for the Histoire website. Maybe? I mean I'm a big fan of VanillaJS, but using it means re-creating your own Web framework like thing to make it nice and productive to use.
- despite being a fun hack, using Github as a backend has algorithmic limitations, that can make the web app sluggish. In particular, a combined feed for N users on M eras (think: periods) will trigger NxM Github API requests. Using a plain database with a plain API would probably be simpler at this point. This is mitigated with an in-memory cache so only the first time all the requests happen, but crafting my own requests would be more expressive and efficient, and allow for more features too (like displaying the list of rooms on the start view).
- provide a (better) commands parser. Regular expressions in this context are a bit feeble and limited. Also right now each module could in theory reuse the same command triggers as another one, etc.
- implement the chat modules in WebAssembly :-) In fact, I think there's a
whole business model which would consist in having the bot framework
including a wasm VM, and interacting with different communication platforms
(not restricted to Matrix). Developers in such a bot platform could choose
which source language to use for developing their own modules. It ought to be
possible to define a clear, restricted, WASI-like capabilities-based
interface that gets passed to each chat module. In such a sandboxed
environment, the responsibility for hosting the bot's code is decoupled from
the responsibility of writing modules. So a company could make the platform
available, and paying users would develop the modules and host them. Imagine
git pushing your chat modules and they get compiled to wasm and deployed on the fly. But I digress! (Please do not forget to credit me with a large $$$ envelope/a nice piece of swag if implementing this at least multi-billion dollars idea.)
I'd like to finish by thanking the authors of the previous Mozilla bots, namely
sfink and glob: your puppets have been incredible sources of
inspiration. Also huge thanks to the people hanging in the
chat room, who've answered questions and provided
help in a few occasions.
I hope you liked this presentation of Botzilla and its features! Of course, all the code is free and open-source, including the bot as well as the histoire frontend. At this point it is addressing most of the needs I had, so I don't have immediate plans to extend it further. I'd happily take contributions, though, so feel free to chime in if you'd like to implement anything! It's also a breeze to run on any machine, thanks to Docker-based deployment. Have fun with it!