Skip to content

Plugins

Add your own services to a bot with plugins, which seedcord starts and stops with it. Covers attaching a plugin, reading it from a handler, and where a plugin can run.

Most bots need something beside Discord, like a database, a cache, or a client for another API. Wiring one up by hand means starting it before the bot logs in, handing it to every handler through a global or an import, and stopping it on shutdown before the process exits.

A plugin packages that work. It starts and stops at defined points in your bot's lifecycle, takes its own options, and logs on its own channel. You attach it under a key you pick, then read it from this.core anywhere core is available, like a handler.

src/bot.tshover for typestap for types, arrow keys walk the tokens
import { function resolve(...paths: string[]): stringresolve } from 'node:path';

import { class SeedcordSeedcord } from '@seedcord/gateway';
import { class MongooseMongoose } from '@seedcord/plugin-mongoose';
import { enum GatewayIntentBitsGatewayIntentBits } from 'discord.js';

export const const seedcord: Seedcord & Record<"db", Mongoose>seedcord = new new Seedcord(config: GatewayConfig): SeedcordSeedcord({
    GatewayConfig.bot: GatewayBotConfigbot: {
        GatewayBotConfig.clientOptions: ClientOptionsclientOptions: { 
ClientOptions.intents: BitFieldResolvable<
    | "Guilds"
    | "GuildMembers"
    | "GuildModeration"
    | "GuildBans"
    | "GuildExpressions"
    | "GuildEmojisAndStickers"
    | "GuildIntegrations"
    | "GuildWebhooks"
    | "GuildInvites"
    | "GuildVoiceStates"
    | "GuildPresences"
    | "GuildMessages"
    | "GuildMessageReactions"
    | "GuildMessageTyping"
    | "DirectMessages"
    | "DirectMessageReactions"
    | "DirectMessageTyping"
    | "MessageContent"
    | "GuildScheduledEvents"
    | "AutoModerationConfiguration"
    | "AutoModerationExecution"
    | "GuildMessagePolls"
    | "DirectMessagePolls",
    number
>
intents
: [enum GatewayIntentBitsGatewayIntentBits.function (enum member) GatewayIntentBits.Guilds = 1Guilds] },
BotConfig.interactions: InteractionsConfiginteractions: { path: stringpath: function resolve(...paths: string[]): stringresolve(import.meta.ImportMeta.dirname: stringdirname, './handlers') }, BotConfig.commands: CommandsConfigcommands: { path: stringpath: function resolve(...paths: string[]): stringresolve(import.meta.ImportMeta.dirname: stringdirname, './commands') }, GatewayBotConfig.events: EventsConfigevents: { path: nullpath: null } }, Config.subscribers: SubscribersConfigsubscribers: { path: nullpath: null } }).Pluggable<"gateway", "server">.attach<"db", typeof Mongoose>(this: Seedcord, key: "db", Plugin: typeof Mongoose, options: MongooseOptions): Seedcord & Record<"db", Mongoose>attach('db', class MongooseMongoose, { MongooseOptions.dir: stringdir: function resolve(...paths: string[]): stringresolve(import.meta.ImportMeta.dirname: stringdirname, './services'), MongooseOptions.uri: stringuri: 'mongodb://localhost:27017/', MongooseOptions.name: stringname: 'seedcord' }); export default const seedcord: Seedcord & Record<"db", Mongoose>seedcord;

attach takes the key, the plugin class, and whatever that class's constructor takes after the host. Here that's 'db', Mongoose, and its options object. If a plugin's constructor takes only the host, attach it with two arguments.

Keep the export default. seedcord codegen imports that default export to type this.core.db. Without it, this.core.db doesn't compile.

Reading it from a handler

this.core carries every attached plugin under its key.

src/handlers/History.ts
@SlashRoute('history')
export class History extends SlashHandler<'history'> {
    public async execute(): Promise<void> {
        const found =
            await this.core.db.services.users.findByName('ada');

        await this.reply(
            found ? `Found ${found.username}.` : 'No record.'
        );
    }
}

History reaches the mongoose service through this.core.db.

The sample declares its Core block by hand so it compiles on this page. In your project, seedcord codegen writes that block for you.

Attaching more than one

A bot can attach more than one plugin, each under its own key. Chain another attach for each one. Inside one startup phase the plugins start one after another, in the order you attached them. If your plugin's init() reads another plugin, attach that one first. Otherwise your init() runs before the other plugin has started.

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: {
            path: resolve(import.meta.dirname, './handlers')
        },
        commands: {
            path: resolve(import.meta.dirname, './commands')
        },
        events: { path: null }
    },
    subscribers: { path: null }
})
    .attach('db', Mongoose, {
        dir: resolve(import.meta.dirname, './mongo'),
        uri: 'mongodb://localhost:27017/',
        name: 'seedcord'
    })
    .attach('sql', KyselyPostgres, {
        dir: resolve(import.meta.dirname, './postgres'),
        connectionString: 'postgres://localhost:5432/seedcord',
        migrations: {
            path: resolve(import.meta.dirname, './migrations')
        }
    });

The key also names a log channel

Every mongoose log line prints on the db channel, since a plugin logs on the channel named after its attach key. You can then filter or silence one plugin's logs by the key you gave it. seedcord already logs on reserved channels of its own, so those names can't be keys.

seedcord.attach('commands', Mongoose, {
Argument of type '"commands"' is not assignable to parameter of type '"'commands' is a channel the framework logs on. Pick another plugin key."'.
dir: resolve(import.meta.dirname, './services'), uri: 'mongodb://localhost:27017/', name: 'seedcord' });

Warning

commands is one of those reserved channels, so that attach fails to compile. If the compiler can't see the key, like one read from an environment variable, attach throws CorePluginReservedChannel when it runs. Logging lists the reserved channels with what each one carries.

attach throws in two more cases when it runs:

  • If the bot has already started, it throws CorePluginAfterInit.
  • If another plugin or seedcord itself already uses the key on core, like bus, it throws CorePluginKeyExists.

A plugin declares where it runs

Some plugins only work in one setup, like one that reads discord.js objects on gateway. A plugin can declare the transport it supports, 'gateway' or 'http', and the runtime, 'server' or 'edge'. Both default to 'any', which attaches to any bot. If you attach a plugin to a bot that doesn't match, TypeScript reports the mismatch in your editor before the bot runs.

this plugin declares transport 'gateway' but this bot runs 'http'

Http only

An edge bot can't use plugins. createSeedcord from @seedcord/http/edge returns a request handler, which doesn't have attach.

Typing a plugin explains the codegen step behind this.core.db. The lifecycle says when a plugin starts and when it stops. To write one, start at Writing your own.