Repository: python-discord/bot

Issue:
Display word triggered by watchlist in #mod-alerts
## Abstract

In `#mod-alerts` when a word is triggered in the `watchlist`, display the word that was triggered in addition to the original message content.

## Rationale

Without knowing the trigger word, reading the original content of the message to figure out what it was is unnecessarily difficult. In order to see if moderation action is required, we have to first find what the word was and where it is in the message, before finally being able to determine what context it was used in.

## Motivation

* Often a trigger will come from a very long message, making it very time consuming to find the instance of the word.
* If the word was triggered in a codeblock, the word is often a substring of some other larger unrelated word, making finding the trigger incredibly difficult

## Specification

Alongside the meta info of who sent the message and in what channel, show what the trigger word was, and if possible, a short snippet of the part of the message that caused the trigger.

**Example:**

```
The watch_tokens watchlist was triggered by lemon#0001 (95872159741644800)
in #admin-spam

Trigger word: "nigg"
Match: "...word niggardly is c..."

Original message:

The word niggardly is controversial due to its phonetic similarity
to the racial slur, but etymologically they're completely unrelated!
```


Discussion:

Resolving patch:
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -154,11 +154,11 @@ async def _filter_message(self, msg: Message, delta: Optional[int] = None) -> No
 
                     # Does the filter only need the message content or the full message?
                     if _filter["content_only"]:
-                        triggered = await _filter["function"](msg.content)
+                        match = await _filter["function"](msg.content)
                     else:
-                        triggered = await _filter["function"](msg)
+                        match = await _filter["function"](msg)
 
-                    if triggered:
+                    if match:
                         # If this is a filter (not a watchlist), we should delete the message.
                         if _filter["type"] == "filter":
                             try:
@@ -184,12 +184,23 @@ async def _filter_message(self, msg: Message, delta: Optional[int] = None) -> No
                         else:
                             channel_str = f"in {msg.channel.mention}"
 
+                        # Word and match stats for watch_words and watch_tokens
+                        if filter_name in ("watch_words", "watch_tokens"):
+                            surroundings = match.string[max(match.start() - 10, 0): match.end() + 10]
+                            message_content = (
+                                f"**Match:** '{match[0]}'\n"
+                                f"**Location:** '...{surroundings}...'\n"
+                                f"\n**Original Message:**\n{msg.content}"
+                            )
+                        else:  # Use content of discord Message
+                            message_content = msg.content
+
                         message = (
                             f"The {filter_name} {_filter['type']} was triggered "
                             f"by **{msg.author}** "
                             f"(`{msg.author.id}`) {channel_str} with [the "
                             f"following message]({msg.jump_url}):\n\n"
-                            f"{msg.content}"
+                            f"{message_content}"
                         )
 
                         log.debug(message)
@@ -199,7 +210,7 @@ async def _filter_message(self, msg: Message, delta: Optional[int] = None) -> No
 
                         if filter_name == "filter_invites":
                             additional_embeds = []
-                            for invite, data in triggered.items():
+                            for invite, data in match.items():
                                 embed = discord.Embed(description=(
                                     f"**Members:**\n{data['members']}\n"
                                     f"**Active:**\n{data['active']}"
@@ -230,31 +241,33 @@ async def _filter_message(self, msg: Message, delta: Optional[int] = None) -> No
                         break  # We don't want multiple filters to trigger
 
     @staticmethod
-    async def _has_watchlist_words(text: str) -> bool:
+    async def _has_watchlist_words(text: str) -> Union[bool, re.Match]:
         """
         Returns True if the text contains one of the regular expressions from the word_watchlist in our filter config.
 
         Only matches words with boundaries before and after the expression.
         """
         for regex_pattern in WORD_WATCHLIST_PATTERNS:
-            if regex_pattern.search(text):
-                return True
+            match = regex_pattern.search(text)
+            if match:
+                return match  # match objects always have a boolean value of True
 
         return False
 
     @staticmethod
-    async def _has_watchlist_tokens(text: str) -> bool:
+    async def _has_watchlist_tokens(text: str) -> Union[bool, re.Match]:
         """
         Returns True if the text contains one of the regular expressions from the token_watchlist in our filter config.
 
         This will match the expression even if it does not have boundaries before and after.
         """
         for regex_pattern in TOKEN_WATCHLIST_PATTERNS:
-            if regex_pattern.search(text):
+            match = regex_pattern.search(text)
+            if match:
 
                 # Make sure it's not a URL
                 if not URL_RE.search(text):
-                    return True
+                    return match  # match objects always have a boolean value of True
 
         return False
 


Test patch: