Skip to content

Permissions in a handler

Check permissions inside a handler with assertPermissions() and the gateway helpers. Covers asserting a bitfield, refusing a permission the target holds, rewording the refusal, and checking 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
subjectstringwho is checked, printed at the start of the refusal
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
missingNoticeNotice subclass, optionalreplaces the refusal for missing bits, see replacing the refusal card
dangerousNoticeNotice subclass, optionalreplaces the inverse refusal

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, and 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 your own card. Each of the two refusals has its own override, and each takes a Notice subclass.

  • missingNotice replaces the refusal for missing permissions.
  • dangerousNotice replaces the inverse: true refusal for permissions the subject holds.

Both constructors receive the same three arguments: a message, the subject string, and the permission names. seedcord passes undefined as the message, so give your class its own wording.

Throwing shows how to write one.

assertPermissions({
    subject: `<@${target.id}>`,
    permissions: target.permissions.bitfield,
    scope: [PermissionFlagsBits.BanMembers],
    missingNotice: 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 computes the bitfield, builds subject as <@id> or <@&id>, and calls assertPermissions with both. What it computes depends on the second argument.

second argumentreads
a Guildthe base bits from the target's roles, before any channel overwrite
a TextChannelthe set that channel's overwrites resolve to

Pass a Guild for a server-wide permission like Kick Members. Pass a channel for a permission that channel overwrites can change, like Send Messages. checkPermissions has three call shapes: the guild form, the channel form, and an options object.

checkPermissions(member, guild, [PermissionFlagsBits.KickMembers]);

checkPermissions(member, channel, [
    PermissionFlagsBits.SendMessages
]);

checkPermissions(
    member,
    guild,
    [PermissionFlagsBits.Administrator],
    true
);

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

A positional call takes inverse fourth and an object of notice overrides fifth. The object form takes them as fields: inverse, missingNotice, and dangerousNotice.

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 takes the guild or the channel, without a target.

checkBotPermissions(channel, [
    PermissionFlagsBits.SendMessages,
    PermissionFlagsBits.ViewChannel
]);

Its arguments after scope match checkPermissions, with inverse third and an object of notice overrides fourth.

If your bot's member isn't cached, the refusal lists every permission in scope as missing. The channel form also logs a warning on the errors log channel.

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.