aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar kwzrd <[email protected]>2020-06-12 21:09:23 +0200
committerGravatar kwzrd <[email protected]>2020-06-12 21:09:23 +0200
commit20ae5c0dd8f5b6211c43e8f06138ee1acb456b62 (patch)
tree05c60deee5ec0f7860fdbffb86edd9655ab94358
parentIncidents: extend documentation (diff)
parentReplace mention of Flask with Django (diff)
Merge branch 'origin/master' into kwzrd/incidents
-rw-r--r--bot/cogs/filtering.py61
-rw-r--r--bot/cogs/site.py2
-rw-r--r--bot/utils/redis_cache.py11
-rw-r--r--tests/bot/utils/test_redis_cache.py10
4 files changed, 62 insertions, 22 deletions
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 1d9fddb12..4ebc831e1 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -1,6 +1,8 @@
+import asyncio
import logging
import re
-from typing import Optional, Union
+from datetime import datetime, timedelta
+from typing import List, Optional, Union
import discord.errors
from dateutil.relativedelta import relativedelta
@@ -14,6 +16,7 @@ from bot.constants import (
Channels, Colours,
Filter, Icons, URLs
)
+from bot.utils.redis_cache import RedisCache
log = logging.getLogger(__name__)
@@ -40,6 +43,8 @@ TOKEN_WATCHLIST_PATTERNS = [
]
WATCHLIST_PATTERNS = WORD_WATCHLIST_PATTERNS + TOKEN_WATCHLIST_PATTERNS
+DAYS_BETWEEN_ALERTS = 3
+
def expand_spoilers(text: str) -> str:
"""Return a string containing all interpretations of a spoilered message."""
@@ -52,8 +57,12 @@ def expand_spoilers(text: str) -> str:
class Filtering(Cog):
"""Filtering out invites, blacklisting domains, and warning us of certain regular expressions."""
+ # Redis cache mapping a user ID to the last timestamp a bad nickname alert was sent
+ name_alerts = RedisCache()
+
def __init__(self, bot: Bot):
self.bot = bot
+ self.name_lock = asyncio.Lock()
staff_mistake_str = "If you believe this was a mistake, please let staff know!"
self.filters = {
@@ -112,6 +121,7 @@ class Filtering(Cog):
async def on_message(self, msg: Message) -> None:
"""Invoke message filter for new messages."""
await self._filter_message(msg)
+ await self.check_bad_words_in_name(msg.author)
@Cog.listener()
async def on_message_edit(self, before: Message, after: Message) -> None:
@@ -126,6 +136,55 @@ class Filtering(Cog):
delta = relativedelta(after.edited_at, before.edited_at).microseconds
await self._filter_message(after, delta)
+ @staticmethod
+ def get_name_matches(name: str) -> List[re.Match]:
+ """Check bad words from passed string (name). Return list of matches."""
+ matches = []
+ for pattern in WATCHLIST_PATTERNS:
+ if match := pattern.search(name):
+ matches.append(match)
+ return matches
+
+ async def check_send_alert(self, member: Member) -> bool:
+ """When there is less than 3 days after last alert, return `False`, otherwise `True`."""
+ if last_alert := await self.name_alerts.get(member.id):
+ last_alert = datetime.utcfromtimestamp(last_alert)
+ if datetime.utcnow() - timedelta(days=DAYS_BETWEEN_ALERTS) < last_alert:
+ log.trace(f"Last alert was too recent for {member}'s nickname.")
+ return False
+
+ return True
+
+ async def check_bad_words_in_name(self, member: Member) -> None:
+ """Send a mod alert every 3 days if a username still matches a watchlist pattern."""
+ # Use lock to avoid race conditions
+ async with self.name_lock:
+ # Check whether the users display name contains any words in our blacklist
+ matches = self.get_name_matches(member.display_name)
+
+ if not matches or not await self.check_send_alert(member):
+ return
+
+ log.info(f"Sending bad nickname alert for '{member.display_name}' ({member.id}).")
+
+ log_string = (
+ f"**User:** {member.mention} (`{member.id}`)\n"
+ f"**Display Name:** {member.display_name}\n"
+ f"**Bad Matches:** {', '.join(match.group() for match in matches)}"
+ )
+
+ await self.mod_log.send_log_message(
+ icon_url=Icons.token_removed,
+ colour=Colours.soft_red,
+ title="Username filtering alert",
+ text=log_string,
+ channel_id=Channels.mod_alerts,
+ thumbnail=member.avatar_url
+ )
+
+ # Update time when alert sent
+ await self.name_alerts.set(member.id, datetime.utcnow().timestamp())
+
async def _filter_message(self, msg: Message, delta: Optional[int] = None) -> None:
"""Filter the input message to see if it violates any of our rules, and then respond accordingly."""
# Should we filter this message?
diff --git a/bot/cogs/site.py b/bot/cogs/site.py
index e61cd5003..ac29daa1d 100644
--- a/bot/cogs/site.py
+++ b/bot/cogs/site.py
@@ -33,7 +33,7 @@ class Site(Cog):
embed.colour = Colour.blurple()
embed.description = (
f"[Our official website]({url}) is an open-source community project "
- "created with Python and Flask. It contains information about the server "
+ "created with Python and Django. It contains information about the server "
"itself, lets you sign up for upcoming events, has its own wiki, contains "
"a list of valuable learning resources, and much more."
)
diff --git a/bot/utils/redis_cache.py b/bot/utils/redis_cache.py
index de80cee84..354e987b9 100644
--- a/bot/utils/redis_cache.py
+++ b/bot/utils/redis_cache.py
@@ -100,16 +100,7 @@ class RedisCache:
def _set_namespace(self, namespace: str) -> None:
"""Try to set the namespace, but do not permit collisions."""
- # We need a unique namespace, to prevent collisions. This loop
- # will try appending underscores to the end of the namespace until
- # it finds one that is unique.
- #
- # For example, if `john` and `john_` are both taken, the namespace will
- # be `john__` at the end of this loop.
- while namespace in self._namespaces:
- namespace += "_"
-
- log.trace(f"RedisCache setting namespace to {self._namespace}")
+ log.trace(f"RedisCache setting namespace to {namespace}")
self._namespaces.append(namespace)
self._namespace = namespace
diff --git a/tests/bot/utils/test_redis_cache.py b/tests/bot/utils/test_redis_cache.py
index 8c1a40640..e5d6e4078 100644
--- a/tests/bot/utils/test_redis_cache.py
+++ b/tests/bot/utils/test_redis_cache.py
@@ -44,16 +44,6 @@ class RedisCacheTests(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(RuntimeError):
await bad_cache.set("test", "me_up_deadman")
- def test_namespace_collision(self):
- """Test that we prevent colliding namespaces."""
- bob_cache_1 = RedisCache()
- bob_cache_1._set_namespace("BobRoss")
- self.assertEqual(bob_cache_1._namespace, "BobRoss")
-
- bob_cache_2 = RedisCache()
- bob_cache_2._set_namespace("BobRoss")
- self.assertEqual(bob_cache_2._namespace, "BobRoss_")
-
async def test_set_get_item(self):
"""Test that users can set and get items from the RedisDict."""
test_cases = (