aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/__init__.py1
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/filtering.py232
-rw-r--r--bot/constants.py29
-rw-r--r--config-default.yml142
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: