Repository: python-discord/bot

Issue:
Infraction notifications are sent before the infraction is carried out.
As of now we send the notification to users about their infraction before we actually get Discord to add the muted role or kick them. This is fine as long as the site and the infraction are actually carried out. If they are not carried out then the user gets the notification about the infraction but then it is not logged to the site or it is not applied on Discord.

https://github.com/python-discord/bot/blob/master/bot/cogs/moderation.py#L111-L115

The behaviour of bans must remain the same, because when we ban a user the bot cannot message them anymore (no common servers), but maybe for the other infraction types we can move the notification to the users to the end of the process, after we are sure that the site knows about the infraction and the actions have executed successfully.

EDIT: As stated by Hemlock below, kicks would have to be the same as the bot would not have a mutual guild in the event that they were kicked from the server, mutes & warns are fine to send notifications after actions.


Discussion:

Resolving patch:
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -45,7 +45,7 @@ def proxy_user(user_id: str) -> Object:
 
 class Moderation(Scheduler):
     """
-    Rowboat replacement moderation tools.
+    Server moderation tools.
     """
 
     def __init__(self, bot: Bot):
@@ -66,32 +66,32 @@ async def on_ready(self):
             headers=self.headers
         )
         infraction_list = await response.json()
-        loop = asyncio.get_event_loop()
         for infraction_object in infraction_list:
             if infraction_object["expires_at"] is not None:
-                self.schedule_task(loop, infraction_object["id"], infraction_object)
+                self.schedule_task(self.bot.loop, infraction_object["id"], infraction_object)
 
     # region: Permanent infractions
 
     @with_role(*MODERATION_ROLES)
-    @command(name="warn")
+    @command()
     async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
         """
         Create a warning infraction in the database for a user.
-        :param user: accepts user mention, ID, etc.
-        :param reason: The reason for the warning.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the warning.
         """
 
+        response_object = await post_infraction(ctx, user, type="warning", reason=reason)
+        if response_object is None:
+            return
+
         notified = await self.notify_infraction(
             user=user,
             infr_type="Warning",
             reason=reason
         )
 
-        response_object = await post_infraction(ctx, user, type="warning", reason=reason)
-        if response_object is None:
-            return
-
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: warned {user.mention}"
 
@@ -100,10 +100,13 @@ async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "warning")
+        if notified:
+            dm_status = "Sent"
+            log_content = None
+        else:
+            dm_status = "**Failed**"
+            log_content = ctx.author.mention
 
-        # Send a message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_warn,
             colour=Colour(Colours.soft_red),
@@ -111,32 +114,41 @@ async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
-                Actor: {ctx.message.author}
+                Actor: {ctx.author}
+                DM: {dm_status}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="kick")
+    @command()
     async def kick(self, ctx: Context, user: Member, *, reason: str = None):
         """
         Kicks a user.
-        :param user: accepts user mention, ID, etc.
-        :param reason: The reason for the kick.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the kick.
         """
 
+        response_object = await post_infraction(ctx, user, type="kick", reason=reason)
+        if response_object is None:
+            return
+
         notified = await self.notify_infraction(
             user=user,
             infr_type="Kick",
             reason=reason
         )
 
-        response_object = await post_infraction(ctx, user, type="kick", reason=reason)
-        if response_object is None:
-            return
-
         self.mod_log.ignore(Event.member_remove, user.id)
-        await user.kick(reason=reason)
+
+        try:
+            await user.kick(reason=reason)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: kicked {user.mention}"
@@ -146,31 +158,39 @@ async def kick(self, ctx: Context, user: Member, *, reason: str = None):
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "kick")
+        dm_status = "Sent" if notified else "**Failed**"
+        title = "Member kicked" if action_result else "Member kicked (Failed)"
+        log_content = None if all((notified, action_result)) else ctx.author.mention
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.sign_out,
             colour=Colour(Colours.soft_red),
-            title="Member kicked",
+            title=title,
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
+                DM: {dm_status}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="ban")
+    @command()
     async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
         """
         Create a permanent ban infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param reason: The reason for the ban.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the ban.
         """
 
+        response_object = await post_infraction(ctx, user, type="ban", reason=reason)
+        if response_object is None:
+            return
+
         notified = await self.notify_infraction(
             user=user,
             infr_type="Ban",
@@ -178,13 +198,14 @@ async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str
             reason=reason
         )
 
-        response_object = await post_infraction(ctx, user, type="ban", reason=reason)
-        if response_object is None:
-            return
-
         self.mod_log.ignore(Event.member_ban, user.id)
         self.mod_log.ignore(Event.member_remove, user.id)
-        await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+
+        try:
+            await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: permanently banned {user.mention}"
@@ -194,46 +215,51 @@ async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "ban")
+        dm_status = "Sent" if notified else "**Failed**"
+        log_content = None if all((notified, action_result)) else ctx.author.mention
+        title = "Member permanently banned"
+        if not action_result:
+            title += " (Failed)"
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_ban,
             colour=Colour(Colours.soft_red),
-            title="Member permanently banned",
+            title=title,
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
+                DM: {dm_status}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="mute")
+    @command()
     async def mute(self, ctx: Context, user: Member, *, reason: str = None):
         """
         Create a permanent mute infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param reason: The reason for the mute.
-        """
 
-        notified = await self.notify_infraction(
-            user=user,
-            infr_type="Mute",
-            duration="Permanent",
-            reason=reason
-        )
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the mute.
+        """
 
         response_object = await post_infraction(ctx, user, type="mute", reason=reason)
         if response_object is None:
             return
 
-        # add the mute role
         self.mod_log.ignore(Event.member_update, user.id)
         await user.add_roles(self._muted_role, reason=reason)
 
+        notified = await self.notify_infraction(
+            user=user,
+            infr_type="Mute",
+            duration="Permanent",
+            reason=reason
+        )
+
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: permanently muted {user.mention}"
 
@@ -242,10 +268,13 @@ async def mute(self, ctx: Context, user: Member, *, reason: str = None):
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "mute")
+        if notified:
+            dm_status = "Sent"
+            log_content = None
+        else:
+            dm_status = "**Failed**"
+            log_content = ctx.author.mention
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_mute,
             colour=Colour(Colours.soft_red),
@@ -254,42 +283,47 @@ async def mute(self, ctx: Context, user: Member, *, reason: str = None):
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
+                DM: {dm_status}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     # endregion
     # region: Temporary infractions
 
     @with_role(*MODERATION_ROLES)
-    @command(name="tempmute")
+    @command()
     async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None):
         """
         Create a temporary mute infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param duration: The duration for the temporary mute infraction
-        :param reason: The reason for the temporary mute.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`duration`:** The duration for the temporary mute infraction
+        **`reason`:** The reason for the temporary mute.
         """
 
-        notified = await self.notify_infraction(
-            user=user,
-            infr_type="Mute",
-            duration=duration,
-            reason=reason
+        response_object = await post_infraction(
+            ctx, user, type="mute", reason=reason, duration=duration
         )
-
-        response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration)
         if response_object is None:
             return
 
         self.mod_log.ignore(Event.member_update, user.id)
         await user.add_roles(self._muted_role, reason=reason)
 
+        notified = await self.notify_infraction(
+            user=user,
+            infr_type="Mute",
+            duration=duration,
+            reason=reason
+        )
+
         infraction_object = response_object["infraction"]
         infraction_expiration = infraction_object["expires_at"]
 
-        loop = asyncio.get_event_loop()
-        self.schedule_task(loop, infraction_object["id"], infraction_object)
+        self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object)
 
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}"
@@ -299,10 +333,13 @@ async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: s
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "mute")
+        if notified:
+            dm_status = "Sent"
+            log_content = None
+        else:
+            dm_status = "**Failed**"
+            log_content = ctx.author.mention
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_mute,
             colour=Colour(Colours.soft_red),
@@ -311,22 +348,34 @@ async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: s
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
+                DM: {dm_status}
                 Reason: {reason}
                 Duration: {duration}
                 Expires: {infraction_expiration}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="tempban")
-    async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None):
+    @command()
+    async def tempban(
+        self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None
+    ):
         """
         Create a temporary ban infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param duration: The duration for the temporary ban infraction
-        :param reason: The reason for the temporary ban.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`duration`:** The duration for the temporary ban infraction
+        **`reason`:** The reason for the temporary ban.
         """
 
+        response_object = await post_infraction(
+            ctx, user, type="ban", reason=reason, duration=duration
+        )
+        if response_object is None:
+            return
+
         notified = await self.notify_infraction(
             user=user,
             infr_type="Ban",
@@ -334,20 +383,19 @@ async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: s
             reason=reason
         )
 
-        response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration)
-        if response_object is None:
-            return
-
         self.mod_log.ignore(Event.member_ban, user.id)
         self.mod_log.ignore(Event.member_remove, user.id)
-        guild: Guild = ctx.guild
-        await guild.ban(user, reason=reason, delete_message_days=0)
+
+        try:
+            await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         infraction_object = response_object["infraction"]
         infraction_expiration = infraction_object["expires_at"]
 
-        loop = asyncio.get_event_loop()
-        self.schedule_task(loop, infraction_object["id"], infraction_object)
+        self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object)
 
         dm_result = ":incoming_envelope: " if notified else ""
         action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}"
@@ -357,67 +405,74 @@ async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: s
         else:
             await ctx.send(f"{action} ({reason}).")
 
-        if not notified:
-            await self.log_notify_failure(user, ctx.author, "ban")
+        dm_status = "Sent" if notified else "**Failed**"
+        log_content = None if all((notified, action_result)) else ctx.author.mention
+        title = "Member temporarily banned"
+        if not action_result:
+            title += " (Failed)"
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_ban,
             colour=Colour(Colours.soft_red),
             thumbnail=user.avatar_url_as(static_format="png"),
-            title="Member temporarily banned",
+            title=title,
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
+                DM: {dm_status}
                 Reason: {reason}
                 Duration: {duration}
                 Expires: {infraction_expiration}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     # endregion
     # region: Permanent shadow infractions
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_warn", hidden=True, aliases=['shadowwarn', 'swarn', 'note'])
-    async def shadow_warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
+    @command(hidden=True, aliases=['shadowwarn', 'swarn', 'shadow_warn'])
+    async def note(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
         """
-        Create a warning infraction in the database for a user.
-        :param user: accepts user mention, ID, etc.
-        :param reason: The reason for the warning.
+        Create a private infraction note in the database for a user.
+
+        **`user`:** accepts user mention, ID, etc.
+        **`reason`:** The reason for the warning.
         """
 
-        response_object = await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)
+        response_object = await post_infraction(
+            ctx, user, type="warning", reason=reason, hidden=True
+        )
         if response_object is None:
             return
 
         if reason is None:
-            result_message = f":ok_hand: note added for {user.mention}."
+            await ctx.send(f":ok_hand: note added for {user.mention}.")
         else:
-            result_message = f":ok_hand: note added for {user.mention} ({reason})."
-
-        await ctx.send(result_message)
+            await ctx.send(f":ok_hand: note added for {user.mention} ({reason}).")
 
-        # Send a message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_warn,
             colour=Colour(Colours.soft_red),
-            title="Member shadow warned",
+            title="Member note added",
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
                 Reason: {reason}
-            """)
+            """),
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick'])
+    @command(hidden=True, aliases=['shadowkick', 'skick'])
     async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None):
         """
         Kicks a user.
-        :param user: accepts user mention, ID, etc.
-        :param reason: The reason for the kick.
+
+        **`user`:** accepts user mention, ID, etc.
+        **`reason`:** The reason for the kick.
         """
 
         response_object = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True)
@@ -425,35 +480,47 @@ async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None):
             return
 
         self.mod_log.ignore(Event.member_remove, user.id)
-        await user.kick(reason=reason)
+
+        try:
+            await user.kick(reason=reason)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         if reason is None:
-            result_message = f":ok_hand: kicked {user.mention}."
+            await ctx.send(f":ok_hand: kicked {user.mention}.")
         else:
-            result_message = f":ok_hand: kicked {user.mention} ({reason})."
+            await ctx.send(f":ok_hand: kicked {user.mention} ({reason}).")
 
-        await ctx.send(result_message)
+        title = "Member shadow kicked"
+        if action_result:
+            log_content = None
+        else:
+            log_content = ctx.author.mention
+            title += " (Failed)"
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.sign_out,
             colour=Colour(Colours.soft_red),
-            title="Member shadow kicked",
+            title=title,
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_ban", hidden=True, aliases=['shadowban', 'sban'])
+    @command(hidden=True, aliases=['shadowban', 'sban'])
     async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
         """
         Create a permanent ban infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param reason: The reason for the ban.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the ban.
         """
 
         response_object = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True)
@@ -462,53 +529,61 @@ async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reaso
 
         self.mod_log.ignore(Event.member_ban, user.id)
         self.mod_log.ignore(Event.member_remove, user.id)
-        await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+
+        try:
+            await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         if reason is None:
-            result_message = f":ok_hand: permanently banned {user.mention}."
+            await ctx.send(f":ok_hand: permanently banned {user.mention}.")
         else:
-            result_message = f":ok_hand: permanently banned {user.mention} ({reason})."
+            await ctx.send(f":ok_hand: permanently banned {user.mention} ({reason}).")
 
-        await ctx.send(result_message)
+        title = "Member permanently banned"
+        if action_result:
+            log_content = None
+        else:
+            log_content = ctx.author.mention
+            title += " (Failed)"
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_ban,
             colour=Colour(Colours.soft_red),
-            title="Member permanently banned",
+            title=title,
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
                 Reason: {reason}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_mute", hidden=True, aliases=['shadowmute', 'smute'])
+    @command(hidden=True, aliases=['shadowmute', 'smute'])
     async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None):
         """
         Create a permanent mute infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param reason: The reason for the mute.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`reason`:** The reason for the mute.
         """
 
         response_object = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True)
         if response_object is None:
             return
 
-        # add the mute role
         self.mod_log.ignore(Event.member_update, user.id)
         await user.add_roles(self._muted_role, reason=reason)
 
         if reason is None:
-            result_message = f":ok_hand: permanently muted {user.mention}."
+            await ctx.send(f":ok_hand: permanently muted {user.mention}.")
         else:
-            result_message = f":ok_hand: permanently muted {user.mention} ({reason})."
-
-        await ctx.send(result_message)
+            await ctx.send(f":ok_hand: permanently muted {user.mention} ({reason}).")
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_mute,
             colour=Colour(Colours.soft_red),
@@ -518,23 +593,29 @@ async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None):
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
                 Reason: {reason}
-            """)
+            """),
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     # endregion
     # region: Temporary shadow infractions
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_tempmute", hidden=True, aliases=["shadowtempmute, stempmute"])
-    async def shadow_tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None):
+    @command(hidden=True, aliases=["shadowtempmute, stempmute"])
+    async def shadow_tempmute(
+        self, ctx: Context, user: Member, duration: str, *, reason: str = None
+    ):
         """
         Create a temporary mute infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param duration: The duration for the temporary mute infraction
-        :param reason: The reason for the temporary mute.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`duration`:** The duration for the temporary mute infraction
+        **`reason`:** The reason for the temporary mute.
         """
 
-        response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration, hidden=True)
+        response_object = await post_infraction(
+            ctx, user, type="mute", reason=reason, duration=duration, hidden=True
+        )
         if response_object is None:
             return
 
@@ -544,17 +625,15 @@ async def shadow_tempmute(self, ctx: Context, user: Member, duration: str, *, re
         infraction_object = response_object["infraction"]
         infraction_expiration = infraction_object["expires_at"]
 
-        loop = asyncio.get_event_loop()
-        self.schedule_expiration(loop, infraction_object)
+        self.schedule_expiration(ctx.bot.loop, infraction_object)
 
         if reason is None:
-            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}."
+            await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.")
         else:
-            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
-
-        await ctx.send(result_message)
+            await ctx.send(
+                f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
+            )
 
-        # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_mute,
             colour=Colour(Colours.soft_red),
@@ -566,67 +645,84 @@ async def shadow_tempmute(self, ctx: Context, user: Member, duration: str, *, re
                 Reason: {reason}
                 Duration: {duration}
                 Expires: {infraction_expiration}
-            """)
+            """),
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     @with_role(*MODERATION_ROLES)
-    @command(name="shadow_tempban", hidden=True, aliases=["shadowtempban, stempban"])
+    @command(hidden=True, aliases=["shadowtempban, stempban"])
     async def shadow_tempban(
             self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None
     ):
         """
         Create a temporary ban infraction in the database for a user.
-        :param user: Accepts user mention, ID, etc.
-        :param duration: The duration for the temporary ban infraction
-        :param reason: The reason for the temporary ban.
+
+        **`user`:** Accepts user mention, ID, etc.
+        **`duration`:** The duration for the temporary ban infraction
+        **`reason`:** The reason for the temporary ban.
         """
 
-        response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration, hidden=True)
+        response_object = await post_infraction(
+            ctx, user, type="ban", reason=reason, duration=duration, hidden=True
+        )
         if response_object is None:
             return
 
         self.mod_log.ignore(Event.member_ban, user.id)
         self.mod_log.ignore(Event.member_remove, user.id)
-        guild: Guild = ctx.guild
-        await guild.ban(user, reason=reason, delete_message_days=0)
+
+        try:
+            await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+            action_result = True
+        except Forbidden:
+            action_result = False
 
         infraction_object = response_object["infraction"]
         infraction_expiration = infraction_object["expires_at"]
 
-        loop = asyncio.get_event_loop()
-        self.schedule_expiration(loop, infraction_object)
+        self.schedule_expiration(ctx.bot.loop, infraction_object)
 
         if reason is None:
-            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}."
+            await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.")
         else:
-            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+            await ctx.send(
+                f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+            )
 
-        await ctx.send(result_message)
+        title = "Member temporarily banned"
+        if action_result:
+            log_content = None
+        else:
+            log_content = ctx.author.mention
+            title += " (Failed)"
 
         # Send a log message to the mod log
         await self.mod_log.send_log_message(
             icon_url=Icons.user_ban,
             colour=Colour(Colours.soft_red),
             thumbnail=user.avatar_url_as(static_format="png"),
-            title="Member temporarily banned",
+            title=title,
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
                 Actor: {ctx.message.author}
                 Reason: {reason}
                 Duration: {duration}
                 Expires: {infraction_expiration}
-            """)
+            """),
+            content=log_content,
+            footer=f"ID {response_object['infraction']['id']}"
         )
 
     # endregion
     # region: Remove infractions (un- commands)
 
     @with_role(*MODERATION_ROLES)
-    @command(name="unmute")
+    @command()
     async def unmute(self, ctx: Context, user: Member):
         """
         Deactivates the active mute infraction for a user.
-        :param user: Accepts user mention, ID, etc.
+
+        **`user`:** Accepts user mention, ID, etc.
         """
 
         try:
@@ -638,16 +734,20 @@ async def unmute(self, ctx: Context, user: Member):
                 ),
                 headers=self.headers
             )
+
             response_object = await response.json()
             if "error_code" in response_object:
-                await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
-                return
+                return await ctx.send(
+                    ":x: There was an error removing the infraction: "
+                    f"{response_object['error_message']}"
+                )
 
             infraction_object = response_object["infraction"]
             if infraction_object is None:
                 # no active infraction
-                await ctx.send(f":x: There is no active mute infraction for user {user.mention}.")
-                return
+                return await ctx.send(
+                    f":x: There is no active mute infraction for user {user.mention}."
+                )
 
             await self._deactivate_infraction(infraction_object)
             if infraction_object["expires_at"] is not None:
@@ -660,11 +760,16 @@ async def unmute(self, ctx: Context, user: Member):
                 icon_url=Icons.user_unmute
             )
 
-            dm_result = ":incoming_envelope: " if notified else ""
-            await ctx.send(f"{dm_result}:ok_hand: Un-muted {user.mention}.")
+            if notified:
+                dm_status = "Sent"
+                dm_emoji = ":incoming_envelope: "
+                log_content = None
+            else:
+                dm_status = "**Failed**"
+                dm_emoji = ""
+                log_content = ctx.author.mention
 
-            if not notified:
-                await self.log_notify_failure(user, ctx.author, "unmute")
+            await ctx.send(f"{dm_emoji}:ok_hand: Un-muted {user.mention}.")
 
             # Send a log message to the mod log
             await self.mod_log.send_log_message(
@@ -676,19 +781,23 @@ async def unmute(self, ctx: Context, user: Member):
                     Member: {user.mention} (`{user.id}`)
                     Actor: {ctx.message.author}
                     Intended expiry: {infraction_object['expires_at']}
-                """)
+                    DM: {dm_status}
+                """),
+                footer=infraction_object["id"],
+                content=log_content
             )
-        except Exception:
-            log.exception("There was an error removing an infraction.")
+
+        except Exception as e:
+            log.exception("There was an error removing an infraction.", exc_info=e)
             await ctx.send(":x: There was an error removing the infraction.")
-            return
 
     @with_role(*MODERATION_ROLES)
-    @command(name="unban")
+    @command()
     async def unban(self, ctx: Context, user: Union[User, proxy_user]):
         """
         Deactivates the active ban infraction for a user.
-        :param user: Accepts user mention, ID, etc.
+
+        **`user`:** Accepts user mention, ID, etc.
         """
 
         try:
@@ -702,14 +811,17 @@ async def unban(self, ctx: Context, user: Union[User, proxy_user]):
             )
             response_object = await response.json()
             if "error_code" in response_object:
-                await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
-                return
+                return await ctx.send(
+                    ":x: There was an error removing the infraction: "
+                    f"{response_object['error_message']}"
+                )
 
             infraction_object = response_object["infraction"]
             if infraction_object is None:
                 # no active infraction
-                await ctx.send(f":x: There is no active ban infraction for user {user.mention}.")
-                return
+                return await ctx.send(
+                    f":x: There is no active ban infraction for user {user.mention}."
+                )
 
             await self._deactivate_infraction(infraction_object)
             if infraction_object["expires_at"] is not None:
@@ -732,7 +844,6 @@ async def unban(self, ctx: Context, user: Union[User, proxy_user]):
         except Exception:
             log.exception("There was an error removing an infraction.")
             await ctx.send(":x: There was an error removing the infraction.")
-            return
 
     # endregion
     # region: Edit infraction commands
@@ -755,10 +866,12 @@ async def infraction_edit_group(self, ctx: Context):
     @infraction_edit_group.command(name="duration")
     async def edit_duration(self, ctx: Context, infraction_id: str, duration: str):
         """
-        Sets the duration of the given infraction, relative to the time of updating.
-        :param infraction_id: the id (UUID) of the infraction
-        :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark
-        the infraction as permanent.
+        Sets the duration of the given infraction, relative to the time of
+        updating.
+
+        **`infraction_id`:** The ID (UUID) of the infraction.
+        **`duration`:** The new duration of the infraction, relative to the
+          time of updating. Use "permanent" to the infraction as permanent.
         """
 
         try:
@@ -784,8 +897,10 @@ async def edit_duration(self, ctx: Context, infraction_id: str, duration: str):
             )
             response_object = await response.json()
             if "error_code" in response_object or response_object.get("success") is False:
-                await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
-                return
+                return await ctx.send(
+                    ":x: There was an error updating the infraction: "
+                    f"{response_object['error_message']}"
+                )
 
             infraction_object = response_object["infraction"]
             # Re-schedule
@@ -796,7 +911,10 @@ async def edit_duration(self, ctx: Context, infraction_id: str, duration: str):
             if duration is None:
                 await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")
             else:
-                await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.")
+                await ctx.send(
+                    ":ok_hand: Updated infraction: set to expire on "
+                    f"{infraction_object['expires_at']}."
+                )
 
         except Exception:
             log.exception("There was an error updating an infraction.")
@@ -839,8 +957,8 @@ async def edit_duration(self, ctx: Context, infraction_id: str, duration: str):
     async def edit_reason(self, ctx: Context, infraction_id: str, *, reason: str):
         """
         Sets the reason of the given infraction.
-        :param infraction_id: the id (UUID) of the infraction
-        :param reason: The new reason of the infraction
+        **`infraction_id`:** The ID (UUID) of the infraction.
+        **`reason`:** The new reason of the infraction.
         """
 
         try:
@@ -863,14 +981,15 @@ async def edit_reason(self, ctx: Context, infraction_id: str, *, reason: str):
             )
             response_object = await response.json()
             if "error_code" in response_object or response_object.get("success") is False:
-                await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
-                return
+                return await ctx.send(
+                    ":x: There was an error updating the infraction: "
+                    f"{response_object['error_message']}"
+                )
 
             await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".")
         except Exception:
             log.exception("There was an error updating an infraction.")
-            await ctx.send(":x: There was an error updating the infraction.")
-            return
+            return await ctx.send(":x: There was an error updating the infraction.")
 
         new_infraction = response_object["infraction"]
         prev_infraction = previous_object["infraction"]
@@ -1004,6 +1123,7 @@ async def send_infraction_list(self, ctx: Context, embed: Embed, infractions: li
     def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict):
         """
         Schedules a task to expire a temporary infraction.
+
         :param loop: the asyncio event loop
         :param infraction_object: the infraction object to expire at the end of the task
         """
@@ -1032,9 +1152,10 @@ def cancel_expiration(self, infraction_id: str):
 
     async def _scheduled_task(self, infraction_object: dict):
         """
-        A co-routine which marks an infraction as expired after the delay from the time of scheduling
-        to the time of expiration. At the time of expiration, the infraction is marked as inactive on the website,
-        and the expiration task is cancelled.
+        A co-routine which marks an infraction as expired after the delay from the time of
+        scheduling to the time of expiration. At the time of expiration, the infraction is
+        marked as inactive on the website, and the expiration task is cancelled.
+
         :param infraction_object: the infraction in question
         """
 
@@ -1061,8 +1182,9 @@ async def _scheduled_task(self, infraction_object: dict):
 
     async def _deactivate_infraction(self, infraction_object):
         """
-        A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or
-        un-schedule an expiration task.
+        A co-routine which marks an infraction as inactive on the website. This co-routine does
+        not cancel or un-schedule an expiration task.
+
         :param infraction_object: the infraction in question
         """
 
@@ -1116,7 +1238,8 @@ def _infraction_to_string(self, infraction_object):
         return lines.strip()
 
     async def notify_infraction(
-            self, user: Union[User, Member], infr_type: str, duration: str = None, reason: str = None
+            self, user: Union[User, Member], infr_type: str, duration: str = None,
+            reason: str = None
     ):
         """
         Notify a user of their fresh infraction :)
@@ -1150,7 +1273,8 @@ async def notify_infraction(
         return await self.send_private_embed(user, embed)
 
     async def notify_pardon(
-            self, user: Union[User, Member], title: str, content: str, icon_url: str = Icons.user_verified
+            self, user: Union[User, Member], title: str, content: str,
+            icon_url: str = Icons.user_verified
     ):
         """
         Notify a user that an infraction has been lifted.
@@ -1197,7 +1321,10 @@ async def log_notify_failure(self, target: str, actor: Member, infraction_type:
             content=actor.mention,
             colour=Colour(Colours.soft_red),
             title="Notification Failed",
-            text=f"Direct message was unable to be sent.\nUser: {target.mention}\nType: {infraction_type}"
+            text=(
+                f"Direct message was unable to be sent.\nUser: {target.mention}\n"
+                f"Type: {infraction_type}"
+            )
         )
 
     # endregion
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -116,7 +116,7 @@ async def send_log_message(
             content: Optional[str] = None,
             additional_embeds: Optional[List[Embed]] = None,
             timestamp_override: Optional[datetime.datetime] = None,
-            footer_override: Optional[str] = None,
+            footer: Optional[str] = None,
     ):
         embed = Embed(description=text)
 
@@ -127,8 +127,8 @@ async def send_log_message(
 
         embed.timestamp = timestamp_override or datetime.datetime.utcnow()
 
-        if footer_override:
-            embed.set_footer(text=footer_override)
+        if footer:
+            embed.set_footer(text=footer)
 
         if thumbnail:
             embed.set_thumbnail(url=thumbnail)
@@ -710,7 +710,7 @@ async def on_message_edit(self, before: Message, after: Message):
 
         await self.send_log_message(
             Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response,
-            channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer
+            channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer
         )
 
         await self.send_log_message(


Test patch: