This repository contains the source code for the Wheatley bot, made for the Together C & C++ discord server.
indexes/
Code for processing cppreference and man7 data to create a searchable indexsrc/
Main source code for the botalgorithm/
Algorithmic utilities for the bot, such as levenshtein distancecomponents/
Bot componentsinfra/
Bot infrastructure, such as database interactiontest/
Test caseswheatley-private/
Private components, these are primarily internal moderation and administration tools such as raid detection and handling.
The bot is very modular and most components are completely independent of other components.
In order to run the bot locally you'll need to create a bot and setup some basic information for Wheatley:
- Go to https://discord.com/developers/applications and create an application
- Move or copy
auth.default.json
toauth.json
- Go to Application Settings > Bot
- Request or reset your bot's token, copy it to
token
inauth.json
- Under Privileged Gateway Intents, select presence intent, server members intent, and the message content intent
- Request or reset your bot's token, copy it to
- Go to Application Settings > Installation
- Select "Guild Install"
- Select "Discord Provided Link"
- Select scopes:
applications.commands
andbot
- Select permissions:
Administrator
- Setup a test server (ask on TCCPP for help if needed)
- Install your bot on a test server
Once that is setup, the easiest way to get started with local bot development is to run make run-dev-container
, builds
a container and runs it with podman. Once the container builds, run make dev
in the container's shell and you should
be good to go.
The bot relies on a lot of server-specific information, such as IDs for channels and roles. Components which do not rely on any server-specific information are marked as freestanding. When developing locally, configure the bot as freestanding (see below). If you are working on a component which relies on server specific information, the best solution currently is the following:
- Look at what server-specific pieces the component needs (channels, roles, etc.) and create copies in your development
server. Server-specific pieces needed by the component can be found easily by searching for
this.wheatley.channels.
andthis.wheatley.roles.
. - Update constants in
src/wheatley.ts
as needed - all constants are at the top of the file. - Set the component to be enabled in freestanding mode with:
static override get is_freestanding() {
return true;
}
Secrets and other bot info must be configured in the auth.json
file. An example looks like:
{
"id": "<bot id>",
"guild": "<guild id>",
"token": "<discord api token>",
"mongo": {
"user": "wheatley",
"password": "<mongo password>",
"host": "127.0.0.1", // optional
"port": 27017 // optional
},
"freestanding": false, // optional
"passive": false // optional
}
Mongo credentials can be omitted locally if you don't need to work on components that use mongo. freestanding: true
can be specified to turn on only components which don't rely on channels etc. specific to Together C & C++ to exist.
Freestanding mode also disables connecting to MongoDB. passive: true
can be specified to run the bot in passive mode,
where it will not advertise or react to commands, and only load components that are marked as passive (useful for
testing new features in a second instance without interfering with a running primary instance).
The bot is built of modular components. The BotComponent base class defines the following api:
export class BotComponent {
static get is_freestanding() {
return false;
}
static get is_passive() {
return false;
}
// Add a command
add_command<T extends unknown[]>(
command:
| TextBasedCommandBuilder<T, true, true>
| TextBasedCommandBuilder<T, true, false, true>
| MessageContextMenuCommandBuilder<true>
| ModalHandler<true>,
);
// Called after all components are constructed and the bot logs in, but before bot commands are finalized
async setup();
// Called when Wheatley is ready
async on_ready();
// General discord events
async on_message_create?(message: Discord.Message): Promise<void>;
async on_message_delete?(message: Discord.Message | Discord.PartialMessage): Promise<void>;
async on_message_update?(
old_message: Discord.Message | Discord.PartialMessage,
new_message: Discord.Message | Discord.PartialMessage,
): Promise<void>;
async on_interaction_create?(interaction: Discord.Interaction): Promise<void>;
async on_guild_member_add?(member: Discord.GuildMember): Promise<void>;
async on_guild_member_update?(
old_member: Discord.GuildMember | Discord.PartialGuildMember,
new_member: Discord.GuildMember,
): Promise<void>;
async on_reaction_add?(
reaction: Discord.MessageReaction | Discord.PartialMessageReaction,
user: Discord.User | Discord.PartialUser,
): Promise<void>;
async on_reaction_remove?(
reaction: Discord.MessageReaction | Discord.PartialMessageReaction,
user: Discord.User | Discord.PartialUser,
): Promise<void>;
async on_thread_create?(thread: Discord.ThreadChannel): Promise<void>;
}
A component should extend BotComponent and override methods as needed.
For the bot I've created a command abstraction that internally handles both text and slash commands. An example component and command looks like this:
export default class Echo extends BotComponent {
static override get is_freestanding() {
return true;
}
constructor(wheatley: Wheatley) {
super(wheatley);
this.add_command(
new TextBasedCommandBuilder("echo")
.set_description("echo")
.add_string_option({
title: "input",
description: "The input to echo back",
required: true,
})
.set_handler(this.echo.bind(this)),
);
}
async echo(command: TextBasedCommand, input: string) {
M.debug("Received echo command", input);
await command.reply(input, true);
}
}
TextBasedCommandBuilder
defines the following methods:
- Configuration:
set_description(description)
set_handler(handler)
set_slash(slash)
set_permissions(permissions_bigint)
- Options:
add_string_option(parameter_options)
add_number_option(parameter_options)
add_user_option(parameter_options)
add_role_option(parameter_options)
Each parameter_options
must contain at least a title and description.
A user option will translate to a user picker in a slash command and in text either a user mention or a user id is accepted. A role option will translate to a role picker in a slash command and in text either a case-insensitive role name is accepted. For a string option, if it's the last string option the entire remaining command body is taken (after other arguments). If the string does not correspond to the last positional option, either one whitespace-terminated word is read or a regex can be specified.
The bot uses MongoDB. It previously used a giant json file (the migration script is located in the scripts folder). The development docker container sets up and orchestrates a mongodb server for the bot to use.