Skip to content

Permissions in a handler

Check permissions inside a handler with assertPermissions() and the gateway helpers, for a member, a role, or your own bot.

A permission gate checks the caller and your bot before execute() starts. /ban @someone needs the target checked too. You get the target from the options inside execute(), after every gate has run. By hand, that check means comparing bitfields and writing a refusal message that lists what's missing.

The helpers on this page do that comparison inside your handler and throw the same refusal card a gate would. assertPermissions runs on both transports. checkPermissions and checkBotPermissions take discord.js objects, so they are gateway only.

Asserting a bitfield

assertPermissions takes one object. It returns when the subject holds every bit in scope. Otherwise it throws a Notice that lists what's missing.

fieldtypeholds
subjectstring | nullwho is checked, printed at the start of the refusal. null addresses the caller as "You"
permissionsbigintthe permission bits the subject holds, which you compute
scopereadonly bigint[]the PermissionFlagsBits values to check
inverseboolean, optionalrefuses when the subject holds a bit in scope, see refusing a permission they hold
noticeNotice class, optionalreplaces the refusal, see replacing the refusal card

You compute permissions yourself, so assertPermissions works on either transport. Here /maintenance checks that the person it is about to notify can see the channel.

src/handlers/Maintenance.ts
import {
    assertPermissions,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';
import { PermissionFlagsBits } from 'discord.js';

@SlashRoute('maintenance')
export class Maintenance extends SlashHandler<'maintenance'> {
    public async execute(): Promise<void> {
        const notify = this.options.getMember('notify');
        const channel = this.options.getChannel('target');

        if (notify === null) {
            await this.reply('That user is not in this server.');
            return;
        }

        assertPermissions({
            subject: `<@${notify.id}>`,
            permissions: channel.permissionsFor(notify, true)
                .bitfield,
            scope: [PermissionFlagsBits.ViewChannel]
        });

        await this.reply('Notified.');
    }
}

If the notified member can't see the channel, the default refusal card starts with subject and lists what's missing.

### Cannot Proceed
<@123456789> is missing the following permission entries:

• View Channel

subject goes into the card exactly as you pass it. To show a mention there, pass <@id> for a member or <@&id> for a role.

getMember can return null even for a required user option. It reads the member from the interaction Discord sends. Discord leaves the member out in two cases.

  • The command ran in a direct message.
  • The chosen user isn't a member of the server.

Gateway and http differ

getMember returns a different shape on each transport, so you read the bits differently.

transportgetMember returnsthe bits
gatewaydiscord.js GuildMembermember.permissions.bitfield
httpAPIInteractionDataResolvedGuildMemberBigInt(member.permissions)

Both shapes carry the target's permissions, so assertPermissions works on either without a REST call. On http, Discord computes those bits for the channel the command ran in.

Refusing a permission they hold

inverse: true flips the check. It throws when the subject holds any bit in scope, and returns otherwise.

Use it when the target must not hold a permission. banMember checks the target before it bans them.

async function banMember(target: GuildMember): Promise<void> {
    assertPermissions({
        subject: `<@${target.id}>`,
        permissions: target.permissions.bitfield,
        scope: [PermissionFlagsBits.Administrator],
        inverse: true
    });

    await target.ban();
}

If the target holds Administrator, assertPermissions throws before target.ban() runs.

Warning

The Administrator rule from permissions applies here too. The inverse check reports every bit in scope as present for anyone holding Administrator. Put only the bits you mean in scope, and read the refusal as "holds at least one of these".

Replacing the refusal card

The default card says which permissions are missing. When your command needs its own wording, like "You can't ban a moderator", pass a Notice class you wrote as notice. It replaces the refusal with or without inverse: true. seedcord constructs it with subject and the permission names: the missing ones, or the held ones under inverse: true.

Throwing shows how to write one.

assertPermissions({
    subject: `<@${target.id}>`,
    permissions: target.permissions.bitfield,
    scope: [PermissionFlagsBits.BanMembers],
    notice: CannotBan
});

Checking a role or member

On gateway you already hold a discord.js member or role, so you can skip reading its bitfield and writing its mention. checkPermissions takes the member or role as for, computes the bitfield, builds subject as <@id> or <@&id>, and calls assertPermissions with both. Its other fields match assertPermissions.

Leave in out for a server-wide permission like Kick Members, and checkPermissions reads the base set from the target's roles. Pass a channel or thread as in for a permission its overwrites can change, like Send Messages.

checkPermissions({
    for: member,
    scope: [PermissionFlagsBits.KickMembers]
});

checkPermissions({
    for: member,
    in: channel,
    scope: [PermissionFlagsBits.SendMessages]
});

checkPermissions({
    for: member,
    scope: [PermissionFlagsBits.Administrator],
    inverse: true
});

Checking your own bot

A gate checks your bot's permissions in the channel where the command ran. If your handler posts in a different channel, check that channel. checkBotPermissions runs the same check on your bot's member. It doesn't take for, and its in is required: the guild for your bot's base set, or a channel or thread.

checkBotPermissions({
    in: channel,
    scope: [
        PermissionFlagsBits.SendMessages,
        PermissionFlagsBits.ViewChannel
    ]
});

If your bot's own member isn't in the member cache, checkBotPermissions throws CoreBotMemberMissing. The error says how to keep it there.

A passing check for Manage Roles still leaves role position to check. Discord rejects a role your bot assigns if that role is at or above the bot's highest role. Changing roles covers that check.