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:
- CommonJS
- ESM
- TypeScript
const { SapphireClient } = require('@sapphire/framework');
class MyOverloadedClient extends SapphireClient {
constructor(options) {
super(options);
}
}
module.exports = {
MyOverloadedClient
};
import { SapphireClient } from '@sapphire/framework';
export class MyOverloadedClient extends SapphireClient {
constructor(options) {
super(options);
}
}
import { SapphireClient } from '@sapphire/framework';
import type { ClientOptions } from 'discord.js';
export class MyOverloadedClient extends SapphireClient {
public constructor(options: ClientOptions) {
super(options);
}
}
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.
- CommonJS
- ESM
- TypeScript
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
};
import { SapphireClient } from '@sapphire/framework';
import { getRootData } from '@sapphire/pieces';
import { join } from 'node:path';
export class MyOverloadedClient extends SapphireClient {
rootData = getRootData();
constructor(options) {
super(options);
this.stores.registerPath(join(this.rootData.root, 'audio'));
}
}
import { SapphireClient } from '@sapphire/framework';
import { getRootData } from '@sapphire/pieces';
import type { ClientOptions } from 'discord.js';
import { join } from 'node:path';
export class MyOverloadedClient extends SapphireClient {
private rootData = getRootData();
public constructor(options: ClientOptions) {
super(options);
this.stores.registerPath(join(this.rootData.root, 'audio'));
}
}
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.