diff options
| author | 2021-08-23 20:49:25 +0300 | |
|---|---|---|
| committer | 2021-08-23 20:49:25 +0300 | |
| commit | 164233780b0e44144bb219914579c580d7c0ff54 (patch) | |
| tree | dfce0f2e999841a1293630b7a82ee3ecfd8c310a | |
| parent | Merge pull request #1771 from python-discord/TizzySaurus-patch-1 (diff) | |
| parent | Merge branch 'main' into feat/mod/1665/override-auto-mute (diff) | |
Merge pull request #1682 from python-discord/feat/mod/1665/override-auto-mute
Allow manual mutes to override auto mutes
| -rw-r--r-- | bot/exts/moderation/infraction/_scheduler.py | 25 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/_utils.py | 13 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/infractions.py | 79 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/superstarify.py | 25 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_infractions.py | 6 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_utils.py | 4 | 
6 files changed, 99 insertions, 53 deletions
| diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 3c5e5d3bf..6ba4e74e9 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -258,13 +258,17 @@ class InfractionScheduler:              ctx: Context,              infr_type: str,              user: MemberOrUser, -            send_msg: bool = True +            *, +            send_msg: bool = True, +            notify: bool = True      ) -> None:          """          Prematurely end an infraction for a user and log the action in the mod log.          If `send_msg` is True, then a pardoning confirmation message will be sent to -        the context channel.  Otherwise, no such message will be sent. +        the context channel. Otherwise, no such message will be sent. + +        If `notify` is True, notify the user of the pardon via DM where applicable.          """          log.trace(f"Pardoning {infr_type} infraction for {user}.") @@ -285,7 +289,7 @@ class InfractionScheduler:              return          # Deactivate the infraction and cancel its scheduled expiration task. -        log_text = await self.deactivate_infraction(response[0], send_log=False) +        log_text = await self.deactivate_infraction(response[0], send_log=False, notify=notify)          log_text["Member"] = messages.format_user(user)          log_text["Actor"] = ctx.author.mention @@ -338,7 +342,9 @@ class InfractionScheduler:      async def deactivate_infraction(          self,          infraction: _utils.Infraction, -        send_log: bool = True +        *, +        send_log: bool = True, +        notify: bool = True      ) -> t.Dict[str, str]:          """          Deactivate an active infraction and return a dictionary of lines to send in a mod log. @@ -347,6 +353,8 @@ class InfractionScheduler:          expiration task cancelled. If `send_log` is True, a mod log is sent for the          deactivation of the infraction. +        If `notify` is True, notify the user of the pardon via DM where applicable. +          Infractions of unsupported types will raise a ValueError.          """          guild = self.bot.get_guild(constants.Guild.id) @@ -373,7 +381,7 @@ class InfractionScheduler:          try:              log.trace("Awaiting the pardon action coroutine.") -            returned_log = await self._pardon_action(infraction) +            returned_log = await self._pardon_action(infraction, notify)              if returned_log is not None:                  log_text = {**log_text, **returned_log}  # Merge the logs together @@ -461,10 +469,15 @@ class InfractionScheduler:          return log_text      @abstractmethod -    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]: +    async def _pardon_action( +        self, +        infraction: _utils.Infraction, +        notify: bool +    ) -> t.Optional[t.Dict[str, str]]:          """          Execute deactivation steps specific to the infraction's type and return a log dict. +        If `notify` is True, notify the user of the pardon via DM where applicable.          If an infraction type is unsupported, return None instead.          """          raise NotImplementedError diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 9d94bca2d..b20ef1d06 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -139,15 +139,20 @@ async def get_active_infraction(          # Checks to see if the moderator should be told there is an active infraction          if send_msg:              log.trace(f"{user} has active infractions of type {infr_type}.") -            await ctx.send( -                f":x: According to my records, this user already has a {infr_type} infraction. " -                f"See infraction **#{active_infractions[0]['id']}**." -            ) +            await send_active_infraction_message(ctx, active_infractions[0])          return active_infractions[0]      else:          log.trace(f"{user} does not have active infractions of type {infr_type}.") +async def send_active_infraction_message(ctx: Context, infraction: Infraction) -> None: +    """Send a message stating that the given infraction is active.""" +    await ctx.send( +        f":x: According to my records, this user already has a {infraction['type']} infraction. " +        f"See infraction **#{infraction['id']}**." +    ) + +  async def notify_infraction(          user: MemberOrUser,          infr_type: str, diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 48ffbd773..2f9083c29 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -279,8 +279,19 @@ class Infractions(InfractionScheduler, commands.Cog):      async def apply_mute(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:          """Apply a mute infraction with kwargs passed to `post_infraction`.""" -        if await _utils.get_active_infraction(ctx, user, "mute"): -            return +        if active := await _utils.get_active_infraction(ctx, user, "mute", send_msg=False): +            if active["actor"] != self.bot.user.id: +                await _utils.send_active_infraction_message(ctx, active) +                return + +            # Allow the current mute attempt to override an automatically triggered mute. +            log_text = await self.deactivate_infraction(active, notify=False) +            if "Failure" in log_text: +                await ctx.send( +                    f":x: can't override infraction **mute** for {user.mention}: " +                    f"failed to deactivate. {log_text['Failure']}" +                ) +                return          infraction = await _utils.post_infraction(ctx, user, "mute", reason, active=True, **kwargs)          if infraction is None: @@ -344,7 +355,7 @@ class Infractions(InfractionScheduler, commands.Cog):                  return              log.trace("Old tempban is being replaced by new permaban.") -            await self.pardon_infraction(ctx, "ban", user, is_temporary) +            await self.pardon_infraction(ctx, "ban", user, send_msg=is_temporary)          infraction = await _utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs)          if infraction is None: @@ -402,8 +413,15 @@ class Infractions(InfractionScheduler, commands.Cog):      # endregion      # region: Base pardon functions -    async def pardon_mute(self, user_id: int, guild: discord.Guild, reason: t.Optional[str]) -> t.Dict[str, str]: -        """Remove a user's muted role, DM them a notification, and return a log dict.""" +    async def pardon_mute( +        self, +        user_id: int, +        guild: discord.Guild, +        reason: t.Optional[str], +        *, +        notify: bool = True +    ) -> t.Dict[str, str]: +        """Remove a user's muted role, optionally DM them a notification, and return a log dict."""          user = guild.get_member(user_id)          log_text = {} @@ -412,16 +430,17 @@ class Infractions(InfractionScheduler, commands.Cog):              self.mod_log.ignore(Event.member_update, user.id)              await user.remove_roles(self._muted_role, reason=reason) -            # DM the user about the expiration. -            notified = await _utils.notify_pardon( -                user=user, -                title="You have been unmuted", -                content="You may now send messages in the server.", -                icon_url=_utils.INFRACTION_ICONS["mute"][1] -            ) +            if notify: +                # DM the user about the expiration. +                notified = await _utils.notify_pardon( +                    user=user, +                    title="You have been unmuted", +                    content="You may now send messages in the server.", +                    icon_url=_utils.INFRACTION_ICONS["mute"][1] +                ) +                log_text["DM"] = "Sent" if notified else "**Failed**"              log_text["Member"] = format_user(user) -            log_text["DM"] = "Sent" if notified else "**Failed**"          else:              log.info(f"Failed to unmute user {user_id}: user not found")              log_text["Failure"] = "User was not found in the guild." @@ -443,31 +462,39 @@ class Infractions(InfractionScheduler, commands.Cog):          return log_text -    async def pardon_voice_ban(self, user_id: int, guild: discord.Guild, reason: t.Optional[str]) -> t.Dict[str, str]: -        """Add Voice Verified role back to user, DM them a notification, and return a log dict.""" +    async def pardon_voice_ban( +        self, +        user_id: int, +        guild: discord.Guild, +        *, +        notify: bool = True +    ) -> t.Dict[str, str]: +        """Optionally DM the user a pardon notification and return a log dict."""          user = guild.get_member(user_id)          log_text = {}          if user: -            # DM user about infraction expiration -            notified = await _utils.notify_pardon( -                user=user, -                title="Voice ban ended", -                content="You have been unbanned and can verify yourself again in the server.", -                icon_url=_utils.INFRACTION_ICONS["voice_ban"][1] -            ) +            if notify: +                # DM user about infraction expiration +                notified = await _utils.notify_pardon( +                    user=user, +                    title="Voice ban ended", +                    content="You have been unbanned and can verify yourself again in the server.", +                    icon_url=_utils.INFRACTION_ICONS["voice_ban"][1] +                ) +                log_text["DM"] = "Sent" if notified else "**Failed**"              log_text["Member"] = format_user(user) -            log_text["DM"] = "Sent" if notified else "**Failed**"          else:              log_text["Info"] = "User was not found in the guild."          return log_text -    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]: +    async def _pardon_action(self, infraction: _utils.Infraction, notify: bool) -> t.Optional[t.Dict[str, str]]:          """          Execute deactivation steps specific to the infraction's type and return a log dict. +        If `notify` is True, notify the user of the pardon via DM where applicable.          If an infraction type is unsupported, return None instead.          """          guild = self.bot.get_guild(constants.Guild.id) @@ -475,11 +502,11 @@ class Infractions(InfractionScheduler, commands.Cog):          reason = f"Infraction #{infraction['id']} expired or was pardoned."          if infraction["type"] == "mute": -            return await self.pardon_mute(user_id, guild, reason) +            return await self.pardon_mute(user_id, guild, reason, notify=notify)          elif infraction["type"] == "ban":              return await self.pardon_ban(user_id, guild, reason)          elif infraction["type"] == "voice_ban": -            return await self.pardon_voice_ban(user_id, guild, reason) +            return await self.pardon_voice_ban(user_id, guild, notify=notify)      # endregion diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 07e79b9fe..05a2bbe10 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -192,8 +192,8 @@ class Superstarify(InfractionScheduler, Cog):          """Remove the superstarify infraction and allow the user to change their nickname."""          await self.pardon_infraction(ctx, "superstar", member) -    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]: -        """Pardon a superstar infraction and return a log dict.""" +    async def _pardon_action(self, infraction: _utils.Infraction, notify: bool) -> t.Optional[t.Dict[str, str]]: +        """Pardon a superstar infraction, optionally notify the user via DM, and return a log dict."""          if infraction["type"] != "superstar":              return @@ -208,18 +208,19 @@ class Superstarify(InfractionScheduler, Cog):              )              return {} +        log_text = {"Member": format_user(user)} +          # DM the user about the expiration. -        notified = await _utils.notify_pardon( -            user=user, -            title="You are no longer superstarified", -            content="You may now change your nickname on the server.", -            icon_url=_utils.INFRACTION_ICONS["superstar"][1] -        ) +        if notify: +            notified = await _utils.notify_pardon( +                user=user, +                title="You are no longer superstarified", +                content="You may now change your nickname on the server.", +                icon_url=_utils.INFRACTION_ICONS["superstar"][1] +            ) +            log_text["DM"] = "Sent" if notified else "**Failed**" -        return { -            "Member": format_user(user), -            "DM": "Sent" if notified else "**Failed**" -        } +        return log_text      @staticmethod      def get_nick(infraction_id: int, member_id: int) -> str: diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index b9d527770..f844a9181 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -195,7 +195,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):      async def test_voice_unban_user_not_found(self):          """Should include info to return dict when user was not found from guild."""          self.guild.get_member.return_value = None -        result = await self.cog.pardon_voice_ban(self.user.id, self.guild, "foobar") +        result = await self.cog.pardon_voice_ban(self.user.id, self.guild)          self.assertEqual(result, {"Info": "User was not found in the guild."})      @patch("bot.exts.moderation.infraction.infractions._utils.notify_pardon") @@ -206,7 +206,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):          notify_pardon_mock.return_value = True          format_user_mock.return_value = "my-user" -        result = await self.cog.pardon_voice_ban(self.user.id, self.guild, "foobar") +        result = await self.cog.pardon_voice_ban(self.user.id, self.guild)          self.assertEqual(result, {              "Member": "my-user",              "DM": "Sent" @@ -221,7 +221,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):          notify_pardon_mock.return_value = False          format_user_mock.return_value = "my-user" -        result = await self.cog.pardon_voice_ban(self.user.id, self.guild, "foobar") +        result = await self.cog.pardon_voice_ban(self.user.id, self.guild)          self.assertEqual(result, {              "Member": "my-user",              "DM": "**Failed**" diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5f95ced9f..eb256f1fd 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -94,8 +94,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):          test_case = namedtuple("test_case", ["get_return_value", "expected_output", "infraction_nr", "send_msg"])          test_cases = [              test_case([], None, None, True), -            test_case([{"id": 123987}], {"id": 123987}, "123987", False), -            test_case([{"id": 123987}], {"id": 123987}, "123987", True) +            test_case([{"id": 123987, "type": "ban"}], {"id": 123987, "type": "ban"}, "123987", False), +            test_case([{"id": 123987, "type": "ban"}], {"id": 123987, "type": "ban"}, "123987", True)          ]          for case in test_cases: | 
