question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Bug]: AddRoleAsync() and AddRolesAsync() removes roles after adding them

See original GitHub issue

Check The Docs

  • I double checked the docs and couldn’t find any useful information.

Verify Issue Source

  • I verified the issue was caused by Discord.Net.

Check your intents

  • I double checked that I have the required intents.

Description

AddRoleAsync() and AddRolesAsync() removes roles after adding them when using Discord.Net 3.10.0.

After downgrading to Discord.Net 3.9.0, AddRoleAsync() and AddRolesAsync() functions as intended.

Version

3.10.0

Working Version

3.9.0

Logs

n/a

Sample

public async Task HandleMenuInteraction(SocketMessageComponent messageComponent)
{
    switch(messageComponent.Data.CustomId)
    {
        case "role-selection":
            Console.WriteLine($"{messageComponent.User.Username} has changed their role subscriptions.");

            var values = messageComponent.Data.Values.ToList<string>();

            Console.WriteLine($"{messageComponent.User.Username} has selected the following role subscriptions:");

            foreach(var v in values) {
                Console.WriteLine(v);
            }
            
            var user = (IGuildUser) messageComponent.User;

            if(user != null)
            {
                List<string> allowedRoles = new List<string>()
                {
                    "Games",
                    "Technology",
                    "Programming",
                    "Art",
                    "Music"
                };

                List<ulong> addedRoles = new List<ulong>();
                List<ulong> removedRoles = new List<ulong>();

                string roleSubscriptions = "";

                foreach(var i in allowedRoles) {
                    try {
                        var role = user.Guild.Roles.First(x => x.Name == i);

                        if(values.Contains(role.Name)) {
                            addedRoles.Add(role.Id);
                            roleSubscriptions += $@"
:bell: {role.Name}";
                            Console.WriteLine($"{role.Name} ({role.Id}) was added to {messageComponent.User.Username}.");
                        }
                        else {
                            removedRoles.Add(role.Id);
                            roleSubscriptions += $@"
:no_entry_sign: {role.Name}";
                            Console.WriteLine($"{role.Name} ({role.Id}) was removed from {messageComponent.User.Username}.");
                        }
                    }
                    catch(Exception ex) {
                        Console.WriteLine(ex);
                    }
                }

                await user.AddRolesAsync(addedRoles);
                await user.RemoveRolesAsync(removedRoles);

                await messageComponent.DeferAsync();
                await messageComponent.ModifyOriginalResponseAsync(x => x.Content = $"{user.Mention} is now subscribed to the following channels: {roleSubscriptions}");
            }
            break;
    }
}

Packages

Discord.Net V3.9.0

Issue Analytics

  • State:closed
  • Created 6 months ago
  • Comments:27 (4 by maintainers)

github_iconTop GitHub Comments

4reactions
BobVulcommented, Apr 22, 2023

@cjbonn can you please explain how it works just fine for all previous versions but the latest is breaking? I am not sure how that is a user implementation issue.

Previous versions implemented AddRolesAsync by locally foreaching over the list and calling AddRoleAsync once for each role. That means if you want to add 5 roles, it would make 5 separate API calls (and accumulate 5 hits against your rate limit).

Discord has an endpoint for bulk-editing roles, and naturally the bulk-edit methods should use that endpoint. But that endpoint sets the user’s roles to whatever list is received, rather than adding or removing. Which means AddRolesAsync now locally needs to grab the user’s current role list and update it to add any new IDs, then send the full new list to the server.

Take this example:

// local user.RoleIds == [1, 2]; server-side user has roles 1, 2
await user.AddRolesAsync(new[] {3, 4}); // local [1, 2] + [3, 4] sends [1, 2, 3, 4] to server
// local user.RoleIds == [1, 2]; server-side user has roles 1, 2, 3, 4
await user.AddRolesAsync(new[] {5, 6}); // local [1, 2] + [5, 6] == sends [1, 2, 5, 6] to server
// local user.RoleIds == [1, 2]; server-side user has roles 1, 2, 5, 6

That first AddRolesAsync will calculate the new role list as [1, 2, 3, 4] and send that to the server. That’s fine, the user’s roles are now [1, 2, 3, 4]. But the local cache isn’t updated until we receive a gateway message from the server telling us about the modified User.

The second AddRolesAsync will calculate the new role list as [1, 2, 5, 6] and send that to the server. The user’s roles are now [1, 2, 5, 6]. Because we haven’t yet processed the gateway message, so the local cache is stale, so it’s not possible to calculate the correct set of roles.


So why aren’t we processing the gateway messages in between the two API calls? Well, that depends. If your handler is configured as RunMode.Sync (the default), that’ll be one reason: the handler blocks the gateway thread and no gateway messages can be processed until the entire handler is finished. Using RunMode.Async could help, but that can cause several other issues (you can then have multiple handlers run in parallel, so you need to make sure your code has no race conditions. Also the post-execution flow and error handling are completely different, see docs and docs.

But even using RunMode.Async wouldn’t definitively solve this: there’s no guarantee that you’ll receive the gateway message and process it before the next bulk-edit command is run, since multiple async threads are non-deterministically ordered by nature. And without hooking the user updated event there wouldn’t be any way to be sure that the cache is updated before the second call.

The simple solution is: don’t use multiple bulk (AddRoles/RemoveRoles) role editing methods in a single handler. Do the merge operations yourself and make a single request; Misha provided example code: https://github.com/discord-net/Discord.Net/issues/2655#issuecomment-1494670584. Alternatively do a foreach loop over the roles you want to add/remove and call the singular methods, which is exactly what the old behaviour was - just be aware that this is not ratelimit-friendly.

e: I wonder if there’s any reason the bulk-edit methods can’t update the local cached copy directly? Maybe it’s just hard to do with how it’s currently implemented :\

1reaction
Misha-133commented, Apr 3, 2023

This is most likely a race condition error because AddRoles modifies the guild user directly and AddRole DeleteRole does not so if you add role and modify guild user at the same time it will overwrite the role list of the user.

Ah, yup, that’s most likely the issue

@douglasparker You could use user.ModifyAsync instead in this particular case the code would look like

await user.ModifyAsync(x => x.RoleIds = user.Roles.Select(r => r.Id)  // take roles user currently has
            .Except(new [] {user.Guild.Id} ) // @everyone role cannot be added or removed
            .Except(removedRoles) // remove roles from the user
            .Concat(addedRoles) // add roles
            .Distinct().ToList());

This adds & removes roles in a single API request, so it’s more ratelimit friendly.

Read more comments on GitHub >

github_iconTop Results From Across the Web

userManager.AddToRoleAsync() - Error: role does not exist
I'm creating a user registration system using .NET Core, Identity Core, and MVC Core. I'm able to create users and create roles in...
Read more >
Untitled
Adding roles on discord 170+ Good Discord Role Ideas and Names ... roles with Zapier Zapier [Bug]: AddRoleAsync() and AddRolesAsync() removes roles after...
Read more >
(Possible Bug) Roles are getting removed every time ...
When you've joined a Discord server and entered a voice channel, you either click the button next to 'Voice Disconnected' and leave the...
Read more >
Roles.RemoveUserFromRoles(String, String[]) Method
Removes the specified user from the specified roles.
Read more >
C# (CSharp) IGuildUser.ModifyAsync Examples
Now.AddSeconds(30), async() => { if (guser.VoiceChannel == null) { // If they haven't been muted yet, remove them from the 'to mute' list...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found