Gates
Run checks before your handler with @Gated, so a caller who fails one never reaches your code. Covers how a gate refuses, stacking several on one handler, the gates seedcord ships, and rewording the refusal a user sees.
Most handlers open with the same few checks. Is this a server? Can this person ban? If you write those checks inside execute(), every handler starts with its own copy of them. If you forget to copy one into a handler, that command runs for anyone.
A gate puts that check on the class and runs before execute(). If it passes, your handler runs, and if it refuses, the call stops there. Attach one with @Gated, above the route decorator. The Ban handler below runs only for someone in a server who holds BanMembers.
import {
Gated,
GuildOnly,
RequirePermissions,
SlashHandler,
SlashRoute
} from '@seedcord/gateway';
import { PermissionFlagsBits } from 'discord.js';
@Gated(
GuildOnly(),
RequirePermissions([PermissionFlagsBits.BanMembers])
)
@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
public async execute(): Promise<void> {
const target = this.options.getUser('target');
await this.reply(`Banned ${target.username}.`);
}
}execute() runs once both gates pass. GuildOnly() refuses a call from a DM, and RequirePermissions refuses a caller without BanMembers.
A gate refuses by throwing
seedcord catches a gate's throw at the same boundary it catches your handler's. The user sees the same card either way. Throwing covers what each class produces.
Every gate in the table below throws a Notice except IgnoreBots, which throws a Silence. The message gets dropped and your bot doesn't reply.
Several gates on one handler
@Gated takes one gate or several, and all of them have to pass. They run left to right, and the first one to refuse stops the rest.
The order you pick matters once a gate does something costly, like reading a database. Put the cheap checks first, so a caller who fails one never reaches the read behind it.
A second @Gated on the same class adds its gates to the list. TypeScript applies class decorators from the bottom up, so the one nearer the class runs first. One @Gated with every gate in it keeps the order where you can read it.
The catalog gates
| gate | passes when |
|---|---|
OwnerOnly() | the caller's id is in ownerIds |
GuildOnly() | the call came from a server |
DmOnly() | the call came from a direct message |
RequireRole(id) | the caller holds that role |
RequirePermissions(bits) | the caller holds every bit you name |
RequireBotPermissions(bits) | your bot holds every bit you name |
Cooldown(duration) | uses remain in the current window |
Nsfw() | the channel is age-restricted. Gateway interaction handlers only, modals excluded |
IgnoreBots | the actor is a person. Gateway only, and event handlers only |
Gateway only
Nsfw and IgnoreBots ship in @seedcord/gateway alone. Nsfw reads the discord.js channel object on the interaction, and IgnoreBots attaches to event handlers, which an http bot doesn't have.
Most of these are functions, so you call one and pass the result. IgnoreBots doesn't take options, so you attach it directly.
import {
EventHandler,
Gated,
IgnoreBots,
RegisterEvent
} from '@seedcord/gateway';
import { Events } from 'discord.js';
@Gated(IgnoreBots)
@RegisterEvent([Events.MessageCreate])
export class Mentioned extends EventHandler<Events.MessageCreate> {
public async execute(): Promise<void> {
const [message] = this.event;
await message.reply('hey');
}
}Every gate carries the context it reads, and @Gated checks that against the handler you put it on. Nsfw() reads an interaction's channel, so attaching it to the event handler below fails to compile. The error names the gate and the handler kind.
@Gated(Nsfw())@RegisterEvent([Events.MessageCreate])
export class Welcome extends EventHandler<Events.MessageCreate> {
public async execute(): Promise<void> {}
}Cooldown has a page of its own.
Who counts as an owner
OwnerOnly fits a command only you should run, like a reload or a shutdown. It reads ownerIds from your bot config, and with that key unset it refuses everyone.
import { resolve } from 'node:path';
import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';
export const seedcord = new Seedcord({
ownerIds: ['221439658686939136'],
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 }
});Rewording a refusal
Each refusal card uses seedcord's default wording. Reword it when your command has a next step to offer, like pointing someone at the server where it works.
OwnerOnly, GuildOnly, DmOnly, and Nsfw take the same GateNoticeOptions. message swaps the text and keeps the card. notice replaces the whole refusal with a Notice you wrote.
GuildOnly({ message: 'Try this in a server.' });
OwnerOnly({ notice: new Locked() });The Require gates nest that shape one level deeper, since each can refuse for two different reasons. RequirePermissions takes a notInGuild and a missing object, each holding its own message or notice. Cooldown takes message and notice as functions of resetAt, the time in milliseconds when the caller can try again, so its refusal can print that time. Each gate in the catalog table links to its API page, which lists every option it takes.
Permissions takes the permission gates in turn, including what Administrator does to them. When none of the shipped gates fits, you can write your own or combine several into one.