diff options
| author | 2020-06-20 13:53:00 +0200 | |
|---|---|---|
| committer | 2020-06-20 13:53:00 +0200 | |
| commit | 63802949402a21586b20c8d5873e1a053c730d2b (patch) | |
| tree | c8fc51fa8aa6c579144dead4845ce0439e1d9c1e | |
| parent | Incidents tests: remove unnecessary patch (diff) | |
| parent | Merge pull request #1009 from python-discord/bug/mod/bot-2a/webhook-clyde (diff) | |
Merge branch 'origin/master' into kwzrd/incidents
Adds the `sub_clyde` function, which we'll apply to our webhook.
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | bot/cogs/duck_pond.py | 4 | ||||
| -rw-r--r-- | bot/cogs/python_news.py | 3 | ||||
| -rw-r--r-- | bot/cogs/reddit.py | 8 | ||||
| -rw-r--r-- | bot/cogs/token_remover.py | 17 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/watchchannel.py | 1 | ||||
| -rw-r--r-- | bot/cogs/webhook_remover.py | 14 | ||||
| -rw-r--r-- | bot/utils/messages.py | 22 | ||||
| -rw-r--r-- | tests/bot/cogs/test_token_remover.py | 32 | ||||
| -rw-r--r-- | tests/bot/utils/test_messages.py | 27 | 
10 files changed, 105 insertions, 24 deletions
| @@ -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) | 
