Repository: python-discord/bot

Issue:
Add Embed detection for user messages for posting in mod alerts.
Embeds can only be legitimately posted by webhooks and bot accounts.

If a user account sends an embed, either they're using scripts on their own account (selfbot) or they're using an unofficial client or tools that enable posting the embed data. Both of these are against Discord TOS, and as such require us to give notice to the user and a reminder of the rules.

Thankfully, it's really easy to implement this check, so would be suitable as a quick addition or a first contribution.


Discussion:
I'm guessing the best place for this to fit would be in the filters cog?
I think so, too, since messages are checked there anyway.

I can look into this tomorrow, if you weren't already, @sco1. I think this is another issue that will help me get comfortable with the bot.
Have at it, just note that #251 makes some changes to the filters cog.
Thanks, I was just looking at it.
Is this issue still open, coz I am looking forward to contributing.
The issue is open, as it's not resolved. However, it's already assigned to @SebastiaanZ so he's working on it currently. 

Within issues, you can check if they've been assigned already to see if someone has started working on it in the `Assignees` section on the right side of the Issue page.

Resolving patch:
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -45,6 +45,7 @@ def __init__(self, bot: Bot):
                 "enabled": Filter.filter_zalgo,
                 "function": self._has_zalgo,
                 "type": "filter",
+                "content_only": True,
                 "user_notification": Filter.notify_user_zalgo,
                 "notification_msg": (
                     "Your post has been removed for abusing Unicode character rendering (aka Zalgo text). "
@@ -55,6 +56,7 @@ def __init__(self, bot: Bot):
                 "enabled": Filter.filter_invites,
                 "function": self._has_invites,
                 "type": "filter",
+                "content_only": True,
                 "user_notification": Filter.notify_user_invites,
                 "notification_msg": (
                     f"Per Rule 10, your invite link has been removed. {_staff_mistake_str}\n\n"
@@ -65,20 +67,36 @@ def __init__(self, bot: Bot):
                 "enabled": Filter.filter_domains,
                 "function": self._has_urls,
                 "type": "filter",
+                "content_only": True,
                 "user_notification": Filter.notify_user_domains,
                 "notification_msg": (
                     f"Your URL has been removed because it matched a blacklisted domain. {_staff_mistake_str}"
                 )
             },
+            "filter_rich_embeds": {
+                "enabled": Filter.filter_rich_embeds,
+                "function": self._has_rich_embed,
+                "type": "filter",
+                "content_only": False,
+                "user_notification": Filter.notify_user_rich_embeds,
+                "notification_msg": (
+                    "Your post has been removed because it contained a rich embed. "
+                    "This indicates that you're either using an unofficial discord client or are using a self-bot, "
+                    f"both of which violate Discord's Terms of Service. {_staff_mistake_str}\n\n"
+                    "Please don't use a self-bot or an unofficial Discord client on our server."
+                )
+            },
             "watch_words": {
                 "enabled": Filter.watch_words,
                 "function": self._has_watchlist_words,
                 "type": "watchlist",
+                "content_only": True,
             },
             "watch_tokens": {
                 "enabled": Filter.watch_tokens,
                 "function": self._has_watchlist_tokens,
                 "type": "watchlist",
+                "content_only": True,
             },
         }
 
@@ -121,12 +139,35 @@ async def _filter_message(self, msg: Message):
         # If none of the above, we can start filtering.
         if filter_message:
             for filter_name, _filter in self.filters.items():
-
                 # Is this specific filter enabled in the config?
                 if _filter["enabled"]:
-                    triggered = await _filter["function"](msg.content)
+                    # Does the filter only need the message content or the full message?
+                    if _filter["content_only"]:
+                        triggered = await _filter["function"](msg.content)
+                    else:
+                        triggered = await _filter["function"](msg)
 
                     if triggered:
+                        # If this is a filter (not a watchlist), we should delete the message.
+                        if _filter["type"] == "filter":
+                            try:
+                                # Embeds (can?) trigger both the `on_message` and `on_message_edit`
+                                # event handlers, triggering filtering twice for the same message.
+                                #
+                                # If `on_message`-triggered filtering already deleted the message
+                                # then `on_message_edit`-triggered filtering will raise exception
+                                # since the message no longer exists.
+                                #
+                                # In addition, to avoid sending two notifications to the user, the
+                                # logs, and mod_alert, we return if the message no longer exists.
+                                await msg.delete()
+                            except discord.errors.NotFound:
+                                return
+
+                            # Notify the user if the filter specifies
+                            if _filter["user_notification"]:
+                                await self.notify_member(msg.author, _filter["notification_msg"], msg.channel)
+
                         if isinstance(msg.channel, DMChannel):
                             channel_str = "via DM"
                         else:
@@ -142,6 +183,8 @@ async def _filter_message(self, msg: Message):
 
                         log.debug(message)
 
+                        additional_embeds = msg.embeds if filter_name == "filter_rich_embeds" else None
+
                         # Send pretty mod log embed to mod-alerts
                         await self.mod_log.send_log_message(
                             icon_url=Icons.filtering,
@@ -151,16 +194,9 @@ async def _filter_message(self, msg: Message):
                             thumbnail=msg.author.avatar_url_as(static_format="png"),
                             channel_id=Channels.mod_alerts,
                             ping_everyone=Filter.ping_everyone,
+                            additional_embeds=additional_embeds,
                         )
 
-                        # If this is a filter (not a watchlist), we should delete the message.
-                        if _filter["type"] == "filter":
-                            await msg.delete()
-
-                            # Notify the user if the filter specifies
-                            if _filter["user_notification"]:
-                                await self.notify_member(msg.author, _filter["notification_msg"], msg.channel)
-
                         break  # We don't want multiple filters to trigger
 
     @staticmethod
@@ -272,6 +308,16 @@ async def _has_invites(self, text: str) -> bool:
                 return True
         return False
 
+    @staticmethod
+    async def _has_rich_embed(msg: Message):
+        """
+        Returns True if any of the embeds in the message
+        are of type 'rich', returns False otherwise
+        """
+        if msg.embeds:
+            return any(embed.type == "rich" for embed in msg.embeds)
+        return False
+
     async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel):
         """
         Notify filtered_member about a moderation action with the reason str
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -106,7 +106,7 @@ def ignore(self, event: Event, *items: int):
     async def send_log_message(
             self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str,
             thumbnail: str = None, channel_id: int = Channels.modlog, ping_everyone: bool = False,
-            files: List[File] = None, content: str = None
+            files: List[File] = None, content: str = None, additional_embeds: List[Embed] = None,
     ):
         embed = Embed(description=text)
 
@@ -125,7 +125,14 @@ async def send_log_message(
             else:
                 content = "@everyone"
 
-        await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files)
+        channel = self.bot.get_channel(channel_id)
+
+        await channel.send(content=content, embed=embed, files=files)
+
+        if additional_embeds:
+            await channel.send("With the following embed(s):")
+            for additional_embed in additional_embeds:
+                await channel.send(embed=additional_embed)
 
     async def on_guild_channel_create(self, channel: GUILD_CHANNEL):
         if channel.guild.id != GuildConstant.id:
diff --git a/bot/constants.py b/bot/constants.py
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -201,6 +201,7 @@ class Filter(metaclass=YAMLGetter):
     filter_zalgo: bool
     filter_invites: bool
     filter_domains: bool
+    filter_rich_embeds: bool
     watch_words: bool
     watch_tokens: bool
 
@@ -208,6 +209,7 @@ class Filter(metaclass=YAMLGetter):
     notify_user_zalgo: bool
     notify_user_invites: bool
     notify_user_domains: bool
+    notify_user_rich_embeds: bool
 
     ping_everyone: bool
     guild_invite_whitelist: List[int]


Test patch: