Writing your own
Write your own plugin by extending Plugin. Covers which base to import, declaring where it runs, constructor options, rejecting bad options, logging, and dev reloads.
Let's assume your bot talks to an outside service, like a metrics API or a cache. You created its client in bot.ts and exported it. Nothing closes that client when the bot shuts down, and a handler can import it before it has connected. As a plugin, the client starts and stops with your bot, and handlers read it from core under the key you attach it with.
To write one, extend Plugin and write init(). Everything else on the base is optional.
import { Plugin } from '@seedcord/gateway';
export class Uptime extends Plugin {
private startedAt = 0;
public async init(): Promise<void> {
this.startedAt = Date.now();
}
public elapsed(): number {
return Date.now() - this.startedAt;
}
}Uptime records the time in init() and reports it from elapsed(). Attach it with two arguments, since its constructor takes only the host.
seedcord.attach('uptime', Uptime);Which base to import
Each transport exports its own Plugin class. The one you extend sets the type of this.core. Extend a transport's Plugin when your plugin reads something only that transport has, like this.core.bot on gateway. Extend the Plugin from @seedcord/core/plugin when your plugin only uses what both transports share, so a bot on either transport can attach it.
| import from | this.core carries | extend it when your plugin |
|---|---|---|
@seedcord/gateway | the gateway Core, including bot | reads gateway members, like bot |
@seedcord/http | the http Core | reads http members |
@seedcord/core/plugin | CoreBase, the members both transports share | only uses what both transports share |
CoreBase carries config, rest, applicationId, rateLimiter, and bus. Anything beyond those needs a transport base.
Gateway and http differ
config narrows with the base you extend. A transport base types it as that transport's own config. CoreBase types it as the shared Config.
Declaring where it runs
The gateway base declares transport: 'gateway' and runtime: 'server'. The http base declares transport: 'http'.
The base from @seedcord/core/plugin leaves both at 'any'. When a shared plugin only works in one setup, like one that opens a voice connection, pass the PluginOptions generic to narrow them. If someone attaches it to the wrong kind of bot, their attach call fails to compile.
import { Plugin } from '@seedcord/core/plugin';
export class Voice extends Plugin<{
transport: 'gateway';
runtime: 'server';
}> {
public async init(): Promise<void> {}
}You can set either field without the other. Plugin<{ runtime: 'server' }> keeps a plugin off the edge runtime and takes either transport.
seedcord.attach('voice', Voice);seedcord here is an http bot, so passing Voice to its attach fails to compile. The error names the transport Voice declares and the one the bot runs.
What the constructor takes
Most plugins need settings, like a connection string or a label. The constructor's first parameter is the bot itself, which seedcord passes in when you call attach. Type it as CoreBase. Every parameter after it is yours. You pass their values to attach after the plugin class, where TypeScript checks their types.
export class Uptime extends Plugin {
public constructor(
host: CoreBase,
private readonly label: string
) {
super(host);
}
public async init(): Promise<void> {
this.logger.info(`tracking ${this.label}`);
}
}Uptime takes a label after the bot, so you attach it with attach('uptime', Uptime, label).
If you type that first parameter as the gateway Core, your attach call fails to compile.
class Uptime extends Plugin {
public constructor(host: Core) {
super(host);
}
public async init(): Promise<void> {}
}
seedcord.attach('uptime', Uptime);attach rejects Uptime because its constructor types host as Core. seedcord codegen adds every plugin you attach to that same Core type, so a plugin whose constructor takes Core ends up depending on itself, which TypeScript can't resolve. Keep the parameter as CoreBase, then read the transport's Core from this.core, which your base class already types for you.
Rejecting bad options
A bad option, like an empty label, is cheapest to catch while the bot starts. Call rejectOptions from the constructor to stop the bot at the attach call, before anything connects.
export class Uptime extends Plugin {
public constructor(host: CoreBase, label: string) {
super(host);
if (label.length === 0)
this.rejectOptions('label is empty');
}
public async init(): Promise<void> {}
}Calling rejectOptions throws PluginOptionsRejected. Your class name comes first in the message, then whatever reason you passed.
Logging
this.logger exists from the constructor onward. It prints under your class name, on the channel your attach key sets, so if someone attaches your plugin as metrics, they can filter its lines by that name.
Reacting to a dev reload
seedcord dev swaps a changed file into the running bot. If your plugin loads files itself, the way the mongoose plugin loads its service classes, it keeps the old copies. Override onHmr to reload them. Reloading only helps with files your plugin reads again, though. A schema that a connection read at startup stays as it was, since swapping the file doesn't repeat that read. Pass glob patterns for files like that to registerCriticalFiles. When one of them changes, the dev terminal shows a Restart required card telling you to press r, which restarts the whole bot.
export class class UptimeUptime extends class Plugin<
Opts extends GatewayPluginOptions = {
transport: "gateway";
runtime: "server";
}
>
Plugin {
public async Uptime.init(): Promise<void>init(): interface Promise<T>Promise<void> {
this.Plugin<{ transport: "gateway"; runtime: "server"; }, Core>.registerCriticalFiles(patterns: string[]): voidregisterCriticalFiles(['src/schema/**']);
}
public override async Uptime.onHmr(event: HmrUpdateEvent): Promise<void>onHmr(
event: HmrUpdateEventevent: interface HmrUpdateEventHmrUpdateEvent
): interface Promise<T>Promise<void> {
this.Plugin<{ transport: "gateway"; runtime: "server"; }, Core>.logger: Loggerlogger.Logger.debug(msg: string, ...args: unknown[]): voiddebug(`${event: HmrUpdateEventevent.HmrUpdateEvent.file: stringfile} changed`);
}
}Uptime logs each changed file and marks everything under src/schema/ as needing a restart. Both calls do nothing outside seedcord dev.
seedcord's own plugins extend this same base, starting with Mongoose.