# 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`](https://docs.seedcord.org/packages/gateway/latest/has-perms-to-assign) checks role position and two other rules before the write. If one fails, it refuses with a card the caller can read. [`mergeRoles`](https://docs.seedcord.org/packages/core/latest/merge-roles) 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`](https://docs.seedcord.org/packages/core/latest/notice).

1. The target role is at or above the bot's highest role.
2. The target role is managed.
3. 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.

```ts
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`](https://docs.seedcord.org/packages/core/latest/assert-permissions).

## 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.

{/* prettier-ignore-start */}

| override        | constructor takes after the message    |
| --------------- | -------------------------------------- |
| `higherNotice`  | the target role, then the bot's role   |
| `managedNotice` | nothing                                |
| `missingNotice` | the subject, then the permission names |

{/* prettier-ignore-end */}

```ts
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.

```ts title="src/handlers/AddRole.ts"
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.

```ts
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.
>
> ```ts
> await this.api.guilds.editMember(guildId, userId, { roles: ids });
> ```
