Changing roles
Check that your bot can assign a role, then build and write a member's new role list. Covers hasPermsToAssign(), rewording its refusals, mergeRoles(), and writing the list on each transport.
Your bot holds Manage Roles, yet roles.add still fails. Holding the permission isn't enough, because Discord also checks role position. A bot can change only the roles below its own highest role. Discord rejects a write to a role at or above it. Without a check first, you only find out from the error Discord returns.
hasPermsToAssign checks role position and two other rules before the write. If one fails, it refuses with a card the caller can read. mergeRoles returns the list to write.
Checking before you write
hasPermsToAssign refuses in these cases, checked in order. The first case that matches throws a Notice.
- The target role is at or above the bot's highest role.
- The target role is managed.
- The bot lacks Manage Roles.
A managed role belongs to an integration. Every bot's own role and the Nitro booster role have managed set to true. So does any role a linked service hands out, like a Twitch subscriber tier. Discord assigns those roles itself, so nobody else can assign one.
You pass the role you're about to assign.
import { hasPermsToAssign } from '@seedcord/gateway';
hasPermsToAssign(role);To compare positions, hasPermsToAssign reads your bot's own role from the guild's role cache. If the cache doesn't have it, the call throws a SeedcordError with the code CoreBotRoleMissing.
Gateway only
hasPermsToAssign takes a discord.js Role, so it's gateway only. On http, compare position on the two role payloads and read managed on the target yourself. Then check Manage Roles with assertPermissions.
Replacing the notices
The default cards say what went wrong in general terms. If you want to tell the caller something specific, like which role to move up in server settings, pass your own. You pass an options object with the role and a replacement for any refusal you want to reword. Every replacement constructor takes a message first.
| override | constructor takes after the message |
|---|---|
higherNotice | the target role, then the bot's role |
managedNotice | nothing |
missingNotice | the subject, then the permission names |
hasPermsToAssign({
targetRole: role,
noticeOverrides: { higherNotice: TooHigh }
});TooHigh replaces only the position refusal. The other two refusals keep their default cards.
Building the list
discord.js has member.roles.add() and member.roles.remove(). Each call sends its own request. Swapping a trial role for a staff role that way takes two requests. Between them, the member holds both roles, or neither.
To make every change in one request, set the member's whole role list at once. That list holds every role they keep, since Discord removes any role you leave out. Building it by hand means copying the held ids, pushing one, filtering one out, and removing duplicates.
mergeRoles does that in one call. You pass it the ids a member holds, the ids to add, and the ids to remove. It returns the merged list without duplicates. If an id is in both the add list and the remove list, mergeRoles leaves it out.
import {
hasPermsToAssign,
mergeRoles,
SlashHandler,
SlashRoute
} from '@seedcord/gateway';
@SlashRoute('role/add')
export class AddRole extends SlashHandler<'role/add'> {
public async execute(): Promise<void> {
const member = this.options.getMember('member');
const role = this.options.getRole('role');
if (member === null) {
await this.reply('That user is not in this server.');
return;
}
hasPermsToAssign(role);
const ids = member.roles.cache.map((held) => held.id);
await member.roles.set(mergeRoles(ids, [role.id], []));
await this.reply(`Added ${role.name}.`);
}
}AddRole passes one id to add and an empty removal list. To swap a trial role for a staff role, fill both lists in the same call.
member.roles.set(mergeRoles(ids, [staff], [trial]));Writing it back
mergeRoles only returns the list. You write it yourself. The call differs by transport.
Gateway and http differ
On gateway, pass the list to member.roles.set(). On http, send it through this.api, the typed REST client on every repliable http handler.
await this.api.guilds.editMember(guildId, userId, { roles: ids });