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.

Changed files:
bot/cogs/moderation.py, bot/cogs/modlog.py

diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index ac08d3dd..e9acc27b 100644
--- 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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
 
         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 @@ class Moderation(Scheduler):
                 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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
                 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 @@ class Moderation(Scheduler):
                 ),
                 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 @@ class Moderation(Scheduler):
                 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 @@ class Moderation(Scheduler):
                     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 @@ class Moderation(Scheduler):
             )
             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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
     @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 @@ class Moderation(Scheduler):
             )
             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 @@ class Moderation(Scheduler):
             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 @@ class Moderation(Scheduler):
     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 @@ class Moderation(Scheduler):
             )
             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 @@ class Moderation(Scheduler):
     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 @@ class Moderation(Scheduler):
 
     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 @@ class Moderation(Scheduler):
 
     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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
         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 @@ class Moderation(Scheduler):
             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
index e58ca3ae..495795b6 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -116,7 +116,7 @@ class ModLog:
             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 @@ class ModLog:
 
         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 @@ class ModLog:
 
         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(