Skip to content
sern

Plugins

Installation

Chances are, you just want your bot to work. Plugins can preprocess and create reusable conditions for modules.

To install plugins, you can use the CLI:

Terminal window
sern plugins
  1. Install your favorite(s) (or the ones that look the coolest). I installed the ownerOnly plugin.
  2. Thank the creator of the plugin. (mandatory)
  3. Add the plugin to your module in the plugins field.
src/commands/ping.ts
1
import { commandModule, CommandType } from '@sern/handler'
2
import { ownerOnly } from '../plugins'
3
4
export default commandModule({
5
type: CommandType.Both,
6
plugins: [ownerOnly(['182326315813306368'])],
7
description: 'ping command',
8
execute: (ctx) => {
9
ctx.reply('hello, owner');
10
}
11
})

┗|` O′|┛ perfect, your first plugin!

Creating Plugins

Plugins are essentially functions that use the controller object to determine whether to continue or stop the execution of a command.

Init Plugins

Init plugins modify how commands are loaded or do preprocessing.

src/plugins/updateDescription.js
1
import { CommandInitPlugin } from "@sern/handler";
2
3
export const updateDescription = (description: string) => {
4
return CommandInitPlugin(({ deps }) => {
5
if(description.length > 100) {
6
deps.logger?.info({ message: "Invalid description" })
7
return controller.stop("From updateDescription: description is invalid");
8
}
9
module.description = description;
10
return controller.next(); // continue to next plugin
11
});
12
};

You may modify any of the fields of the module, but please careful! This also includes double checking any plugins which may modify fields of modules.

User Data

Each module has a locals object which you are free to add custom user data. For example, the localizer AND publisher take advantage of this to attach metadata.

src/plugins/dateRegistered.js
1
import { CommandInitPlugin } from "@sern/handler";
2
3
export const dateRegistered = (description: string) => {
4
return CommandInitPlugin(({ module, deps }) => {
5
module.locals.registered = Date.now() // save module registration date
6
return controller.next(); // continue to next plugin
7
});
8
};

Control Plugins

control plugins

1
import { CommandControlPlugin } from "@sern/handler";
2
3
export const inGuild = (guildId: string) => {
4
return CommandInitPlugin(ctx, sdt) => {
5
if(ctx.guild.id !== guildId) {
6
return controller.stop();
7
}
8
return controller.next();
9
});
10
};
  1. An event is emitted by discord.js.
  2. This event is passed to all control plugins in order!!,
  3. If all are successful, the command is executed.

Controller Object

The controller object is passed into every plugin. It has two methods: next and stop.

Plugins use the controller to control the flow of the command. For example, if a plugin fails, it calls controller.stop() to prevent the command from executing.

1
// Reference object, import this from @sern/handler
2
const controller = {
3
next: (val?: Record<string,unknown>) => Ok(val),
4
stop: (val?: string) => Err(val),
5
};

State

SDT = state, dependencies, type (very creative)

Plugins can recieve data from previous plugins. State is created when controller.next is called with a record. If all control plugins are successful, the final state is passed to the module execute.

1
import { commandModule, CommandControlPlugin, CommandType } from '@sern/handler'
2
3
const plugin = CommandControlPlugin((ctx, sdt) => {
4
return controller.next({ a: 'from plugin1' });
5
});
6
const plugin2 = CommandControlPlugin((ctx, sdt) => {
7
return controller.next({ b: ctx.user.id + "from plugin2" });
8
})
9
10
export default commandModule({
11
type: CommandType.Slash,
12
plugins: [plugin, plugin2],
13
execute: (ctx, sdt) => {
14
sdt.state // { a: 'from plugin1', b: '182326315813306368 from plugin2' }
15
}
16
})

Caveats

Passing data with the same key will get overridden by the latest plugin. It is recommended to namespace data keys if you have multiple plugins, or you can ensure no keys are overridden by the plugin chain.

1
return controller.next({ 'cheese-plugin/data' : "From cheese-plugin" })

Dependencies

SDT = state, dependencies, type (very creative)

Plugins also carry an instance of all of your dependencies. Use them and use them as you please! For example, creating a plugin which logs which user uses your command

1
import { commandModule, CommandControlPlugin, CommandType } from '@sern/handler'
2
export const log = CommandControlPlugin((ctx, sdt) => {
3
sdt
4
.deps['@sern/logger']
5
.info({ message: `${ctx.user.id} used this command from ${ctx.guild.id}` });
6
})