Skip to main content

Implementing a discord.py like Cog system

Developer who come from the Python ecosystem may be familiar with the "Cog" system that discord.py has implemented. This system allows you to group commands, listeners and other pieces into a Cog (also sometimes called "module"), which can then be loaded and unloaded as a whole. This is a useful system for large bots, as it allows you to easily split your code into multiple files, and only load the parts you need.

While Sapphire does not natively have a built-in Cog system, it is possible to implement one yourself. This guide will teach you how to do this.

For this guide we will register an audio cog which will have commands and listeners related to playing music through your bot. This cog will be called audio and have subfolders of commands and listeners. That is to say, the general structure of your bot will be something like this:

├── node_modules
├── package.json
└── src
├── audio
│ ├── commands
│ │ ├── play.ts
│ │ └── stop.ts
│ └── listeners
│ ├── audioStarted.ts
│ └── audioStopped.ts
└── index.ts

The best way to ensure that Sapphire will register this audio folder and all store folders under it is by first extending the SapphireClient and overloading the constructor:

const { SapphireClient } = require('@sapphire/framework');

class MyOverloadedClient extends SapphireClient {
constructor(options) {
super(options);
}
}
module.exports = {
MyOverloadedClient
};

Next, we need to register the path for the audio folder. We only need to register this parent folder, we not need to subsequently also register the commands, listeners, etc folders that reside within it. We can do this by using the registerPath method of a StoreRegistry. Here we use the getRootData method from @sapphire/pieces which is also the method that Sapphire uses internally to determine the base folder where the stores are (i.e. dist for TypeScript users, src for JavaScript users). We then use this the result of this getRootData function to join the root folder with the folder name audio, which will handle the registering of the folder.

const { SapphireClient } = require('@sapphire/framework');
const { getRootData } = require('@sapphire/pieces');
const { join } = require('node:path');

class MyOverloadedClient extends SapphireClient {
rootData = getRootData();

constructor(options) {
super(options);

this.stores.registerPath(join(this.rootData.root, 'audio'));
}
}
module.exports = {
MyOverloadedClient
};

Once the audio folder is registered this way, Sapphire will automatically register all the store folder under it. That is to say, pieces in audio/commands will be parsed as commands, pieces in audio/listeners will be parsed as listeners, etc.