diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/exts/filters/filtering.py | 63 |
1 files changed, 40 insertions, 23 deletions
diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 3527bf8bb..6f1374cf4 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,7 +2,7 @@ import asyncio import logging import re from datetime import datetime, timedelta -from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Union +from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import dateutil import discord.errors @@ -137,6 +137,10 @@ class Filtering(Cog): """Fetch items from the filter_list_cache.""" return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"].keys() + def _get_filterlist_value(self, list_type: str, value: Any, *, allowed: bool) -> dict: + """Fetch one specific value from filter_list_cache.""" + return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"][value] + @staticmethod def _expand_spoilers(text: str) -> str: """Return a string containing all interpretations of a spoilered message.""" @@ -236,7 +240,7 @@ class Filtering(Cog): # We also do not need to worry about filters that take the full message, # since all we have is an arbitrary string. if _filter["enabled"] and _filter["content_only"]: - match = await _filter["function"](result) + match, reason = await _filter["function"](result) if match: # If this is a filter (not a watchlist), we set the variable so we know @@ -245,7 +249,7 @@ class Filtering(Cog): filter_triggered = True stats = self._add_stats(filter_name, match, result) - await self._send_log(filter_name, _filter, msg, stats, is_eval=True) + await self._send_log(filter_name, _filter, msg, stats, reason, is_eval=True) break # We don't want multiple filters to trigger @@ -267,9 +271,9 @@ class Filtering(Cog): # Does the filter only need the message content or the full message? if _filter["content_only"]: - match = await _filter["function"](msg.content) + match, reason = await _filter["function"](msg.content) else: - match = await _filter["function"](msg) + match, reason = await _filter["function"](msg) if match: is_private = msg.channel.type is discord.ChannelType.private @@ -316,7 +320,7 @@ class Filtering(Cog): log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}") stats = self._add_stats(filter_name, match, msg.content) - await self._send_log(filter_name, _filter, msg, stats) + await self._send_log(filter_name, _filter, msg, stats, reason) break # We don't want multiple filters to trigger @@ -326,6 +330,7 @@ class Filtering(Cog): _filter: Dict[str, Any], msg: discord.Message, stats: Stats, + reason: Optional[str] = None, *, is_eval: bool = False, ) -> None: @@ -339,6 +344,7 @@ class Filtering(Cog): ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) eval_msg = "using !eval " if is_eval else "" + footer = f"Entry comment: {reason}" if reason else None message = ( f"The {filter_name} {_filter['type']} was triggered by {format_user(msg.author)} " f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n" @@ -357,6 +363,7 @@ class Filtering(Cog): channel_id=Channels.mod_alerts, ping_everyone=ping_everyone, additional_embeds=stats.additional_embeds, + footer=footer, ) def _add_stats(self, name: str, match: FilterMatch, content: str) -> Stats: @@ -381,9 +388,11 @@ class Filtering(Cog): if name == "filter_invites" and match is not True: additional_embeds = [] for _, data in match.items(): + reason = f"\n**Entry comment:**\n{data['reason']}" if data.get('reason') else "" embed = discord.Embed(description=( f"**Members:**\n{data['members']}\n" f"**Active:**\n{data['active']}" + f"{reason}" )) embed.set_author(name=data["name"]) embed.set_thumbnail(url=data["icon"]) @@ -411,7 +420,7 @@ class Filtering(Cog): and not msg.author.bot # Author not a bot ) - async def _has_watch_regex_match(self, text: str) -> Union[bool, re.Match]: + async def _has_watch_regex_match(self, text: str) -> Tuple[Union[bool, re.Match], Optional[str]]: """ Return True if `text` matches any regex from `word_watchlist` or `token_watchlist` configs. @@ -429,9 +438,11 @@ class Filtering(Cog): for pattern in watchlist_patterns: match = re.search(pattern, text, flags=re.IGNORECASE) if match: - return match + return match, self._get_filterlist_value('filter_token', pattern, allowed=False)['comment'] + + return False, None - async def _has_urls(self, text: str) -> bool: + async def _has_urls(self, text: str) -> Tuple[bool, Optional[str]]: """Returns True if the text contains one of the blacklisted URLs from the config file.""" if not URL_RE.search(text): return False @@ -441,20 +452,21 @@ class Filtering(Cog): for url in domain_blacklist: if url.lower() in text: - return True + return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] - return False + return False, None @staticmethod - async def _has_zalgo(text: str) -> bool: + async def _has_zalgo(text: str) -> Tuple[bool, None]: """ Returns True if the text contains zalgo characters. Zalgo range is \u0300 – \u036F and \u0489. + Return None as second value for compability with other filters. """ - return bool(ZALGO_RE.search(text)) + return bool(ZALGO_RE.search(text)), None - async def _has_invites(self, text: str) -> Union[dict, bool]: + async def _has_invites(self, text: str) -> Tuple[Union[dict, bool], None]: """ Checks if there's any invites in the text content that aren't in the guild whitelist. @@ -500,6 +512,10 @@ class Filtering(Cog): ) if invite_not_allowed: + reason = None + if guild_id in guild_invite_blacklist: + reason = self._get_filterlist_value("guild_invite", guild_id, allowed=False)["comment"] + guild_icon_hash = guild["icon"] guild_icon = ( "https://cdn.discordapp.com/icons/" @@ -511,13 +527,14 @@ class Filtering(Cog): "id": guild['id'], "icon": guild_icon, "members": response["approximate_member_count"], - "active": response["approximate_presence_count"] + "active": response["approximate_presence_count"], + "reason": reason } - return invite_data if invite_data else False + return invite_data if invite_data else False, None @staticmethod - async def _has_rich_embed(msg: Message) -> Union[bool, List[discord.Embed]]: + async def _has_rich_embed(msg: Message) -> Tuple[Union[bool, List[discord.Embed]], None]: """Determines if `msg` contains any rich embeds not auto-generated from a URL.""" if msg.embeds: for embed in msg.embeds: @@ -526,24 +543,24 @@ class Filtering(Cog): if not embed.url or embed.url not in urls: # If `embed.url` does not exist or if `embed.url` is not part of the content # of the message, it's unlikely to be an auto-generated embed by Discord. - return msg.embeds + return msg.embeds, None else: log.trace( "Found a rich embed sent by a regular user account, " "but it was likely just an automatic URL embed." ) - return False - return False + return False, None + return False, None @staticmethod - async def _has_everyone_ping(text: str) -> bool: + async def _has_everyone_ping(text: str) -> Tuple[bool, None]: """Determines if `msg` contains an @everyone or @here ping outside of a codeblock.""" # First pass to avoid running re.sub on every message if not EVERYONE_PING_RE.search(text): - return False + return False, None content_without_codeblocks = CODE_BLOCK_RE.sub("", text) - return bool(EVERYONE_PING_RE.search(content_without_codeblocks)) + return bool(EVERYONE_PING_RE.search(content_without_codeblocks)), None async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel) -> None: """ |