Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3] Unmute has odd behaviour on admins
# Command bugs

<!-- 
Did you find a bug with a command? Fill out the following:
-->

#### Command name

Unmute

#### What cog is this command from?

Mod

#### What were you expecting to happen?

Unmute someone from the server.

#### What actually happened?

It sent the "user has been unmuted from the server" but did not remove the permissions from the channels.

#### How can we reproduce this issue?

Mute someone using [p]mute server user and then [p]unmute server user and it will not remove the permissions from the channels.



Discussion:
What kind of info is needed?
traceback error... if there is one

edit: "but did not remove the permissions from the channels." that doesn't mean they weren't unmuted it just means the user profile on the channel wasn't removed if you check it the write perm was re-enabled (if i understood that correctly)
I've been playing around with this a little. This is in a test server with no special channel overwrites beyond an admin role that has access to a private category, text channel, and voice channel. This isn't full QA by any means but it's what I found while messing around with it.

Mute:
For every channel the bot and member have access to...
Categories and VCs are denied the Speak permission.
Text channels are denied Send Messages and Add Reactions.
(As a small note this causes categories and their text channels to no longer be in sync.)
No traceback in console.

Unmute:
I got a couple different results depending on the member's permissions.
If the member has the admin override, nothing happens. Overwrites set via Mute are as they were.
If the member has no admin override, text channel overwrites are deleted, while category and VC overwrites for that member are set to all None (including for categories / VCs that were not touched by mute beforehand).
The bot having a higher role than the member or not has no effect on the above behavior.
No traceback in console in either case.
The bot has the highest role and none of the users I am trying to mute have a role above the BOT, I'm confused why mute works but unmute does not, any info?
Literally that big ol' comment directly above yours was a huge infodump when I tested this issue out. Give it a read if you want info.
Yes I read that, from what I understand it does not work the way it is intended to?
As it stands, based on my testing, `[p]unmute` does *function*, though it could use improvements. In my testing it only ever failed to unmute members who had the Administrator override in their permissions, which allows them to bypass channel permissions. So muting/unmuting doesn't affect them either way. Since currently the bot only unmutes in channels the member can't send messages to, it doesn't change permissions for Administrator members.

In other words, since the muted member was an Administrator, they were never muted in the first place, so there's nothing to unmute, so the bot didn't touch channel overwrites.
I see now, I'm confused why it mutes people with administrator but then does not unmute them though

Resolving patch:
diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py
--- a/redbot/cogs/mod/mod.py
+++ b/redbot/cogs/mod/mod.py
@@ -893,34 +893,33 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso
         author = ctx.author
         if user_voice_state:
             channel = user_voice_state.channel
-            if channel and channel.permissions_for(user).speak:
-                overwrites = channel.overwrites_for(user)
-                overwrites.speak = False
-                audit_reason = get_audit_reason(ctx.author, reason)
-                await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
-                await ctx.send(
-                    _("Muted {user} in channel {channel.name}").format(user, channel=channel)
-                )
-                try:
-                    await modlog.create_case(
-                        self.bot,
-                        guild,
-                        ctx.message.created_at,
-                        "boicemute",
-                        user,
-                        author,
-                        reason,
-                        until=None,
-                        channel=channel,
+            if channel:
+                audit_reason = get_audit_reason(author, reason)
+
+                success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
+
+                if success:
+                    await ctx.send(
+                        _("Muted {user} in channel {channel.name}").format(
+                            user=user, channel=channel
+                        )
                     )
-                except RuntimeError as e:
-                    await ctx.send(e)
-                return
-            elif channel.permissions_for(user).speak is False:
-                await ctx.send(
-                    _("That user is already muted in {channel}!").format(channel=channel.name)
-                )
-                return
+                    try:
+                        await modlog.create_case(
+                            self.bot,
+                            guild,
+                            ctx.message.created_at,
+                            "vmute",
+                            user,
+                            author,
+                            reason,
+                            until=None,
+                            channel=channel,
+                        )
+                    except RuntimeError as e:
+                        await ctx.send(e)
+                else:
+                    await channel.send(issue)
             else:
                 await ctx.send(_("That user is not in a voice channel right now!"))
         else:
@@ -938,13 +937,7 @@ async def channel_mute(
         author = ctx.message.author
         channel = ctx.message.channel
         guild = ctx.guild
-
-        if reason is None:
-            audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author)
-        else:
-            audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format(
-                a=author, r=reason
-            )
+        audit_reason = get_audit_reason(author, reason)
 
         success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
 
@@ -975,26 +968,12 @@ async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reaso
         """Mutes user in the server"""
         author = ctx.message.author
         guild = ctx.guild
-        if reason is None:
-            audit_reason = "server mute requested by {author} (ID {author.id})".format(
-                author=author
-            )
-        else:
-            audit_reason = (
-                "server mute requested by {author} (ID {author.id}). Reason: {reason}"
-            ).format(author=author, reason=reason)
+        audit_reason = get_audit_reason(author, reason)
 
         mute_success = []
         for channel in guild.channels:
-            if not isinstance(channel, discord.TextChannel):
-                if channel.permissions_for(user).speak:
-                    overwrites = channel.overwrites_for(user)
-                    overwrites.speak = False
-                    audit_reason = get_audit_reason(ctx.author, reason)
-                    await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
-            else:
-                success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
-                mute_success.append((success, issue))
+            success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
+            mute_success.append((success, issue))
             await asyncio.sleep(0.1)
         await ctx.send(_("User has been muted in this server."))
         try:
@@ -1015,7 +994,7 @@ async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reaso
     async def mute_user(
         self,
         guild: discord.Guild,
-        channel: discord.TextChannel,
+        channel: discord.abc.GuildChannel,
         author: discord.Member,
         user: discord.Member,
         reason: str,
@@ -1023,25 +1002,32 @@ async def mute_user(
         """Mutes the specified user in the specified channel"""
         overwrites = channel.overwrites_for(user)
         permissions = channel.permissions_for(user)
-        perms_cache = await self.settings.member(user).perms_cache()
 
-        if overwrites.send_messages is False or permissions.send_messages is False:
+        if permissions.administrator:
+            return False, T_(mute_unmute_issues["is_admin"])
+
+        new_overs = {}
+        if not isinstance(channel, discord.TextChannel):
+            new_overs.update(speak=False)
+        if not isinstance(channel, discord.VoiceChannel):
+            new_overs.update(send_messages=False, add_reactions=False)
+
+        if all(getattr(permissions, p) is False for p in new_overs.keys()):
             return False, T_(mute_unmute_issues["already_muted"])
 
         elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
             return False, T_(mute_unmute_issues["hierarchy_problem"])
 
-        perms_cache[str(channel.id)] = {
-            "send_messages": overwrites.send_messages,
-            "add_reactions": overwrites.add_reactions,
-        }
-        overwrites.update(send_messages=False, add_reactions=False)
+        old_overs = {k: getattr(overwrites, k) for k in new_overs}
+        overwrites.update(**new_overs)
         try:
             await channel.set_permissions(user, overwrite=overwrites, reason=reason)
         except discord.Forbidden:
             return False, T_(mute_unmute_issues["permissions_issue"])
         else:
-            await self.settings.member(user).perms_cache.set(perms_cache)
+            await self.settings.member(user).set_raw(
+                "perms_cache", str(channel.id), value=old_overs
+            )
             return True, None
 
     @commands.group()
@@ -1061,37 +1047,39 @@ async def unmute_voice(
     ):
         """Unmute a user in their current voice channel."""
         user_voice_state = user.voice
+        guild = ctx.guild
+        author = ctx.author
         if user_voice_state:
             channel = user_voice_state.channel
-            if channel and channel.permissions_for(user).speak is False:
-                overwrites = channel.overwrites_for(user)
-                overwrites.speak = None
-                audit_reason = get_audit_reason(ctx.author, reason)
-                await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
-                author = ctx.author
-                guild = ctx.guild
-                await ctx.send(
-                    _("Unmuted {}#{} in channel {}").format(
-                        user.name, user.discriminator, channel.name
-                    )
+            if channel:
+                audit_reason = get_audit_reason(author, reason)
+
+                success, message = await self.unmute_user(
+                    guild, channel, author, user, audit_reason
                 )
-                try:
-                    await modlog.create_case(
-                        self.bot,
-                        guild,
-                        ctx.message.created_at,
-                        "voiceunmute",
-                        user,
-                        author,
-                        reason,
-                        until=None,
-                        channel=channel,
+
+                if success:
+                    await ctx.send(
+                        _("Unmuted {user} in channel {channel.name}").format(
+                            user=user, channel=channel
+                        )
                     )
-                except RuntimeError as e:
-                    await ctx.send(e)
-            elif channel.permissions_for(user).speak:
-                await ctx.send(_("That user is already unmuted in {}!").format(channel.name))
-                return
+                    try:
+                        await modlog.create_case(
+                            self.bot,
+                            guild,
+                            ctx.message.created_at,
+                            "vunmute",
+                            user,
+                            author,
+                            reason,
+                            until=None,
+                            channel=channel,
+                        )
+                    except RuntimeError as e:
+                        await ctx.send(e)
+                else:
+                    await ctx.send(_("Unmute failed. Reason: {}").format(message))
             else:
                 await ctx.send(_("That user is not in a voice channel right now!"))
         else:
@@ -1109,8 +1097,9 @@ async def unmute_channel(
         channel = ctx.channel
         author = ctx.author
         guild = ctx.guild
+        audit_reason = get_audit_reason(author, reason)
 
-        success, message = await self.unmute_user(guild, channel, author, user)
+        success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
 
         if success:
             await ctx.send(_("User unmuted in this channel."))
@@ -1141,16 +1130,11 @@ async def unmute_guild(
         """Unmute a user in this server."""
         guild = ctx.guild
         author = ctx.author
+        audit_reason = get_audit_reason(author, reason)
 
         unmute_success = []
         for channel in guild.channels:
-            if not isinstance(channel, discord.TextChannel):
-                if channel.permissions_for(user).speak is False:
-                    overwrites = channel.overwrites_for(user)
-                    overwrites.speak = None
-                    audit_reason = get_audit_reason(author, reason)
-                    await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
-            success, message = await self.unmute_user(guild, channel, author, user)
+            success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
             unmute_success.append((success, message))
             await asyncio.sleep(0.1)
         await ctx.send(_("User has been unmuted in this server."))
@@ -1171,45 +1155,37 @@ async def unmute_guild(
     async def unmute_user(
         self,
         guild: discord.Guild,
-        channel: discord.TextChannel,
+        channel: discord.abc.GuildChannel,
         author: discord.Member,
         user: discord.Member,
+        reason: str,
     ) -> (bool, str):
         overwrites = channel.overwrites_for(user)
-        permissions = channel.permissions_for(user)
         perms_cache = await self.settings.member(user).perms_cache()
 
-        if overwrites.send_messages or permissions.send_messages:
+        if channel.id in perms_cache:
+            old_values = perms_cache[channel.id]
+        else:
+            old_values = {"send_messages": None, "add_reactions": None, "speak": None}
+
+        if all(getattr(overwrites, k) == v for k, v in old_values.items()):
             return False, T_(mute_unmute_issues["already_unmuted"])
 
         elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
             return False, T_(mute_unmute_issues["hierarchy_problem"])
 
-        if channel.id in perms_cache:
-            old_values = perms_cache[channel.id]
-        else:
-            old_values = {"send_messages": None, "add_reactions": None}
-        overwrites.update(
-            send_messages=old_values["send_messages"], add_reactions=old_values["add_reactions"]
-        )
-        is_empty = self.are_overwrites_empty(overwrites)
-
+        overwrites.update(**old_values)
         try:
-            if not is_empty:
-                await channel.set_permissions(user, overwrite=overwrites)
-            else:
+            if overwrites.is_empty():
                 await channel.set_permissions(
-                    user, overwrite=cast(discord.PermissionOverwrite, None)
+                    user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason
                 )
+            else:
+                await channel.set_permissions(user, overwrite=overwrites, reason=reason)
         except discord.Forbidden:
             return False, T_(mute_unmute_issues["permissions_issue"])
         else:
-            try:
-                del perms_cache[channel.id]
-            except KeyError:
-                pass
-            else:
-                await self.settings.member(user).perms_cache.set(perms_cache)
+            await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
             return True, None
 
     @commands.group()
@@ -1695,20 +1671,15 @@ async def on_member_update(self, before: discord.Member, after: discord.Member):
                 while len(nick_list) > 20:
                     nick_list.pop(0)
 
-    @staticmethod
-    def are_overwrites_empty(overwrites):
-        """There is currently no cleaner way to check if a
-        PermissionOverwrite object is empty"""
-        return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
-
 
 _ = lambda s: s
 mute_unmute_issues = {
     "already_muted": _("That user can't send messages in this channel."),
-    "already_unmuted": _("That user isn't muted in this channel!"),
+    "already_unmuted": _("That user isn't muted in this channel."),
     "hierarchy_problem": _(
-        "I cannot let you do that. You are not higher than " "the user in the role hierarchy."
+        "I cannot let you do that. You are not higher than the user in the role hierarchy."
     ),
+    "is_admin": _("That user cannot be muted, as they have the Administrator permission."),
     "permissions_issue": _(
         "Failed to mute user. I need the manage roles "
         "permission and the user I'm muting must be "


Test patch: