diff options
-rw-r--r-- | bot/__init__.py | 1 | ||||
-rw-r--r-- | bot/__main__.py | 1 | ||||
-rw-r--r-- | bot/cogs/filtering.py | 232 | ||||
-rw-r--r-- | bot/constants.py | 29 | ||||
-rw-r--r-- | config-default.yml | 142 |
5 files changed, 367 insertions, 38 deletions
diff --git a/bot/__init__.py b/bot/__init__.py index a87d31541..df168cba4 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -93,4 +93,5 @@ logging.getLogger("discord.client").setLevel(logging.ERROR) logging.getLogger("discord.gateway").setLevel(logging.ERROR) logging.getLogger("discord.state").setLevel(logging.ERROR) logging.getLogger("discord.http").setLevel(logging.ERROR) +logging.getLogger("PIL.PngImagePlugin").setLevel(logging.ERROR) logging.getLogger("websockets.protocol").setLevel(logging.ERROR) diff --git a/bot/__main__.py b/bot/__main__.py index 6790da79e..c5c8b8909 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -40,6 +40,7 @@ else: bot.load_extension("bot.cogs.logging") bot.load_extension("bot.cogs.security") bot.load_extension("bot.cogs.events") +bot.load_extension("bot.cogs.filtering") # Commands, etc bot.load_extension("bot.cogs.antispam") diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py new file mode 100644 index 000000000..89735e57c --- /dev/null +++ b/bot/cogs/filtering.py @@ -0,0 +1,232 @@ +import logging +import re + +from discord import Colour, Member, Message +from discord.ext.commands import Bot + +from bot.cogs.modlog import ModLog +from bot.constants import ( + Channels, Colours, DEBUG_MODE, + Filter, Icons +) + +log = logging.getLogger(__name__) + +INVITE_RE = ( + r"(?:discord(?:[\.,]|dot)gg|" # Could be discord.gg/ + r"discord(?:[\.,]|dot)com(?:\/|slash)invite|" # or discord.com/invite/ + r"discordapp(?:[\.,]|dot)com(?:\/|slash)invite|" # or discordapp.com/invite/ + r"discord(?:[\.,]|dot)me|" # or discord.me + r"discord(?:[\.,]|dot)io" # or discord.io. + r")(?:[\/]|slash)" # / or 'slash' + r"([a-zA-Z0-9]+)" # the invite code itself +) + +URL_RE = "(https?://[^\s]+)" +ZALGO_RE = r"[\u0300-\u036F\u0489]" + + +class Filtering: + """ + Filtering out invites, blacklisting domains, + and warning us of certain regular expressions + """ + + def __init__(self, bot: Bot): + self.bot = bot + + self.filters = { + "filter_zalgo": { + "enabled": Filter.filter_zalgo, + "function": self._has_zalgo, + "type": "filter" + }, + "filter_invites": { + "enabled": Filter.filter_invites, + "function": self._has_invites, + "type": "filter" + }, + "filter_domains": { + "enabled": Filter.filter_domains, + "function": self._has_urls, + "type": "filter" + }, + "watch_words": { + "enabled": Filter.watch_words, + "function": self._has_watchlist_words, + "type": "watchlist" + }, + "watch_tokens": { + "enabled": Filter.watch_tokens, + "function": self._has_watchlist_tokens, + "type": "watchlist" + }, + } + + @property + def mod_log(self) -> ModLog: + return self.bot.get_cog("ModLog") + + async def on_message(self, msg: Message): + await self._filter_message(msg) + + async def on_message_edit(self, _: Message, after: Message): + await self._filter_message(after) + + async def _filter_message(self, msg: Message): + """ + Whenever a message is sent or edited, + run it through our filters to see if it + violates any of our rules, and then respond + accordingly. + """ + + # Should we filter this message? + role_whitelisted = False + + if type(msg.author) is Member: # Only Member has roles, not User. + for role in msg.author.roles: + if role.id in Filter.role_whitelist: + role_whitelisted = True + + filter_message = ( + msg.channel.id not in Filter.channel_whitelist # Channel not in whitelist + and not role_whitelisted # Role not in whitelist + and not msg.author.bot # Author not a bot + ) + + # If we're running the bot locally, ignore role whitelist and only listen to #dev-test + if DEBUG_MODE: + filter_message = not msg.author.bot and msg.channel.id == Channels.devtest + + # 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) + + if triggered: + message = ( + f"The {filter_name} {_filter['type']} was triggered " + f"by **{msg.author.name}#{msg.author.discriminator}** in " + f"<#{msg.channel.id}> with the following message:\n\n" + f"{msg.content}" + ) + + log.debug(message) + log.debug(Channels.mod_alerts) + + # Send pretty mod log embed to mod-alerts + await self.mod_log.send_log_message( + icon_url=Icons.filtering, + colour=Colour(Colours.soft_red), + title=f"{_filter['type'].title()} triggered!", + text=message, + thumbnail=msg.author.avatar_url_as(static_format="png"), + channel_id=Channels.mod_alerts, + ping_everyone=Filter.ping_everyone, + ) + + # If this is a filter (not a watchlist), we should delete the message. + if _filter["type"] == "filter": + await msg.delete() + + break # We don't want multiple filters to trigger + + @staticmethod + async def _has_watchlist_words(text: str) -> bool: + """ + 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 expression in Filter.word_watchlist: + if re.search(fr"\b{expression}\b", text, re.IGNORECASE): + return True + + return False + + @staticmethod + async def _has_watchlist_tokens(text: str) -> bool: + """ + 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 expression in Filter.token_watchlist: + if re.search(fr"{expression}", text, re.IGNORECASE): + return True + + return False + + @staticmethod + async def _has_urls(text: str) -> bool: + """ + Returns True if the text contains one of + the blacklisted URLs from the config file. + """ + + if not re.search(URL_RE, text, re.IGNORECASE): + return False + + text = text.lower() + + for url in Filter.domain_blacklist: + if url.lower() in text: + return True + + return False + + @staticmethod + async def _has_zalgo(text: str) -> bool: + """ + Returns True if the text contains zalgo characters. + + Zalgo range is \u0300 – \u036F and \u0489. + """ + + return bool(re.search(ZALGO_RE, text)) + + @staticmethod + async def _has_invites(text: str) -> bool: + """ + Returns True if the text contains an invite which + is not on the guild_invite_whitelist in config.yml. + + Also catches a lot of common ways to try to cheat the system. + """ + + # Remove spaces to prevent cases like + # d i s c o r d . c o m / i n v i t e / p y t h o n + text = text.replace(" ", "") + + # Remove backslashes to prevent escape character aroundfuckery like + # discord\.gg/gdudes-pony-farm + text = text.replace("\\", "") + + invites = re.findall(INVITE_RE, text, re.IGNORECASE) + for invite in invites: + + filter_invite = ( + invite not in Filter.guild_invite_whitelist + and invite.lower() not in Filter.vanity_url_whitelist + ) + + if filter_invite: + return True + return False + + +def setup(bot: Bot): + bot.add_cog(Filtering(bot)) + log.info("Cog loaded: Filtering") diff --git a/bot/constants.py b/bot/constants.py index a980de15d..ab426a7ab 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -191,6 +191,26 @@ class Bot(metaclass=YAMLGetter): token: str +class Filter(metaclass=YAMLGetter): + section = "filter" + + filter_zalgo: bool + filter_invites: bool + filter_domains: bool + watch_words: bool + watch_tokens: bool + + ping_everyone: bool + guild_invite_whitelist: List[str] + vanity_url_whitelist: List[str] + domain_blacklist: List[str] + word_watchlist: List[str] + token_watchlist: List[str] + + channel_whitelist: List[int] + role_whitelist: List[int] + + class Cooldowns(metaclass=YAMLGetter): section = "bot" subsection = "cooldowns" @@ -237,10 +257,12 @@ class Icons(metaclass=YAMLGetter): crown_green: str crown_red: str - defcon_denied: str # noqa: E704 + defcon_denied: str # noqa: E704 defcon_disabled: str # noqa: E704 - defcon_enabled: str # noqa: E704 - defcon_updated: str # noqa: E704 + defcon_enabled: str # noqa: E704 + defcon_updated: str # noqa: E704 + + filtering: str guild_update: str @@ -287,6 +309,7 @@ class Channels(metaclass=YAMLGetter): help_5: int helpers: int message_log: int + mod_alerts: int modlog: int off_topic_1: int off_topic_2: int diff --git a/config-default.yml b/config-default.yml index dfc9fe306..0519244b0 100644 --- a/config-default.yml +++ b/config-default.yml @@ -45,6 +45,8 @@ style: defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" + filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" + guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" @@ -70,45 +72,115 @@ guild: id: 267624335836053506 channels: - admins: &ADMINS 365960823622991872 - announcements: 354619224620138496 - big_brother_logs: 468507907357409333 - bot: 267659945086812160 - checkpoint_test: 422077681434099723 - devalerts: 460181980097675264 - devlog: 409308876241108992 - devtest: 414574275865870337 - help_0: 303906576991780866 - help_1: 303906556754395136 - help_2: 303906514266226689 - help_3: 439702951246692352 - help_4: 451312046647148554 - help_5: 454941769734422538 - helpers: 385474242440986624 - message_log: &MESSAGE_LOG 467752170159079424 - mod_alerts: 473092793431097354 - modlog: &MODLOG 282638479504965634 - off_topic_0: 291284109232308226 - off_topic_1: 463035241142026251 - off_topic_2: 463035268514185226 - python: 267624335836053506 - verification: 352442727016693763 + admins: &ADMINS 365960823622991872 + announcements: 354619224620138496 + big_brother_logs: &BBLOGS 468507907357409333 + bot: 267659945086812160 + checkpoint_test: 422077681434099723 + devalerts: 460181980097675264 + devlog: &DEVLOG 409308876241108992 + devtest: &DEVTEST 414574275865870337 + help_0: 303906576991780866 + help_1: 303906556754395136 + help_2: 303906514266226689 + help_3: 439702951246692352 + help_4: 451312046647148554 + help_5: 454941769734422538 + helpers: 385474242440986624 + message_log: &MESSAGE_LOG 467752170159079424 + mod_alerts: 473092532147060736 + modlog: &MODLOG 282638479504965634 + off_topic_0: 291284109232308226 + off_topic_1: 463035241142026251 + off_topic_2: 463035268514185226 + python: 267624335836053506 + staff_lounge: &STAFF_LOUNGE 464905259261755392 + verification: 352442727016693763 ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] roles: - admin: 267628507062992896 - announcements: 463658397560995840 - champion: 430492892331769857 - contributor: 295488872404484098 - developer: 352427296948486144 - devops: 409416496733880320 - jammer: 423054537079783434 - moderator: 267629731250176001 - owner: 267627879762755584 - verified: 352427296948486144 - helpers: 267630620367257601 - muted: &MUTED_ROLE 277914926603829249 + admin: &ADMIN_ROLE 267628507062992896 + announcements: 463658397560995840 + champion: 430492892331769857 + contributor: 295488872404484098 + developer: 352427296948486144 + devops: &DEVOPS_ROLE 409416496733880320 + jammer: 423054537079783434 + moderator: &MOD_ROLE 267629731250176001 + muted: &MUTED_ROLE 277914926603829249 + owner: &OWNER_ROLE 267627879762755584 + verified: 352427296948486144 + helpers: 267630620367257601 + + +filter: + + # What do we filter? + filter_zalgo: true + filter_invites: true + filter_domains: true + watch_words: true + watch_tokens: true + + # Filter configuration + ping_everyone: true # Ping @everyone when we send a mod-alert? + + guild_invite_whitelist: + - vywQPxd # Code Monkeys + - kWJYurV # Functional Programming + - 010z0Kw1A9ql5c1Qe # Programming: Meme Edition + - XBGetGp # STEM + + vanity_url_whitelist: + - python # Python Discord + + domain_blacklist: + - pornhub.com + - liveleak.com + + word_watchlist: + - goo+ks* + - ky+s+ + - gh?[ae]+y+s* + - ki+ke+s* + - beane?r*s* + - coo+ns* + - nig+lets* + - slant-eyes* + - towe?l-?head+s* + - chi*n+k+s* + - spick*s* + - kill* +(?:yo)?urself+ + - jew+s* + - suicide + - rape + - (?:re+)?tar+d+(?:ed)? + - cunts* + + token_watchlist: + - fa+g+s* + - 卐 + - 卍 + - cuck + - nigg+(?:e*r+|a+h+?|u+h+)s? + - fag+o+t+s* + + # Censor doesn't apply to these + channel_whitelist: + - *ADMINS + - *MODLOG + - *MESSAGE_LOG + - *DEVLOG + - *BBLOGS + - *STAFF_LOUNGE + - *DEVTEST + + role_whitelist: + - *ADMIN_ROLE + - *MOD_ROLE + - *OWNER_ROLE + - *DEVOPS_ROLE keys: |