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.
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
in | reads | holds true when |
|---|---|---|
'channel', the default | the effective set for the invoked channel, after overwrites | Discord would allow the action right here |
'guild' | the base set from roles alone | the 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( 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 |
|---|---|
RequirePermissions | reads the caller's role-derived set, the same value 'guild' reads. Channel overwrites go uncounted |
RequireBotPermissions | reads 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.
| gate | slot | refuses when |
|---|---|---|
RequirePermissions | notInGuild | the set is unavailable, outside a server or unresolved inside one |
RequirePermissions | missing | a bit in the scope is absent |
RequireBotPermissions | notInGuild | the same, for your bot's set |
RequireBotPermissions | missing | the same, for your bot's bits |
RequireRole | notInGuild | the call came from outside a server |
RequireRole | missingRole | the 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.