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.
| field | type | holds |
|---|---|---|
subject | string | who is checked, printed at the start of the refusal |
permissions | bigint | the permission bits the subject holds, which you compute |
scope | readonly bigint[] | the PermissionFlagsBits values to check |
inverse | boolean, optional | refuses when the subject holds a bit in scope, see refusing a permission they hold |
missingNotice | Notice subclass, optional | replaces the refusal for missing bits, see replacing the refusal card |
dangerousNotice | Notice subclass, optional | replaces 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.
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 Channelsubject 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.
| transport | getMember returns | the bits |
|---|---|---|
| gateway | discord.js GuildMember | member.permissions.bitfield |
| http | APIInteractionDataResolvedGuildMember | BigInt(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.
missingNoticereplaces the refusal for missing permissions.dangerousNoticereplaces theinverse: truerefusal 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 argument | reads |
|---|---|
a Guild | the base bits from the target's roles, before any channel overwrite |
a TextChannel | the 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.