Repository: Cog-Creators/Red-DiscordBot

Issue:
"[p]muteset makerole" can error out and fail if ratelimited
# Command bugs

#### Command name

`[p]muteset makerole`

#### What cog is this command from?

Mutes

#### What were you expecting to happen?

The command to successfully create a new role with overrides configured for a server's channels and categories

#### What actually happened?

It causes an exception, leaving a partially-configured role and posting an error in chat. The traceback ends with `discord.ext.commands.errors.CommandInvokeError: Command raised an exception: HTTPException: 429 Too Many Requests (error code: 0): You are being rate limited.`

#### How can we reproduce this issue?

1. Start with a server that has no mute role configured, and a considerably large number of channels.
2. Run `[p]muteset makerole [role]`
3. If Discord ratelimits Red while it's applying overrides, the command will error out leaving the role only partially configured.


Discussion:
Please provide the full traceback as it can help debugging the issue. Seeing just the last line isn't very helpful .
My bad, does this work?:
```
redbot[50180]: [2020-10-26 04:09:45] [ERROR] red: Exception in command 'muteset makerole'
redbot[50180]: Traceback (most recent call last):
redbot[50180]:   File "/home/redbeta/.local/lib/python3.8/site-packages/discord/ext/commands/core.py", line 85, in wrapped
redbot[50180]:     ret = await coro(*args, **kwargs)
redbot[50180]:   File "/home/redbeta/.local/lib/python3.8/site-packages/redbot/cogs/mutes/mutes.py", line 697, in make_mute_role
redbot[50180]:     errors = await asyncio.gather(*tasks)
redbot[50180]:   File "/home/redbeta/.local/lib/python3.8/site-packages/redbot/cogs/mutes/mutes.py", line 729, in _set_mute_role_overwrites
redbot[50180]:     await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup"))
redbot[50180]:   File "/home/redbeta/.local/lib/python3.8/site-packages/discord/abc.py", line 658, in set_permissions
redbot[50180]:     await http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type, reason=reason)
redbot[50180]:   File "/home/redbeta/.local/lib/python3.8/site-packages/discord/http.py", line 258, in request
redbot[50180]:     raise HTTPException(r, data)
redbot[50180]: discord.errors.HTTPException: 429 Too Many Requests (error code: 0): You are being rate limited.
```
```py

for channel in ctx.guild.channels:
    tasks.append(self._set_mute_role_overwrites(role, channel))
errors = await asyncio.gather(*tasks)

```
Seems to be the source of the problem, I would assume the command is being run on a server with many channels (@chloecormier  can you confirm how many channels are in the server?)
Right, the server I can reproduce this in has 209 text channels and 19 voice channels... yeah, I know it's excessive.
Whatever we decide to do, we should remember to do it for all the usage of `gather` we have in Mutes.

Resolving patch:
diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py
--- a/redbot/cogs/mutes/mutes.py
+++ b/redbot/cogs/mutes/mutes.py
@@ -12,6 +12,7 @@
 
 from redbot.core.bot import Red
 from redbot.core import commands, checks, i18n, modlog, Config
+from redbot.core.utils import bounded_gather
 from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify
 from redbot.core.utils.mod import get_audit_reason
 from redbot.core.utils.menus import start_adding_reactions
@@ -328,7 +329,7 @@ async def _auto_channel_unmute_user_multi(
             tasks.append(
                 self._auto_channel_unmute_user(guild.get_channel(channel), mute_data, False)
             )
-        results = await asyncio.gather(*tasks)
+        results = await bounded_gather(*tasks)
         unmuted_channels = [guild.get_channel(c) for c in channels.keys()]
         for result in results:
             if not result:
@@ -678,6 +679,7 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None):
     @muteset.command(name="makerole")
     @checks.admin_or_permissions(manage_roles=True)
     @commands.bot_has_guild_permissions(manage_roles=True)
+    @commands.max_concurrency(1, commands.BucketType.guild)
     async def make_mute_role(self, ctx: commands.Context, *, name: str):
         """Create a Muted role.
 
@@ -687,6 +689,15 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str):
         If you already have a muted role created on the server use
         `[p]muteset role ROLE_NAME_HERE`
         """
+        if await self.config.guild(ctx.guild).mute_role():
+            command = f"`{ctx.clean_prefix}muteset role`"
+            return await ctx.send(
+                _(
+                    "There is already a mute role setup in this server. "
+                    "Please remove it with {command} before trying to "
+                    "create a new one."
+                ).format(command=command)
+            )
         async with ctx.typing():
             perms = discord.Permissions()
             perms.update(send_messages=False, speak=False, add_reactions=False)
@@ -694,20 +705,22 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str):
                 role = await ctx.guild.create_role(
                     name=name, permissions=perms, reason=_("Mute role setup")
                 )
+                await self.config.guild(ctx.guild).mute_role.set(role.id)
+                # save the role early incase of issue later
             except discord.errors.Forbidden:
                 return await ctx.send(_("I could not create a muted role in this server."))
             errors = []
             tasks = []
             for channel in ctx.guild.channels:
                 tasks.append(self._set_mute_role_overwrites(role, channel))
-            errors = await asyncio.gather(*tasks)
+            errors = await bounded_gather(*tasks)
             if any(errors):
                 msg = _(
                     "I could not set overwrites for the following channels: {channels}"
                 ).format(channels=humanize_list([i for i in errors if i]))
                 for page in pagify(msg, delims=[" "]):
                     await ctx.send(page)
-            await self.config.guild(ctx.guild).mute_role.set(role.id)
+
             await ctx.send(_("Mute role set to {role}").format(role=role.name))
         if not await self.config.guild(ctx.guild).notification_channel():
             command_1 = f"`{ctx.clean_prefix}muteset notification`"
@@ -1321,7 +1334,7 @@ async def mute_user(
             tasks = []
             for channel in guild.channels:
                 tasks.append(self.channel_mute_user(guild, channel, author, user, until, reason))
-            task_result = await asyncio.gather(*tasks)
+            task_result = await bounded_gather(*tasks)
             for task in task_result:
                 if not task["success"]:
                     ret["channels"].append((task["channel"], task["reason"]))
@@ -1382,7 +1395,7 @@ async def unmute_user(
             tasks = []
             for channel in guild.channels:
                 tasks.append(self.channel_unmute_user(guild, channel, author, user, reason))
-            results = await asyncio.gather(*tasks)
+            results = await bounded_gather(*tasks)
             for task in results:
                 if not task["success"]:
                     ret["channels"].append((task["channel"], task["reason"]))


Test patch: