From cb5e361d04cd9c430bca4fb3496284e469d35c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 14:40:26 +0200 Subject: [PATCH 01/15] Add the #dm_log ID to constants. https://github.com/python-discord/bot/issues/667 --- bot/constants.py | 1 + config-default.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index a1b392c82d..074699025f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -416,6 +416,7 @@ class Channels(metaclass=YAMLGetter): user_log: int verification: int voice_log: int + dm_log: int class Webhooks(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 64c4e715b4..d3ba45f881 100644 --- a/config-default.yml +++ b/config-default.yml @@ -150,6 +150,7 @@ guild: mod_log: &MOD_LOG 282638479504965634 user_log: 528976905546760203 voice_log: 640292421988646961 + dm_log: 653713721625018428 # Off-topic off_topic_0: 291284109232308226 From 9042325f06523e04a2c51b39fd20436cd6eaa3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 14:57:15 +0200 Subject: [PATCH 02/15] Refactor Duck Pond embed sender to be a util. https://github.com/python-discord/bot/issues/667 --- bot/cogs/duck_pond.py | 30 ++++++++---------------------- bot/utils/webhooks.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 bot/utils/webhooks.py diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 5b6a7fd62c..89b4ad0e4c 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Union +from typing import Union import discord from discord import Color, Embed, Member, Message, RawReactionActionEvent, User, errors @@ -7,7 +7,8 @@ from bot import constants from bot.bot import Bot -from bot.utils.messages import send_attachments, sub_clyde +from bot.utils.messages import send_attachments +from bot.utils.webhooks import send_webhook log = logging.getLogger(__name__) @@ -18,6 +19,7 @@ class DuckPond(Cog): def __init__(self, bot: Bot): self.bot = bot self.webhook_id = constants.Webhooks.duck_pond + self.webhook = None self.bot.loop.create_task(self.fetch_webhook()) async def fetch_webhook(self) -> None: @@ -47,24 +49,6 @@ async def has_green_checkmark(self, message: Message) -> bool: return True return False - async def send_webhook( - self, - content: Optional[str] = None, - username: Optional[str] = None, - avatar_url: Optional[str] = None, - embed: Optional[Embed] = None, - ) -> None: - """Send a webhook to the duck_pond channel.""" - try: - await self.webhook.send( - content=content, - username=sub_clyde(username), - avatar_url=avatar_url, - embed=embed - ) - except discord.HTTPException: - log.exception("Failed to send a message to the Duck Pool webhook") - async def count_ducks(self, message: Message) -> int: """ Count the number of ducks in the reactions of a specific message. @@ -97,7 +81,8 @@ async def relay_message(self, message: Message) -> None: clean_content = message.clean_content if clean_content: - await self.send_webhook( + await send_webhook( + webhook=self.webhook, content=message.clean_content, username=message.author.display_name, avatar_url=message.author.avatar_url @@ -111,7 +96,8 @@ async def relay_message(self, message: Message) -> None: description=":x: **This message contained an attachment, but it could not be retrieved**", color=Color.red() ) - await self.send_webhook( + await send_webhook( + webhook=self.webhook, embed=e, username=message.author.display_name, avatar_url=message.author.avatar_url diff --git a/bot/utils/webhooks.py b/bot/utils/webhooks.py new file mode 100644 index 0000000000..37fdfe9075 --- /dev/null +++ b/bot/utils/webhooks.py @@ -0,0 +1,34 @@ +import logging +from typing import Optional + +import discord +from discord import Embed + +from bot.utils.messages import sub_clyde + +log = logging.getLogger(__name__) + + +async def send_webhook( + webhook: discord.Webhook, + content: Optional[str] = None, + username: Optional[str] = None, + avatar_url: Optional[str] = None, + embed: Optional[Embed] = None, + wait: Optional[bool] = False +) -> None: + """ + Send a message using the provided webhook. + + This uses sub_clyde() and tries for an HTTPException to ensure it doesn't crash. + """ + try: + await webhook.send( + content=content, + username=sub_clyde(username), + avatar_url=avatar_url, + embed=embed, + wait=wait, + ) + except discord.HTTPException: + log.exception("Failed to send a message to the webhook!") From ef65033eaed01a2459561dd9fe37133b595f3d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 15:10:00 +0200 Subject: [PATCH 03/15] Refactor python_news.py to use webhook util. https://github.com/python-discord/bot/issues/667 --- bot/cogs/python_news.py | 70 +++++++++++++++++------------------------ bot/utils/webhooks.py | 4 +-- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/bot/cogs/python_news.py b/bot/cogs/python_news.py index adefd5c7c4..1d8f2aeb0d 100644 --- a/bot/cogs/python_news.py +++ b/bot/cogs/python_news.py @@ -10,7 +10,7 @@ from bot import constants from bot.bot import Bot -from bot.utils.messages import sub_clyde +from bot.utils.webhooks import send_webhook PEPS_RSS_URL = "https://www.python.org/dev/peps/peps.rss/" @@ -100,13 +100,20 @@ async def post_pep_news(self) -> None: ): continue - msg = await self.send_webhook( + # Build an embed and send a webhook + embed = discord.Embed( title=new["title"], description=new["summary"], timestamp=new_datetime, url=new["link"], - webhook_profile_name=data["feed"]["title"], - footer=data["feed"]["title"] + colour=constants.Colours.soft_green + ) + embed.set_footer(text=data["feed"]["title"], icon_url=AVATAR_URL) + msg = await send_webhook( + webhook=self.webhook, + username=data["feed"]["title"], + embed=embed, + wait=True, ) payload["data"]["pep"].append(pep_nr) @@ -161,15 +168,28 @@ async def post_maillist_news(self) -> None: content = email_information["content"] link = THREAD_URL.format(id=thread["href"].split("/")[-2], list=maillist) - msg = await self.send_webhook( + + # Build an embed and send a message to the webhook + embed = discord.Embed( title=thread_information["subject"], description=content[:500] + f"... [continue reading]({link})" if len(content) > 500 else content, timestamp=new_date, url=link, - author=f"{email_information['sender_name']} ({email_information['sender']['address']})", - author_url=MAILMAN_PROFILE_URL.format(id=email_information["sender"]["mailman_id"]), - webhook_profile_name=self.webhook_names[maillist], - footer=f"Posted to {self.webhook_names[maillist]}" + colour=constants.Colours.soft_green + ) + embed.set_author( + name=f"{email_information['sender_name']} ({email_information['sender']['address']})", + url=MAILMAN_PROFILE_URL.format(id=email_information["sender"]["mailman_id"]), + ) + embed.set_footer( + text=f"Posted to {self.webhook_names[maillist]}", + icon_url=AVATAR_URL, + ) + msg = await send_webhook( + webhook=self.webhook, + username=self.webhook_names[maillist], + embed=embed, + wait=True, ) payload["data"][maillist].append(thread_information["thread_id"]) @@ -182,38 +202,6 @@ async def post_maillist_news(self) -> None: await self.bot.api_client.put("bot/bot-settings/news", json=payload) - async def send_webhook(self, - title: str, - description: str, - timestamp: datetime, - url: str, - webhook_profile_name: str, - footer: str, - author: t.Optional[str] = None, - author_url: t.Optional[str] = None, - ) -> discord.Message: - """Send webhook entry and return sent message.""" - embed = discord.Embed( - title=title, - description=description, - timestamp=timestamp, - url=url, - colour=constants.Colours.soft_green - ) - if author and author_url: - embed.set_author( - name=author, - url=author_url - ) - embed.set_footer(text=footer, icon_url=AVATAR_URL) - - return await self.webhook.send( - embed=embed, - username=sub_clyde(webhook_profile_name), - avatar_url=AVATAR_URL, - wait=True - ) - async def get_thread_and_first_mail(self, maillist: str, thread_identifier: str) -> t.Tuple[t.Any, t.Any]: """Get mail thread and first mail from mail.python.org based on `maillist` and `thread_identifier`.""" async with self.bot.http_session.get( diff --git a/bot/utils/webhooks.py b/bot/utils/webhooks.py index 37fdfe9075..66f82ec66f 100644 --- a/bot/utils/webhooks.py +++ b/bot/utils/webhooks.py @@ -16,14 +16,14 @@ async def send_webhook( avatar_url: Optional[str] = None, embed: Optional[Embed] = None, wait: Optional[bool] = False -) -> None: +) -> discord.Message: """ Send a message using the provided webhook. This uses sub_clyde() and tries for an HTTPException to ensure it doesn't crash. """ try: - await webhook.send( + return await webhook.send( content=content, username=sub_clyde(username), avatar_url=avatar_url, From 3fce243e15996eb81157c198544fcc705e46e1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 15:27:23 +0200 Subject: [PATCH 04/15] Relay all DMs and embeds to #dm-log. https://github.com/python-discord/bot/issues/667 --- bot/__main__.py | 1 + bot/cogs/dm_relay.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 bot/cogs/dm_relay.py diff --git a/bot/__main__.py b/bot/__main__.py index 37e62c2f18..49388455a9 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -54,6 +54,7 @@ # Feature cogs bot.load_extension("bot.cogs.alias") bot.load_extension("bot.cogs.defcon") +bot.load_extension("bot.cogs.dm_relay") bot.load_extension("bot.cogs.duck_pond") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.information") diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py new file mode 100644 index 0000000000..32ac0e4ee5 --- /dev/null +++ b/bot/cogs/dm_relay.py @@ -0,0 +1,66 @@ +import logging + +import discord +from discord import Color +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.webhooks import send_webhook + +log = logging.getLogger(__name__) + + +class DMRelay(Cog): + """Debug logging module.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.webhook_id = constants.Webhooks.dm_log + self.webhook = None + self.bot.loop.create_task(self.fetch_webhook()) + + async def fetch_webhook(self) -> None: + """Fetches the webhook object, so we can post to it.""" + await self.bot.wait_until_guild_available() + + try: + self.webhook = await self.bot.fetch_webhook(self.webhook_id) + except discord.HTTPException: + log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") + + @Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Relays the message's content and attachments to the dm_log channel.""" + clean_content = message.clean_content + if clean_content: + await send_webhook( + webhook=self.webhook, + content=message.clean_content, + username=message.author.display_name, + avatar_url=message.author.avatar_url + ) + + # Handle any attachments + if message.attachments: + try: + await send_attachments(message, self.webhook) + except (discord.errors.Forbidden, discord.errors.NotFound): + e = discord.Embed( + description=":x: **This message contained an attachment, but it could not be retrieved**", + color=Color.red() + ) + await send_webhook( + webhook=self.webhook, + embed=e, + username=message.author.display_name, + avatar_url=message.author.avatar_url + ) + except discord.HTTPException: + log.exception("Failed to send an attachment to the webhook") + + +def setup(bot: Bot) -> None: + """Load the DMRelay cog.""" + bot.add_cog(DMRelay(bot)) From 5007e736b93017003f02a75d12ce1ef8bae9fd69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 15:34:39 +0200 Subject: [PATCH 05/15] Replace channel ID with webhook ID for dm_log. https://github.com/python-discord/bot/issues/667 --- bot/constants.py | 2 +- config-default.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 074699025f..3f44003a80 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -416,7 +416,6 @@ class Channels(metaclass=YAMLGetter): user_log: int verification: int voice_log: int - dm_log: int class Webhooks(metaclass=YAMLGetter): @@ -428,6 +427,7 @@ class Webhooks(metaclass=YAMLGetter): reddit: int duck_pond: int dev_log: int + dm_log: int class Roles(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index d3ba45f881..c09902a5d6 100644 --- a/config-default.yml +++ b/config-default.yml @@ -150,7 +150,6 @@ guild: mod_log: &MOD_LOG 282638479504965634 user_log: 528976905546760203 voice_log: 640292421988646961 - dm_log: 653713721625018428 # Off-topic off_topic_0: 291284109232308226 @@ -252,10 +251,9 @@ guild: duck_pond: 637821475327311927 dev_log: 680501655111729222 python_news: &PYNEWS_WEBHOOK 704381182279942324 - + dm_log: 654567640664244225 filter: - # What do we filter? filter_zalgo: false filter_invites: true From 4349fdedaae43f35f9821aa61c91a1e76908b0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 15:39:40 +0200 Subject: [PATCH 06/15] Only relay DMs, and only from humans. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 32ac0e4ee5..bb060fe903 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -33,6 +33,10 @@ async def fetch_webhook(self) -> None: @Cog.listener() async def on_message(self, message: discord.Message) -> None: """Relays the message's content and attachments to the dm_log channel.""" + # Only relay DMs from humans + if message.author.bot or message.guild: + return + clean_content = message.clean_content if clean_content: await send_webhook( From df1730ef5d51223fe1d5a2cfe8c027e5177ae9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 16:30:03 +0200 Subject: [PATCH 07/15] Fix DuckPond tests now that send_webhook is gone. Some of the tests were failing because they were expecting send_webhook to be a method of the DuckPond cog, other tests simply were no longer applicable, and have been removed. https://github.com/python-discord/bot/issues/667 --- tests/bot/cogs/test_duck_pond.py | 51 ++++++++------------------------ 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/cogs/test_duck_pond.py index a8c0107c63..cfe10aebff 100644 --- a/tests/bot/cogs/test_duck_pond.py +++ b/tests/bot/cogs/test_duck_pond.py @@ -129,38 +129,6 @@ async def test_has_green_checkmark_correctly_detects_presence_of_green_checkmark ): self.assertEqual(expected_return, actual_return) - def test_send_webhook_correctly_passes_on_arguments(self): - """The `send_webhook` method should pass the arguments to the webhook correctly.""" - self.cog.webhook = helpers.MockAsyncWebhook() - - content = "fake content" - username = "fake username" - avatar_url = "fake avatar_url" - embed = "fake embed" - - asyncio.run(self.cog.send_webhook(content, username, avatar_url, embed)) - - self.cog.webhook.send.assert_called_once_with( - content=content, - username=username, - avatar_url=avatar_url, - embed=embed - ) - - def test_send_webhook_logs_when_sending_message_fails(self): - """The `send_webhook` method should catch a `discord.HTTPException` and log accordingly.""" - self.cog.webhook = helpers.MockAsyncWebhook() - self.cog.webhook.send.side_effect = discord.HTTPException(response=MagicMock(), message="Something failed.") - - log = logging.getLogger('bot.cogs.duck_pond') - with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher: - asyncio.run(self.cog.send_webhook()) - - self.assertEqual(len(log_watcher.records), 1) - - record = log_watcher.records[0] - self.assertEqual(record.levelno, logging.ERROR) - def _get_reaction( self, emoji: typing.Union[str, helpers.MockEmoji], @@ -280,16 +248,20 @@ async def test_count_ducks_correctly_counts_the_number_of_eligible_duck_emojis(s async def test_relay_message_correctly_relays_content_and_attachments(self): """The `relay_message` method should correctly relay message content and attachments.""" - send_webhook_path = f"{MODULE_PATH}.DuckPond.send_webhook" + send_webhook_path = f"{MODULE_PATH}.send_webhook" send_attachments_path = f"{MODULE_PATH}.send_attachments" + author = MagicMock( + display_name="x", + avatar_url="https://" + ) self.cog.webhook = helpers.MockAsyncWebhook() test_values = ( - (helpers.MockMessage(clean_content="", attachments=[]), False, False), - (helpers.MockMessage(clean_content="message", attachments=[]), True, False), - (helpers.MockMessage(clean_content="", attachments=["attachment"]), False, True), - (helpers.MockMessage(clean_content="message", attachments=["attachment"]), True, True), + (helpers.MockMessage(author=author, clean_content="", attachments=[]), False, False), + (helpers.MockMessage(author=author, clean_content="message", attachments=[]), True, False), + (helpers.MockMessage(author=author, clean_content="", attachments=["attachment"]), False, True), + (helpers.MockMessage(author=author, clean_content="message", attachments=["attachment"]), True, True), ) for message, expect_webhook_call, expect_attachment_call in test_values: @@ -314,14 +286,14 @@ async def test_relay_message_handles_irretrievable_attachment_exceptions(self, s for side_effect in side_effects: # pragma: no cover send_attachments.side_effect = side_effect - with patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=AsyncMock) as send_webhook: + with patch(f"{MODULE_PATH}.send_webhook", new_callable=AsyncMock) as send_webhook: with self.subTest(side_effect=type(side_effect).__name__): with self.assertNotLogs(logger=log, level=logging.ERROR): await self.cog.relay_message(message) self.assertEqual(send_webhook.call_count, 2) - @patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=AsyncMock) + @patch(f"{MODULE_PATH}.send_webhook", new_callable=AsyncMock) @patch(f"{MODULE_PATH}.send_attachments", new_callable=AsyncMock) async def test_relay_message_handles_attachment_http_error(self, send_attachments, send_webhook): """The `relay_message` method should handle irretrievable attachments.""" @@ -337,6 +309,7 @@ async def test_relay_message_handles_attachment_http_error(self, send_attachment await self.cog.relay_message(message) send_webhook.assert_called_once_with( + webhook=self.cog.webhook, content=message.clean_content, username=message.author.display_name, avatar_url=message.author.avatar_url From aaf8db7550e8b95354d6f079c99ef2beb400cac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Sun, 12 Jul 2020 20:19:54 +0200 Subject: [PATCH 08/15] Add a way to respond to DMs. This shouldn't be used as a replacement for ModMail, but I think it makes sense to have the feature just in case #dm-log provides an interesting use-case where responding as the bot makes sense. It's a bit of a curiosity, and Ves hates it, but I included it anyway. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index bb060fe903..df19000fe0 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -1,7 +1,9 @@ import logging +from typing import Optional import discord from discord import Color +from discord.ext import commands from discord.ext.commands import Cog from bot import constants @@ -20,6 +22,32 @@ def __init__(self, bot: Bot): self.webhook_id = constants.Webhooks.dm_log self.webhook = None self.bot.loop.create_task(self.fetch_webhook()) + self.last_dm_user = None + + @commands.command(aliases=("reply",)) + async def send_dm(self, ctx: commands.Context, member: Optional[discord.Member], *, message: str) -> None: + """ + Allows you to send a DM to a user from the bot. + + If `member` is not provided, it will send to the last user who DM'd the bot. + + This feature should be used extremely sparingly. Use ModMail if you need to have a serious + conversation with a user. This is just for responding to extraordinary DMs, having a little + fun with users, and telling people they are DMing the wrong bot. + + NOTE: This feature will be removed if it is overused. + """ + if member: + await member.send(message) + await ctx.message.add_reaction("✅") + return + elif self.last_dm_user: + await self.last_dm_user.send(message) + await ctx.message.add_reaction("✅") + return + else: + log.debug("Unable to send a DM to the user.") + await ctx.message.add_reaction("❌") async def fetch_webhook(self) -> None: """Fetches the webhook object, so we can post to it.""" @@ -45,6 +73,7 @@ async def on_message(self, message: discord.Message) -> None: username=message.author.display_name, avatar_url=message.author.avatar_url ) + self.last_dm_user = message.author # Handle any attachments if message.attachments: From ed2368791870bd0b464391d9da7b13de15b322a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 15:13:39 +0200 Subject: [PATCH 09/15] Better docstring for DMRelay cog. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index df19000fe0..c6206629e5 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -15,7 +15,7 @@ class DMRelay(Cog): - """Debug logging module.""" + """Relay direct messages to and from the bot.""" def __init__(self, bot: Bot): self.bot = bot From ab1546611a9952ddb45f211901ad129c2e8c5007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 15:14:31 +0200 Subject: [PATCH 10/15] Add avatar_url in python_news.py https://github.com/python-discord/bot/issues/667 --- bot/cogs/python_news.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/cogs/python_news.py b/bot/cogs/python_news.py index 1d8f2aeb0d..0ab5738a49 100644 --- a/bot/cogs/python_news.py +++ b/bot/cogs/python_news.py @@ -113,6 +113,7 @@ async def post_pep_news(self) -> None: webhook=self.webhook, username=data["feed"]["title"], embed=embed, + avatar_url=AVATAR_URL, wait=True, ) payload["data"]["pep"].append(pep_nr) @@ -189,6 +190,7 @@ async def post_maillist_news(self) -> None: webhook=self.webhook, username=self.webhook_names[maillist], embed=embed, + avatar_url=AVATAR_URL, wait=True, ) payload["data"][maillist].append(thread_information["thread_id"]) From 87c2ef7610a42207b0289820458285648f5dd41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 15:20:59 +0200 Subject: [PATCH 11/15] Only mods+ may use the commands in this cog. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index c6206629e5..67411f57b8 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -8,6 +8,8 @@ from bot import constants from bot.bot import Bot +from bot.constants import MODERATION_ROLES +from bot.utils.checks import with_role_check from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -93,6 +95,10 @@ async def on_message(self, message: discord.Message) -> None: except discord.HTTPException: log.exception("Failed to send an attachment to the webhook") + def cog_check(self, ctx: commands.Context) -> bool: + """Only allow moderators to invoke the commands in this cog.""" + return with_role_check(ctx, *MODERATION_ROLES) + def setup(bot: Bot) -> None: """Load the DMRelay cog.""" From 311936991d5543e35dbe4a5a5a13261fb44c27f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 15:24:58 +0200 Subject: [PATCH 12/15] Don't run on_message if self.webhook is None. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 67411f57b8..3d16db8a07 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -64,7 +64,7 @@ async def fetch_webhook(self) -> None: async def on_message(self, message: discord.Message) -> None: """Relays the message's content and attachments to the dm_log channel.""" # Only relay DMs from humans - if message.author.bot or message.guild: + if message.author.bot or message.guild or self.webhook is None: return clean_content = message.clean_content From aa0b20bdd780bc75cadde781981d063287bfe5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 15:27:41 +0200 Subject: [PATCH 13/15] Remove redundant clean_content variable. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 3 +-- bot/cogs/duck_pond.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 3d16db8a07..494c710669 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -67,8 +67,7 @@ async def on_message(self, message: discord.Message) -> None: if message.author.bot or message.guild or self.webhook is None: return - clean_content = message.clean_content - if clean_content: + if message.clean_content: await send_webhook( webhook=self.webhook, content=message.clean_content, diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 89b4ad0e4c..7021069fae 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -78,9 +78,7 @@ async def count_ducks(self, message: Message) -> int: async def relay_message(self, message: Message) -> None: """Relays the message's content and attachments to the duck pond channel.""" - clean_content = message.clean_content - - if clean_content: + if message.clean_content: await send_webhook( webhook=self.webhook, content=message.clean_content, From ea62b6bae85113be913101a41053c91497b23c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Mon, 13 Jul 2020 21:09:41 +0200 Subject: [PATCH 14/15] Store last DM user in RedisCache. Also now catches the exception if a user has disabled DMs, and adds a red cross reaction. https://github.com/python-discord/bot/issues/667 --- bot/cogs/dm_relay.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 494c710669..3fce52b93b 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -9,6 +9,7 @@ from bot import constants from bot.bot import Bot from bot.constants import MODERATION_ROLES +from bot.utils import RedisCache from bot.utils.checks import with_role_check from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -19,12 +20,14 @@ class DMRelay(Cog): """Relay direct messages to and from the bot.""" + # RedisCache[str, t.Union[discord.User.id, discord.Member.id]] + dm_cache = RedisCache() + def __init__(self, bot: Bot): self.bot = bot self.webhook_id = constants.Webhooks.dm_log self.webhook = None self.bot.loop.create_task(self.fetch_webhook()) - self.last_dm_user = None @commands.command(aliases=("reply",)) async def send_dm(self, ctx: commands.Context, member: Optional[discord.Member], *, message: str) -> None: @@ -39,16 +42,23 @@ async def send_dm(self, ctx: commands.Context, member: Optional[discord.Member], NOTE: This feature will be removed if it is overused. """ - if member: - await member.send(message) - await ctx.message.add_reaction("✅") - return - elif self.last_dm_user: - await self.last_dm_user.send(message) - await ctx.message.add_reaction("✅") - return - else: - log.debug("Unable to send a DM to the user.") + user_id = await self.dm_cache.get("last_user") + last_dm_user = ctx.guild.get_member(user_id) if user_id else None + + try: + if member: + await member.send(message) + await ctx.message.add_reaction("✅") + return + elif last_dm_user: + await last_dm_user.send(message) + await ctx.message.add_reaction("✅") + return + else: + log.debug("This bot has never gotten a DM, or the RedisCache has been cleared.") + await ctx.message.add_reaction("❌") + except discord.errors.Forbidden: + log.debug("User has disabled DMs.") await ctx.message.add_reaction("❌") async def fetch_webhook(self) -> None: @@ -74,7 +84,7 @@ async def on_message(self, message: discord.Message) -> None: username=message.author.display_name, avatar_url=message.author.avatar_url ) - self.last_dm_user = message.author + await self.dm_cache.set("last_user", message.author.id) # Handle any attachments if message.attachments: From 042f472ac3207ad685a5acb659a5a69f22c72282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Sand=C3=B8y?= Date: Wed, 15 Jul 2020 01:09:35 +0200 Subject: [PATCH 15/15] Remove caching of last_dm_user. If you're typing up a reply and the bot gets another DM while you're typing, you might accidentally send your reply to the wrong person. This could happen even if you're very attentive, because it might be a matter of milliseconds. The complexity to prevent this isn't worth the convenience of the feature, and it's nice to get rid of the caching as well, so I've decided to just make .reply require a user for every reply. https://github.com/python-discord/bot/issues/1041 --- bot/cogs/dm_relay.py | 42 +++++++++++++++++------------------------- bot/constants.py | 2 ++ config-default.yml | 1 + 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 3fce52b93b..f62d6105e5 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import discord from discord import Color @@ -8,9 +7,7 @@ from bot import constants from bot.bot import Bot -from bot.constants import MODERATION_ROLES -from bot.utils import RedisCache -from bot.utils.checks import with_role_check +from bot.utils.checks import in_whitelist_check, with_role_check from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -20,9 +17,6 @@ class DMRelay(Cog): """Relay direct messages to and from the bot.""" - # RedisCache[str, t.Union[discord.User.id, discord.Member.id]] - dm_cache = RedisCache() - def __init__(self, bot: Bot): self.bot = bot self.webhook_id = constants.Webhooks.dm_log @@ -30,11 +24,11 @@ def __init__(self, bot: Bot): self.bot.loop.create_task(self.fetch_webhook()) @commands.command(aliases=("reply",)) - async def send_dm(self, ctx: commands.Context, member: Optional[discord.Member], *, message: str) -> None: + async def send_dm(self, ctx: commands.Context, member: discord.Member, *, message: str) -> None: """ Allows you to send a DM to a user from the bot. - If `member` is not provided, it will send to the last user who DM'd the bot. + A `member` must be provided. This feature should be used extremely sparingly. Use ModMail if you need to have a serious conversation with a user. This is just for responding to extraordinary DMs, having a little @@ -42,21 +36,11 @@ async def send_dm(self, ctx: commands.Context, member: Optional[discord.Member], NOTE: This feature will be removed if it is overused. """ - user_id = await self.dm_cache.get("last_user") - last_dm_user = ctx.guild.get_member(user_id) if user_id else None - try: - if member: - await member.send(message) - await ctx.message.add_reaction("✅") - return - elif last_dm_user: - await last_dm_user.send(message) - await ctx.message.add_reaction("✅") - return - else: - log.debug("This bot has never gotten a DM, or the RedisCache has been cleared.") - await ctx.message.add_reaction("❌") + await member.send(message) + await ctx.message.add_reaction("✅") + return + except discord.errors.Forbidden: log.debug("User has disabled DMs.") await ctx.message.add_reaction("❌") @@ -84,7 +68,6 @@ async def on_message(self, message: discord.Message) -> None: username=message.author.display_name, avatar_url=message.author.avatar_url ) - await self.dm_cache.set("last_user", message.author.id) # Handle any attachments if message.attachments: @@ -106,7 +89,16 @@ async def on_message(self, message: discord.Message) -> None: def cog_check(self, ctx: commands.Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *MODERATION_ROLES) + checks = [ + with_role_check(ctx, *constants.MODERATION_ROLES), + in_whitelist_check( + ctx, + channels=[constants.Channels.dm_log], + redirect=None, + fail_silently=True, + ) + ] + return all(checks) def setup(bot: Bot) -> None: diff --git a/bot/constants.py b/bot/constants.py index 3f44003a80..778bc093c0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -395,6 +395,7 @@ class Channels(metaclass=YAMLGetter): dev_contrib: int dev_core: int dev_log: int + dm_log: int esoteric: int helpers: int how_to_get_help: int @@ -461,6 +462,7 @@ class Guild(metaclass=YAMLGetter): staff_channels: List[int] staff_roles: List[int] + class Keys(metaclass=YAMLGetter): section = "keys" diff --git a/config-default.yml b/config-default.yml index d12b9be271..8061e5e16a 100644 --- a/config-default.yml +++ b/config-default.yml @@ -150,6 +150,7 @@ guild: mod_log: &MOD_LOG 282638479504965634 user_log: 528976905546760203 voice_log: 640292421988646961 + dm_log: 653713721625018428 # Off-topic off_topic_0: 291284109232308226