# Writing your own

Write your own plugin by extending Plugin, with typed options, logging, and dev reloads, then publish it on npm.

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`](https://seedcord.org/docs/packages/core/latest/classes/plugin) and write `init()`. Everything else on the base is optional.

```ts title="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.

```ts
export default seedcord.attach('uptime', Uptime);
```

## Calling it from a handler

A handler calls your plugin's methods on `this.core.uptime`, since `uptime` is the key you attached it under. Nothing gets exported from `bot.ts` or imported into the handler.

```ts title="src/handlers/UptimeCommand.ts"
import { SlashHandler, SlashRoute } from '@seedcord/gateway';

@SlashRoute('uptime')
export class UptimeCommand extends SlashHandler<'uptime'> {
    public async execute(): Promise<void> {
        const ms = this.core.uptime.elapsed();
        await this.reply(
            `Up for ${Math.floor(ms / 1000)} seconds.`
        );
    }
}
```

`this.core.uptime` has the type `Uptime`, so your editor offers `elapsed()` and types its result as a `number`.

A handler can call every public member of your class. `startedAt` is private, so a handler reads the time through `elapsed()` and can't overwrite it. Keep connections and state private, and make public only the methods you want handlers to call.

With the default phases, handlers start receiving interactions after every `init()` has finished. `ready()` may still be running at that point, so if a method reads something that `ready()` sets, check that it's set first. [The lifecycle](https://seedcord.org/guide/plugins/lifecycle/) has the full order.

## 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.

{/* prettier-ignore-start */}

| 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`](https://seedcord.org/docs/packages/core/latest/interfaces/core-base), the members both transports share | only uses what both transports share |

{/* prettier-ignore-end */}

`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`](https://seedcord.org/docs/packages/core/latest/interfaces/plugin-options) generic to narrow them. If someone attaches it to the wrong kind of bot, their `attach` call fails to compile.

```ts
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.

```ts
seedcord.attach('voice', Voice);
```

`seedcord` here is an http bot, so passing `Voice` to its `attach` fails to compile. The error prints the transport `Voice` declares beside 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.

```ts
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.

```ts
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.

## A plugin with a type parameter

A plugin that keeps values for the bot, like a key-value cache, doesn't know their type when you write it. A type parameter leaves that to the bot that attaches it.

```ts
interface Session {
    userId: string;
}

export class KeyValue<TValue> extends Plugin {
    private readonly entries = new Map<string, TValue>();

    public async init(): Promise<void> {}

    public get(key: string): TValue | undefined {
        return this.entries.get(key);
    }
}

const bot = seedcord.attach('sessions', KeyValue<Session>);
const session = bot.sessions.get('abc');
```

`KeyValue<Session>` in the `attach` call sets `TValue`, so `session` comes back as `Session | undefined`. A handler reading `this.core.sessions` gets the same type, because `seedcord codegen` copies the attached type onto `Core`.

If you pass `KeyValue` alone, TypeScript sets `TValue` to its constraint, `unknown` here. It does that even when `TValue` has a default. A value you pass to the constructor doesn't set it either, so write the type argument in the `attach` call whenever you want a narrower type.

## Rejecting bad options

A bad option, like an empty label, is cheapest to catch while the bot starts. Call [`rejectOptions`](https://seedcord.org/docs/packages/core/latest/classes/plugin#reject-options) from the constructor to stop the bot at the `attach` call, before anything connects.

```ts
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](https://seedcord.org/guide/plugins/#the-key-is-also-a-log-channel), 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`](https://seedcord.org/docs/packages/core/latest/classes/plugin#on-hmr) 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`](https://seedcord.org/docs/packages/core/latest/classes/plugin#register-critical-files). When one of them changes, the dev terminal shows a **Restart required** card telling you to press `r`, which restarts the whole bot. Register only the files your plugin reads. seedcord already requests a restart on its own startup files, listed in [Hot reload](https://seedcord.org/guide/tooling/hot-reload/#saves-that-need-a-restart).

```ts
export class Uptime extends Plugin {
    public async init(): Promise<void> {
        this.registerCriticalFiles(['src/schema/**']);
    }

    public override async onHmr(
        event: HmrUpdateEvent
    ): Promise<void> {
        this.logger.debug(`${event.file} 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](https://seedcord.org/guide/plugins/mongoose/).

## Publishing to npm

Someone who wants a plugin searches npm for seedcord, and that search reads package names and keywords.

To help with discoverability, and to follow the convention, name your package `seedcord-plugin-<thing>`, like `seedcord-plugin-uptime`, and add `seedcord` and `plugin` to your keywords. Official plugins will always be published under the `@seedcord/plugin-` scope, which only seedcord can publish to.

Whichever package you import `Plugin` from goes in `peerDependencies`, with the versions you tested against. Mongoose extends the base from `@seedcord/core/plugin`, so it declares `@seedcord/core` at `>=0.2.0 <1.0.0`. seedcord is below 1.0, so a breaking change can ship in a minor, and a plugin written for 0.2 can break on 0.3. The range tells people which versions work before they install.
