Repository: Cog-Creators/Red-DiscordBot

Issue:
Configurable mute messages
# Feature request

<!-- This template is for feature requests. Please fill out the following: -->


#### Select the type of feature you are requesting:

<!-- To check a box, replace the space between the [] with a x -->

Functionality

#### Describe your requested feature

<!--
Feel free to describe in as much detail as you wish.

If you are requesting a cog to be included in core: 
    - Describe the functionality in as much detail as possible
    - Include the command structure, if possible
    - Please note that unless it's something that should be core functionality,
      we reserve the right to reject your suggestion and point you to our cog 
      board to request it for a third-party cog
      
If you are requesting a command:
    - Include what cog it should be in and a name for the command
    - Describe the intended functionality for the command
    - Note any restrictions on who can use the command or where it can be used

If you are requesting API functionality:
    - Describe what it should do
    - Note whether it is to extend existing functionality or introduce new functionality
   
-->
- [ ] Have the option of sending a DM on mute to the user being muted just like the Warnings cog or ``[p]kick`` and ``[p]ban`` already do.

- [ ] Add toggle to sending mute/Unmute messages to the channel the command was executed in. This is especially useful for those using 3rd-party tempmutes cogs, however it might be a desired feature for others too, since the modlog entry is sometimes sufficient as a confirmation/logging message.

I would have made a PR but the mute feature branch is not in a working state and I didn't want to just PR to dev.

<!-- Aaaaaah markdown is annoying! -->


Discussion:

Resolving patch:
diff --git a/redbot/cogs/mutes/abc.py b/redbot/cogs/mutes/abc.py
--- a/redbot/cogs/mutes/abc.py
+++ b/redbot/cogs/mutes/abc.py
@@ -1,5 +1,5 @@
 from abc import ABC, abstractmethod
-from typing import List, Tuple, Optional, Dict
+from typing import List, Tuple, Optional, Dict, Union
 from datetime import datetime
 
 import discord
@@ -25,3 +25,15 @@ async def _voice_perm_check(
         ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
     ) -> bool:
         raise NotImplementedError()
+
+    @abstractmethod
+    async def _send_dm_notification(
+        self,
+        user: Union[discord.User, discord.Member],
+        moderator: Optional[Union[discord.User, discord.Member]],
+        guild: discord.Guild,
+        mute_type: str,
+        reason: Optional[str],
+        duration=None,
+    ):
+        raise NotImplementedError()
diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py
--- a/redbot/cogs/mutes/mutes.py
+++ b/redbot/cogs/mutes/mutes.py
@@ -13,7 +13,7 @@
 from redbot.core.bot import Red
 from redbot.core import commands, checks, i18n, modlog, Config
 from redbot.core.utils import bounded_gather
-from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify
+from redbot.core.utils.chat_formatting import bold, humanize_timedelta, humanize_list, pagify
 from redbot.core.utils.mod import get_audit_reason
 from redbot.core.utils.menus import start_adding_reactions
 from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
@@ -74,6 +74,8 @@ def __init__(self, bot: Red):
             "notification_channel": None,
             "muted_users": {},
             "default_time": 0,
+            "dm": False,
+            "show_mod": False,
         }
         self.config.register_global(force_role_mutes=True)
         # Tbh I would rather force everyone to use role mutes.
@@ -253,6 +255,9 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict):
                 _("Automatic unmute"),
                 until=None,
             )
+            await self._send_dm_notification(
+                member, author, guild, _("Server unmute"), _("Automatic unmute")
+            )
         else:
             chan_id = await self.config.guild(guild).notification_channel()
             notification_channel = guild.get_channel(chan_id)
@@ -360,6 +365,9 @@ async def _auto_channel_unmute_user_multi(
             modlog_reason,
             until=None,
         )
+        await self._send_dm_notification(
+            member, author, guild, _("Server unmute"), _("Automatic unmute")
+        )
         self._channel_mute_events[guild.id].set()
         if any(results):
             reasons = {}
@@ -421,8 +429,10 @@ async def _auto_channel_unmute_user(
             if create_case:
                 if isinstance(channel, discord.VoiceChannel):
                     unmute_type = "vunmute"
+                    notification_title = _("Voice unmute")
                 else:
                     unmute_type = "cunmute"
+                    notification_title = _("Channel unmute")
                 await modlog.create_case(
                     self.bot,
                     channel.guild,
@@ -434,6 +444,9 @@ async def _auto_channel_unmute_user(
                     until=None,
                     channel=channel,
                 )
+                await self._send_dm_notification(
+                    member, author, channel.guild, notification_title, _("Automatic unmute")
+                )
             return None
         else:
             error_msg = _(
@@ -454,6 +467,72 @@ async def _auto_channel_unmute_user(
             else:
                 return (member, channel, success["reason"])
 
+    async def _send_dm_notification(
+        self,
+        user: Union[discord.User, discord.Member],
+        moderator: Optional[Union[discord.User, discord.Member]],
+        guild: discord.Guild,
+        mute_type: str,
+        reason: Optional[str],
+        duration=None,
+    ):
+        if not await self.config.guild(guild).dm():
+            return
+
+        show_mod = await self.config.guild(guild).show_mod()
+        title = bold(mute_type)
+        if duration:
+            duration_str = humanize_timedelta(timedelta=duration)
+            until = datetime.now(timezone.utc) + duration
+            until_str = until.strftime("%Y-%m-%d %H:%M:%S UTC")
+
+        if moderator is None:
+            moderator_str = _("Unknown")
+        else:
+            moderator_str = str(moderator)
+
+        if not reason:
+            reason = _("No reason provided.")
+
+        # okay, this is some poor API to require PrivateChannel here...
+        if await self.bot.embed_requested(await user.create_dm(), user):
+            em = discord.Embed(
+                title=title,
+                description=reason,
+                color=await self.bot.get_embed_color(user),
+            )
+            em.timestamp = datetime.utcnow()
+            if duration:
+                em.add_field(name=_("Until"), value=until_str)
+                em.add_field(name=_("Duration"), value=duration_str)
+            em.add_field(name=_("Guild"), value=guild.name, inline=False)
+            if show_mod:
+                em.add_field(name=_("Moderator"), value=moderator_str)
+            try:
+                await user.send(embed=em)
+            except discord.Forbidden:
+                pass
+        else:
+            message = f"{title}\n>>> "
+            message += reason
+            message += (
+                _("\n**Moderator**: {moderator}").format(moderator=moderator_str)
+                if show_mod
+                else ""
+            )
+            message += (
+                _("\n**Until**: {until}\n**Duration**: {duration}").format(
+                    until=until_str, duration=duration_str
+                )
+                if duration
+                else ""
+            )
+            message += _("\n**Guild**: {guild_name}").format(guild_name=guild.name)
+            try:
+                await user.send(message)
+            except discord.Forbidden:
+                pass
+
     @commands.Cog.listener()
     async def on_member_update(self, before: discord.Member, after: discord.Member):
         """
@@ -491,6 +570,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member):
                 )
                 del self._server_mutes[guild.id][after.id]
                 should_save = True
+                await self._send_dm_notification(
+                    after, None, guild, _("Server unmute"), _("Manually removed mute role")
+                )
         elif mute_role in roles_added:
             # send modlog case for mute and add to cache
             if guild.id not in self._server_mutes:
@@ -512,6 +594,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member):
                     "until": None,
                 }
                 should_save = True
+                await self._send_dm_notification(
+                    after, None, guild, _("Server mute"), _("Manually applied mute role")
+                )
         if should_save:
             await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id])
 
@@ -552,15 +637,27 @@ async def on_guild_channel_update(
                     user_id not in after_perms or any((send_messages, speak))
                 ):
                     user = after.guild.get_member(user_id)
+                    send_dm_notification = True
                     if not user:
+                        send_dm_notification = False
                         user = discord.Object(id=user_id)
                     log.debug(f"{user} - {type(user)}")
                     to_del.append(user_id)
                     log.debug("creating case")
                     if isinstance(after, discord.VoiceChannel):
                         unmute_type = "vunmute"
+                        notification_title = _("Voice unmute")
                     else:
                         unmute_type = "cunmute"
+                        notification_title = _("Channel unmute")
+                    if send_dm_notification:
+                        await self._send_dm_notification(
+                            user,
+                            None,
+                            after.guild,
+                            notification_title,
+                            _("Manually removed channel overwrites"),
+                        )
                     await modlog.create_case(
                         self.bot,
                         after.guild,
@@ -606,6 +703,34 @@ async def muteset(self, ctx: commands.Context):
         """Mute settings."""
         pass
 
+    @muteset.command()
+    @commands.guild_only()
+    async def senddm(self, ctx: commands.Context, true_or_false: bool):
+        """Set whether mute notifications should be sent to users in DMs."""
+        await self.config.guild(ctx.guild).dm.set(true_or_false)
+        if true_or_false:
+            await ctx.send(_("I will now try to send mute notifications to users DMs."))
+        else:
+            await ctx.send(_("Mute notifications will no longer be sent to users DMs."))
+
+    @muteset.command()
+    @commands.guild_only()
+    async def showmoderator(self, ctx, true_or_false: bool):
+        """Decide whether the name of the moderator muting a user should be included in the DM to that user."""
+        await self.config.guild(ctx.guild).show_mod.set(true_or_false)
+        if true_or_false:
+            await ctx.send(
+                _(
+                    "I will include the name of the moderator who issued the mute when sending a DM to a user."
+                )
+            )
+        else:
+            await ctx.send(
+                _(
+                    "I will not include the name of the moderator who issued the mute when sending a DM to a user."
+                )
+            )
+
     @muteset.command(name="forcerole")
     @commands.is_owner()
     async def force_role_mutes(self, ctx: commands.Context, force_role_mutes: bool):
@@ -630,11 +755,17 @@ async def show_mutes_settings(self, ctx: commands.Context):
         notification_channel = ctx.guild.get_channel(data["notification_channel"])
         default_time = timedelta(seconds=data["default_time"])
         msg = _(
-            "Mute Role: {role}\nNotification Channel: {channel}\n" "Default Time: {time}"
+            "Mute Role: {role}\n"
+            "Notification Channel: {channel}\n"
+            "Default Time: {time}\n"
+            "Send DM: {dm}\n"
+            "Show moderator: {show_mod}"
         ).format(
             role=mute_role.mention if mute_role else _("None"),
             channel=notification_channel.mention if notification_channel else _("None"),
             time=humanize_timedelta(timedelta=default_time) if default_time else _("None"),
+            dm=data["dm"],
+            show_mod=data["show_mod"],
         )
         await ctx.maybe_send_embed(msg)
 
@@ -994,6 +1125,9 @@ async def mute(
                         until=until,
                         channel=None,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Server mute"), reason, duration
+                    )
                 else:
                     issue_list.append(success)
         if success_list:
@@ -1138,6 +1272,9 @@ async def channel_mute(
                         until=until,
                         channel=channel,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Channel mute"), reason, duration
+                    )
                     async with self.config.member(user).perms_cache() as cache:
                         cache[channel.id] = success["old_overs"]
                 else:
@@ -1204,6 +1341,9 @@ async def unmute(
                         reason,
                         until=None,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Server unmute"), reason
+                    )
                 else:
                     issue_list.append(success)
         self._channel_mute_events[guild.id].set()
@@ -1268,6 +1408,9 @@ async def unmute_channel(
                         until=None,
                         channel=channel,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Channel unmute"), reason
+                    )
                 else:
                     issue_list.append((user, success["reason"]))
         if success_list:
diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py
--- a/redbot/cogs/mutes/voicemutes.py
+++ b/redbot/cogs/mutes/voicemutes.py
@@ -1,10 +1,11 @@
-from typing import Optional, Tuple
+from typing import Optional, Tuple, Union
 from datetime import timezone, timedelta, datetime
 from .abc import MixinMeta
 
 import discord
 from redbot.core import commands, checks, i18n, modlog
 from redbot.core.utils.chat_formatting import (
+    bold,
     humanize_timedelta,
     humanize_list,
     pagify,
@@ -143,6 +144,9 @@ async def voice_mute(
                         until=until,
                         channel=channel,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Voice mute"), reason, duration
+                    )
                     async with self.config.member(user).perms_cache() as cache:
                         cache[channel.id] = success["old_overs"]
                 else:
@@ -216,6 +220,9 @@ async def unmute_voice(
                         until=None,
                         channel=channel,
                     )
+                    await self._send_dm_notification(
+                        user, author, guild, _("Voice unmute"), reason
+                    )
                 else:
                     issue_list.append((user, success["reason"]))
         if success_list:


Test patch: