aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile1
-rw-r--r--bot/cogs/duck_pond.py4
-rw-r--r--bot/cogs/python_news.py3
-rw-r--r--bot/cogs/reddit.py8
-rw-r--r--bot/cogs/token_remover.py17
-rw-r--r--bot/cogs/watchchannels/watchchannel.py1
-rw-r--r--bot/cogs/webhook_remover.py14
-rw-r--r--bot/utils/messages.py22
-rw-r--r--tests/bot/cogs/test_token_remover.py32
-rw-r--r--tests/bot/utils/test_messages.py27
10 files changed, 105 insertions, 24 deletions
diff --git a/Pipfile b/Pipfile
index b42ca6d58..33be99587 100644
--- a/Pipfile
+++ b/Pipfile
@@ -50,4 +50,5 @@ precommit = "pre-commit install"
build = "docker build -t pythondiscord/bot:latest -f Dockerfile ."
push = "docker push pythondiscord/bot:latest"
test = "coverage run -m unittest"
+html = "coverage html"
report = "coverage report"
diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py
index 37d1786a2..5b6a7fd62 100644
--- a/bot/cogs/duck_pond.py
+++ b/bot/cogs/duck_pond.py
@@ -7,7 +7,7 @@ from discord.ext.commands import Cog
from bot import constants
from bot.bot import Bot
-from bot.utils.messages import send_attachments
+from bot.utils.messages import send_attachments, sub_clyde
log = logging.getLogger(__name__)
@@ -58,7 +58,7 @@ class DuckPond(Cog):
try:
await self.webhook.send(
content=content,
- username=username,
+ username=sub_clyde(username),
avatar_url=avatar_url,
embed=embed
)
diff --git a/bot/cogs/python_news.py b/bot/cogs/python_news.py
index d15d0371e..adefd5c7c 100644
--- a/bot/cogs/python_news.py
+++ b/bot/cogs/python_news.py
@@ -10,6 +10,7 @@ from discord.ext.tasks import loop
from bot import constants
from bot.bot import Bot
+from bot.utils.messages import sub_clyde
PEPS_RSS_URL = "https://www.python.org/dev/peps/peps.rss/"
@@ -208,7 +209,7 @@ class PythonNews(Cog):
return await self.webhook.send(
embed=embed,
- username=webhook_profile_name,
+ username=sub_clyde(webhook_profile_name),
avatar_url=AVATAR_URL,
wait=True
)
diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py
index 3b77538a0..d853ab2ea 100644
--- a/bot/cogs/reddit.py
+++ b/bot/cogs/reddit.py
@@ -16,6 +16,7 @@ from bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfi
from bot.converters import Subreddit
from bot.decorators import with_role
from bot.pagination import LinePaginator
+from bot.utils.messages import sub_clyde
log = logging.getLogger(__name__)
@@ -218,7 +219,8 @@ class Reddit(Cog):
for subreddit in RedditConfig.subreddits:
top_posts = await self.get_top_posts(subreddit=subreddit, time="day")
- message = await self.webhook.send(username=f"{subreddit} Top Daily Posts", embed=top_posts, wait=True)
+ username = sub_clyde(f"{subreddit} Top Daily Posts")
+ message = await self.webhook.send(username=username, embed=top_posts, wait=True)
if message.channel.is_news():
await message.publish()
@@ -228,8 +230,8 @@ class Reddit(Cog):
for subreddit in RedditConfig.subreddits:
# Send and pin the new weekly posts.
top_posts = await self.get_top_posts(subreddit=subreddit, time="week")
-
- message = await self.webhook.send(wait=True, username=f"{subreddit} Top Weekly Posts", embed=top_posts)
+ username = sub_clyde(f"{subreddit} Top Weekly Posts")
+ message = await self.webhook.send(wait=True, username=username, embed=top_posts)
if subreddit.lower() == "r/python":
if not self.channel:
diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py
index d55e079e9..ef979f222 100644
--- a/bot/cogs/token_remover.py
+++ b/bot/cogs/token_remover.py
@@ -4,7 +4,7 @@ import logging
import re
import typing as t
-from discord import Colour, Message
+from discord import Colour, Message, NotFound
from discord.ext.commands import Cog
from bot import utils
@@ -63,6 +63,10 @@ class TokenRemover(Cog):
See: https://discordapp.com/developers/docs/reference#snowflakes
"""
+ # Ignore DMs; can't delete messages in there anyway.
+ if not msg.guild or msg.author.bot:
+ return
+
found_token = self.find_token_in_message(msg)
if found_token:
await self.take_action(msg, found_token)
@@ -79,7 +83,13 @@ class TokenRemover(Cog):
async def take_action(self, msg: Message, found_token: Token) -> None:
"""Remove the `msg` containing the `found_token` and send a mod log message."""
self.mod_log.ignore(Event.message_delete, msg.id)
- await msg.delete()
+
+ try:
+ await msg.delete()
+ except NotFound:
+ log.debug(f"Failed to remove token in message {msg.id}: message already deleted.")
+ return
+
await msg.channel.send(DELETION_MESSAGE_TEMPLATE.format(mention=msg.author.mention))
log_message = self.format_log_message(msg, found_token)
@@ -112,9 +122,6 @@ class TokenRemover(Cog):
@classmethod
def find_token_in_message(cls, msg: Message) -> t.Optional[Token]:
"""Return a seemingly valid token found in `msg` or `None` if no token is found."""
- if msg.author.bot:
- return
-
# Use finditer rather than search to guard against method calls prematurely returning the
# token check (e.g. `message.channel.send` also matches our token pattern)
for match in TOKEN_RE.finditer(msg.content):
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index 436778c46..7c58a0fb5 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -204,6 +204,7 @@ class WatchChannel(metaclass=CogABCMeta):
embed: Optional[Embed] = None,
) -> None:
"""Sends a message to the webhook with the specified kwargs."""
+ username = messages.sub_clyde(username)
try:
await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed)
except discord.HTTPException as exc:
diff --git a/bot/cogs/webhook_remover.py b/bot/cogs/webhook_remover.py
index 1b5c3f821..543869215 100644
--- a/bot/cogs/webhook_remover.py
+++ b/bot/cogs/webhook_remover.py
@@ -1,7 +1,7 @@
import logging
import re
-from discord import Colour, Message
+from discord import Colour, Message, NotFound
from discord.ext.commands import Cog
from bot.bot import Bot
@@ -35,7 +35,13 @@ class WebhookRemover(Cog):
"""Delete `msg` and send a warning that it contained the Discord webhook `redacted_url`."""
# Don't log this, due internal delete, not by user. Will make different entry.
self.mod_log.ignore(Event.message_delete, msg.id)
- await msg.delete()
+
+ try:
+ await msg.delete()
+ except NotFound:
+ log.debug(f"Failed to remove webhook in message {msg.id}: message already deleted.")
+ return
+
await msg.channel.send(ALERT_MESSAGE_TEMPLATE.format(user=msg.author.mention))
message = (
@@ -59,6 +65,10 @@ class WebhookRemover(Cog):
@Cog.listener()
async def on_message(self, msg: Message) -> None:
"""Check if a Discord webhook URL is in `message`."""
+ # Ignore DMs; can't delete messages in there anyway.
+ if not msg.guild or msg.author.bot:
+ return
+
matches = WEBHOOK_URL_RE.search(msg.content)
if matches:
await self.delete_and_respond(msg, matches[1] + "xxx")
diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index 23519a514..a40a12e98 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -1,6 +1,7 @@
import asyncio
import contextlib
import logging
+import re
from io import BytesIO
from typing import List, Optional, Sequence, Union
@@ -86,7 +87,7 @@ async def send_attachments(
else:
await destination.send(
file=attachment_file,
- username=message.author.display_name,
+ username=sub_clyde(message.author.display_name),
avatar_url=message.author.avatar_url
)
elif link_large:
@@ -109,8 +110,25 @@ async def send_attachments(
else:
await destination.send(
embed=embed,
- username=message.author.display_name,
+ username=sub_clyde(message.author.display_name),
avatar_url=message.author.avatar_url
)
return urls
+
+
+def sub_clyde(username: Optional[str]) -> Optional[str]:
+ """
+ Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"E" and return the new string.
+
+ Discord disallows "clyde" anywhere in the username for webhooks. It will return a 400.
+ Return None only if `username` is None.
+ """
+ def replace_e(match: re.Match) -> str:
+ char = "е" if match[2] == "e" else "Е"
+ return match[1] + char
+
+ if username:
+ return re.sub(r"(clyd)(e)", replace_e, username, flags=re.I)
+ else:
+ return username # Empty string or None
diff --git a/tests/bot/cogs/test_token_remover.py b/tests/bot/cogs/test_token_remover.py
index a10124d2d..3349caa73 100644
--- a/tests/bot/cogs/test_token_remover.py
+++ b/tests/bot/cogs/test_token_remover.py
@@ -3,7 +3,7 @@ from re import Match
from unittest import mock
from unittest.mock import MagicMock
-from discord import Colour
+from discord import Colour, NotFound
from bot import constants
from bot.cogs import token_remover
@@ -121,15 +121,16 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):
find_token_in_message.assert_called_once_with(self.msg)
take_action.assert_not_awaited()
- @autospec("bot.cogs.token_remover", "TOKEN_RE")
- def test_find_token_ignores_bot_messages(self, token_re):
- """The token finder should ignore messages authored by bots."""
- self.msg.author.bot = True
+ @autospec(TokenRemover, "find_token_in_message")
+ async def test_on_message_ignores_dms_bots(self, find_token_in_message):
+ """Shouldn't parse a message if it is a DM or authored by a bot."""
+ cog = TokenRemover(self.bot)
+ dm_msg = MockMessage(guild=None)
+ bot_msg = MockMessage(author=MagicMock(bot=True))
- return_value = TokenRemover.find_token_in_message(self.msg)
-
- self.assertIsNone(return_value)
- token_re.finditer.assert_not_called()
+ for msg in (dm_msg, bot_msg):
+ await cog.on_message(msg)
+ find_token_in_message.assert_not_called()
@autospec("bot.cogs.token_remover", "TOKEN_RE")
def test_find_token_no_matches(self, token_re):
@@ -281,6 +282,19 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):
channel_id=constants.Channels.mod_alerts
)
+ @mock.patch.object(TokenRemover, "mod_log", new_callable=mock.PropertyMock)
+ async def test_take_action_delete_failure(self, mod_log_property):
+ """Shouldn't send any messages if the token message can't be deleted."""
+ cog = TokenRemover(self.bot)
+ mod_log_property.return_value = mock.create_autospec(ModLog, spec_set=True, instance=True)
+ self.msg.delete.side_effect = NotFound(MagicMock(), MagicMock())
+
+ token = mock.create_autospec(Token, spec_set=True, instance=True)
+ await cog.take_action(self.msg, token)
+
+ self.msg.delete.assert_called_once_with()
+ self.msg.channel.send.assert_not_awaited()
+
class TokenRemoverExtensionTests(unittest.TestCase):
"""Tests for the token_remover extension."""
diff --git a/tests/bot/utils/test_messages.py b/tests/bot/utils/test_messages.py
new file mode 100644
index 000000000..9c22c9751
--- /dev/null
+++ b/tests/bot/utils/test_messages.py
@@ -0,0 +1,27 @@
+import unittest
+
+from bot.utils import messages
+
+
+class TestMessages(unittest.TestCase):
+ """Tests for functions in the `bot.utils.messages` module."""
+
+ def test_sub_clyde(self):
+ """Uppercase E's and lowercase e's are substituted with their cyrillic counterparts."""
+ sub_e = "\u0435"
+ sub_E = "\u0415" # noqa: N806: Uppercase E in variable name
+
+ test_cases = (
+ (None, None),
+ ("", ""),
+ ("clyde", f"clyd{sub_e}"),
+ ("CLYDE", f"CLYD{sub_E}"),
+ ("cLyDe", f"cLyD{sub_e}"),
+ ("BIGclyde", f"BIGclyd{sub_e}"),
+ ("small clydeus the unholy", f"small clyd{sub_e}us the unholy"),
+ ("BIGCLYDE, babyclyde", f"BIGCLYD{sub_E}, babyclyd{sub_e}"),
+ )
+
+ for username_in, username_out in test_cases:
+ with self.subTest(input=username_in, expected_output=username_out):
+ self.assertEqual(messages.sub_clyde(username_in), username_out)