Skip to content

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.

src/plugins/Uptime.ts
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 fromthis.core carriesextend it when your plugin
@seedcord/gatewaythe gateway Core, including botreads gateway members, like bot
@seedcord/httpthe http Corereads http members
@seedcord/core/pluginCoreBase, the members both transports shareonly 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);
Argument of type 'typeof Voice' is not assignable to parameter of type 'typeof Voice & TransportMismatch<"gateway", "http"> & EdgePluginsUnsupported'. Property '"this plugin declares transport 'gateway' but this bot runs 'http'"' is missing in type 'typeof Voice' but required in type 'TransportMismatch<"gateway", "http">'.

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);
Argument of type 'typeof Uptime' is not assignable to parameter of type 'typeof Uptime & CoreParamTooNarrow'. Property '"this plugin constructor must take CoreBase as its first parameter and read the transport Core off this.core"' is missing in type 'typeof Uptime' but required in type 'CoreParamTooNarrow'.

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.

hover for typestap for types, arrow keys walk the tokens
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.