Skip to content

Permissions

Check the caller's permissions, your bot's permissions, or the caller's role before a handler runs. Covers channel and server permissions, event handlers, and rewording each refusal.

A kick command can fail two ways. The caller might not be allowed to kick, or your bot might not be. Checked by hand, the first is a permissions lookup at the top of every handler. For the second, Discord returns a Missing Permissions error after your handler has already replied or started work.

RequirePermissions checks the caller and RequireBotPermissions checks your bot, both before execute() runs. RequireRole checks one role by id.

src/handlers/Kick.ts
import {
    Gated,
    RequireBotPermissions,
    RequirePermissions,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';
import { PermissionFlagsBits } from 'discord.js';

@Gated(
    RequirePermissions([PermissionFlagsBits.KickMembers]),
    RequireBotPermissions([PermissionFlagsBits.KickMembers])
)
@SlashRoute('kick')
export class Kick extends SlashHandler<'kick'> {
    public async execute(): Promise<void> {
        const target = this.options.getUser('target');

        await this.reply(`Kicked ${target.username}.`);
    }
}

The two permission gates take a PermissionScope, an array of flag bits. A scope with several bits requires all of them.

RequirePermissions([
    PermissionFlagsBits.ManageMessages,
    PermissionFlagsBits.ManageThreads
]);

An http bot doesn't depend on discord.js, so the same bits come from discord-api-types/v10 there. The values are identical.

Administrator passes everything

Discord grants every permission to Administrator, so both permission gates apply that rule. A caller holding Administrator passes any scope you name.

Warning

You hold Administrator on your own test server, as the owner or through a role. Both permission gates pass there, so you cannot tell a broken one from a working one. Test with a second account that doesn't have an admin role.

RequireRole reads role ids, which is why it also refuses an administrator who lacks the role.

Channel permissions and server permissions

The in option picks which set the gate reads.

RequirePermissions([PermissionFlagsBits.BanMembers], { in: '
  • channel
  • guild
inreadsholds true when
'channel', the defaultthe effective set for the invoked channel, after overwritesDiscord would allow the action right here
'guild'the base set from roles alonethe roles grant it somewhere in the server

A channel overwrite can remove a permission that a role granted. 'channel' includes that overwrite in what it reads, so it answers "can they do this here". 'guild' answers "do their roles grant this anywhere in the server", which is what you want when the command acts on the whole server.

RequirePermissions([PermissionFlagsBits.BanMembers], {
    in: 'guild'
});

Gateway only

in: 'guild' reads role-derived sets that only the gateway cache carries. So the gate requires a GuildPermissionsContext, which the http transport never provides.

Putting a guild-scoped gate on an http handler fails at the decorator line.

@Gated(
Unable to resolve signature of class decorator when called as an expression. Argument of type 'typeof Ban' is not assignable to parameter of type 'readonly [Constructor<["gate 'RequirePermissions' requires a gateway (guild permissions) handler, and this handler is Slash"]>]'.
RequirePermissions([PermissionFlagsBits.BanMembers], { in: 'guild' }) ) @SlashRoute('ban') export class Ban extends SlashHandler<'ban'> { public async execute(): Promise<void> {} }

On a gateway event handler

An interaction payload carries both channel sets. A gateway event doesn't carry either one, which changes what 'channel' does on each gate.

on an event, in: 'channel'what happens
RequirePermissionsreads the caller's role-derived set, the same value 'guild' reads. Channel overwrites go uncounted
RequireBotPermissionsreads app_permissions, which an event never carries, so it refuses every time and reports the set as unresolved

Use in: 'guild' on an event handler. On the caller side it reads the same set 'channel' would there, and on the bot side it's the only scope that can pass.

RequireBotPermissions([PermissionFlagsBits.ManageMessages], {
    in: 'guild'
});

Requiring a role

RequireRole takes one role id and reads the caller's roles. Roles are server-wide, so it does not take a scope option.

@Gated(RequireRole('221439658686939136'))
@SlashRoute('roles')
export class Roles extends SlashHandler<'roles'> {
    public async execute(): Promise<void> {
        await this.reply('Here they are.');
    }
}

Combine several with or when holding any one of the roles is enough.

Wording each refusal

Each of these gates refuses for two different reasons. Its options carry one slot per reason, and each slot takes the message and notice pair from Gates.

gateslotrefuses when
RequirePermissionsnotInGuildthe set is unavailable, outside a server or unresolved inside one
RequirePermissionsmissinga bit in the scope is absent
RequireBotPermissionsnotInGuildthe same, for your bot's set
RequireBotPermissionsmissingthe same, for your bot's bits
RequireRolenotInGuildthe call came from outside a server
RequireRolemissingRolethe caller lacks the role
RequirePermissions([PermissionFlagsBits.BanMembers], {
    notInGuild: { message: 'Run this in a server.' },
    missing: { message: 'You cannot ban here.' }
});

RequireRole('221439658686939136', {
    missingRole: { message: 'Subscribers only.' }
});

Leaving a slot out keeps the default card. The permission gates list every missing bit on it by name, and RequireRole mentions the role.

A check these gates don't cover, like whether the caller linked an account, is a gate you write yourself.