Creating Plugins

Runtime Hooks

Plugins can hook specific runtime paths. Use these only when your package needs framework-level behavior.

Autocomplete

api.autocomplete.wrap(...) wraps command option autocomplete execution.

import { createPlugin } from 'seyfert';
import { metrics } from './metrics';

export const metricsPlugin = createPlugin({
    name: 'autocomplete-metrics',
    register(api) {
        // wrap autocomplete execution to time it
        api.autocomplete.wrap(async ({ command, interaction, optionsResolver }, next) => {
            const startedAt = Date.now();

            try {
                // run the original autocomplete handler
                await next();
            } finally {
                // record metrics whether or not it threw
                metrics.recordAutocomplete({
                    command: command?.name,
                    fullCommandName: optionsResolver.fullCommandName,
                    interactionId: interaction.id,
                    duration: Date.now() - startedAt,
                });
            }
        });
    },
});

Wrappers compose around the original autocomplete handler. Call next() when the original autocomplete logic should continue.

Gateway Intents

Plugins can contribute gateway intents.

register(api) {
    // contribute gateway intents
    api.gateway.addIntents('Guilds');
}

Intent names are resolved from GatewayIntentBits. Numeric values are also accepted.

Gateway Send Payload Wrappers

api.gateway.wrapSendPayload(...) runs before gateway send payloads leave the client.

register(api) {
    // intercept payloads before they leave the client
    api.gateway.wrapSendPayload(({ payload, shardId }) => {
        // return null to consume the payload and stop sending it
        if (shouldDropHeartbeat(payload, shardId)) return null;

        // return a payload to replace the current one
        return {
            ...payload,
            d: patchPayload(payload.d),
        };
    });
}

Return behavior:

  • return a payload to replace the current payload
  • return undefined to keep the current payload
  • return null to consume the payload and stop sending it (a gateway-send-payload-veto diagnostic is recorded)

This is a low-level hook. Prefer normal events or shared state unless your package specifically needs to modify gateway send behavior.

Inbound Dispatch Interceptors

api.gateway.onDispatch(...) sees inbound gateway packets before cache and events process them. Call next(packet) to continue the chain, or return null to veto the packet. It returns a disposer.

register(api) {
    // see inbound packets before cache and events; returns a disposer
    const dispose = api.gateway.onDispatch((packet, next, meta) => {
        metrics.increment('gateway.dispatch', {
            event: packet.t ?? 'unknown',
            shardId: meta.shardId,
        });
        // continue the chain (return null instead to veto the packet)
        return next(packet);
    });
}

Return null and the packet is dropped before cache and events ever run, with a gateway-dispatch-veto diagnostic recorded — so a dropped packet is a decision, not a mystery.

REST Observers

api.rest.observe(...) watches every REST request with read-only payloads. Observers stack, so multiple plugins can instrument REST without clobbering each other. It returns a disposer.

register(api) {
    // watch every REST request with read-only payloads
    api.rest.observe({
        onRequest({ method, url }) { /* about to fire */ },
        onSuccess({ method, url, response }) {
            metrics.increment('rest.success', { method, url, status: response.status });
        },
        onFail({ method, url, error }) { /* it didn't go well */ },
        onRatelimit({ route }) { /* Discord said slow down */ },
    });
}

A throwing observer is isolated — it doesn't take down the others. Ordering is passed through the options object: api.rest.observe(observer, { order: PluginOrder.Before }).

Language Overlays

api.langs.contribute(...) merges translations under a namespace prefix, so an i18n package can ship its own keys without colliding with the app's.

register(api) {
    // merge translations under a namespace prefix to avoid collisions
    api.langs.contribute('en-US', { balance: 'Balance' }, { prefix: 'plugins.economy' });
}

App code then reads ctx.t.plugins.economy.balance.

Upload Commands Metadata

Plugins can observe command uploads through the custom uploadCommands event.

register(api) {
    // observe command uploads via the custom uploadCommands event
    api.events.on('uploadCommands', (metadata) => {
        metadata.applicationId;
        metadata.commands;
        metadata.scope; // "global" or "guild"
        metadata.status; // "uploaded" or "skipped"
        metadata.reason; // "cache-hit", "cache-miss", or "forced"
    });
}

This is useful for metrics, audit logs, or deployment tooling.