From f9f9ced5320a593bb59836086d0d5983b4df58df Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 27 Mar 2021 13:17:12 -0400 Subject: Restrict redirects for paste uploads with an extension that is not `.py`. --- bot/utils/services.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/utils/services.py b/bot/utils/services.py index 5949c9e48..68cbd896e 100644 --- a/bot/utils/services.py +++ b/bot/utils/services.py @@ -47,7 +47,13 @@ async def send_to_paste_service(contents: str, *, extension: str = "") -> Option continue elif "key" in response_json: log.info(f"Successfully uploaded contents to paste service behind key {response_json['key']}.") - return URLs.paste_service.format(key=response_json['key']) + extension + + paste_link = URLs.paste_service.format(key=response_json['key']) + extension + + if extension == '.py': + return paste_link + return paste_link + "?noredirect" + log.warning( f"Got unexpected JSON response from paste service: {response_json}\n" f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})." -- cgit v1.2.3 From 2abbcc8a63d0378392eba52593a65249d9204e9e Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 27 Mar 2021 13:26:25 -0400 Subject: Remove the old DMRelay cog. The moderation team has decided that this may come in conflict with Discord's ToS, and it does not serve too much of a purpose anymore. It was fun while it lasted! --- bot/exts/moderation/dm_relay.py | 132 ---------------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 bot/exts/moderation/dm_relay.py diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py deleted file mode 100644 index 6d081741c..000000000 --- a/bot/exts/moderation/dm_relay.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -from typing import Optional - -import discord -from async_rediscache import RedisCache -from discord import Color -from discord.ext import commands -from discord.ext.commands import Cog - -from bot import constants -from bot.bot import Bot -from bot.converters import UserMentionOrID -from bot.utils.checks import in_whitelist_check -from bot.utils.messages import send_attachments -from bot.utils.webhooks import send_webhook - -log = logging.getLogger(__name__) - - -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()) - - @commands.command(aliases=("reply",)) - async def send_dm(self, ctx: commands.Context, member: Optional[UserMentionOrID], *, 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 not member: - user_id = await self.dm_cache.get("last_user") - member = ctx.guild.get_member(user_id) if user_id else None - - # If we still don't have a Member at this point, give up - if not member: - log.debug("This bot has never gotten a DM, or the RedisCache has been cleared.") - await ctx.message.add_reaction("❌") - return - - if member.id == self.bot.user.id: - log.debug("Not sending message to bot user") - return await ctx.send("🚫 I can't send messages to myself!") - - try: - await member.send(message) - except discord.errors.Forbidden: - log.debug("User has disabled DMs.") - await ctx.message.add_reaction("❌") - else: - await ctx.message.add_reaction("✅") - self.bot.stats.incr("dm_relay.dm_sent") - - 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.""" - # Only relay DMs from humans - if message.author.bot or message.guild or self.webhook is None: - return - - if message.clean_content: - await send_webhook( - webhook=self.webhook, - content=message.clean_content, - username=f"{message.author.display_name} ({message.author.id})", - avatar_url=message.author.avatar_url - ) - await self.dm_cache.set("last_user", message.author.id) - self.bot.stats.incr("dm_relay.dm_received") - - # Handle any attachments - if message.attachments: - try: - await send_attachments( - message, - self.webhook, - username=f"{message.author.display_name} ({message.author.id})" - ) - 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=f"{message.author.display_name} ({message.author.id})", - avatar_url=message.author.avatar_url - ) - except discord.HTTPException: - log.exception("Failed to send an attachment to the webhook") - - async def cog_check(self, ctx: commands.Context) -> bool: - """Only allow moderators to invoke the commands in this cog.""" - checks = [ - await commands.has_any_role(*constants.MODERATION_ROLES).predicate(ctx), - in_whitelist_check( - ctx, - channels=[constants.Channels.dm_log], - redirect=None, - fail_silently=True, - ) - ] - return all(checks) - - -def setup(bot: Bot) -> None: - """Load the DMRelay cog.""" - bot.add_cog(DMRelay(bot)) -- cgit v1.2.3 From 2759409123d458a4a0a274b835bebb3cc728b83a Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 27 Mar 2021 13:49:22 -0400 Subject: Fix tests for paste uploads. Accounts for no redirects on extensions that are not `.py`. --- tests/bot/utils/test_services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/utils/test_services.py b/tests/bot/utils/test_services.py index 1b48f6560..3b71022db 100644 --- a/tests/bot/utils/test_services.py +++ b/tests/bot/utils/test_services.py @@ -30,9 +30,9 @@ class PasteTests(unittest.IsolatedAsyncioTestCase): """Url with specified extension is returned on successful requests.""" key = "paste_key" test_cases = ( - (f"https://paste_service.com/{key}.txt", "txt"), + (f"https://paste_service.com/{key}.txt?noredirect", "txt"), (f"https://paste_service.com/{key}.py", "py"), - (f"https://paste_service.com/{key}", ""), + (f"https://paste_service.com/{key}?noredirect", ""), ) response = MagicMock( json=AsyncMock(return_value={"key": key}) -- cgit v1.2.3 From e85988e8d63af2a30835a72ec363895b60f22260 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 27 Mar 2021 13:51:12 -0400 Subject: Create the new DMRelay cog. Includes the `!dmrelay` command, allowing moderators to relay direct messages between the bot and other users. --- bot/exts/moderation/dm_relay.py | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 bot/exts/moderation/dm_relay.py diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py new file mode 100644 index 000000000..2bf2391a4 --- /dev/null +++ b/bot/exts/moderation/dm_relay.py @@ -0,0 +1,59 @@ +import logging +import textwrap + +import discord +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import Emojis +from bot.utils.services import send_to_paste_service + +log = logging.getLogger(__name__) + + +class DMRelay(Cog): + """Relay direct messages from the bot.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @command(aliases=("relay", "dr")) + async def dmrelay(self, ctx: Context, user: discord.User, limit: int = 100) -> None: + """Relays the direct message history between the bot and given user.""" + log.trace(f"Relaying DMs with {user.name} ({user.id})") + + if not user.dm_channel: + await ctx.send(f"{Emojis.cross_mark} No direct message history with {user.mention}.") + return + + output = textwrap.dedent(f"""\ + User: {user} ({user.id}) + Channel ID: {user.dm_channel.id}\n + """) + + async for msg in user.history(limit=limit, oldest_first=True): + created_at = msg.created_at.strftime(r"%Y-%m-%d %H:%M") + + # Metadata (author, created_at, id) + output += f"{msg.author} [{created_at}] ({msg.id}): " + + # Content + if msg.content: + output += msg.content + "\n" + + # Embeds + if (embeds := len(msg.embeds)) > 0: + output += f"<{embeds} embed{'s' if embeds > 1 else ''}>\n" + + # Attachments + attachments = "\n".join(a.url for a in msg.attachments) + if attachments: + output += attachments + "\n" + + paste_link = await send_to_paste_service(output, extension="txt") + await ctx.send(paste_link) + + +def setup(bot: Bot) -> None: + """Load the DMRelay cog.""" + bot.add_cog(DMRelay(bot)) -- cgit v1.2.3 From 4be90b3c454138e3548c7394fcb2a1182b05b7d7 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 27 Mar 2021 13:52:43 -0400 Subject: Restrict DMRelay cog to moderators only. --- bot/exts/moderation/dm_relay.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index 2bf2391a4..1d57862d9 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -2,10 +2,10 @@ import logging import textwrap import discord -from discord.ext.commands import Cog, Context, command +from discord.ext.commands import Cog, Context, command, has_any_role from bot.bot import Bot -from bot.constants import Emojis +from bot.constants import Emojis, MODERATION_ROLES from bot.utils.services import send_to_paste_service log = logging.getLogger(__name__) @@ -53,6 +53,10 @@ class DMRelay(Cog): paste_link = await send_to_paste_service(output, extension="txt") await ctx.send(paste_link) + async def cog_check(self, ctx: Context) -> bool: + """Only allow moderators to invoke the commands in this cog.""" + return await has_any_role(*MODERATION_ROLES).predicate(ctx) + def setup(bot: Bot) -> None: """Load the DMRelay cog.""" -- cgit v1.2.3 From 459ce9220ab8659d545e32f4ef1532da50789ca7 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Sun, 28 Mar 2021 11:52:41 -0400 Subject: Added a newline to space out some code. Co-authored-by: Joe Banks <20439493+jb3@users.noreply.github.com> --- bot/utils/services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/utils/services.py b/bot/utils/services.py index 68cbd896e..db9c93d0f 100644 --- a/bot/utils/services.py +++ b/bot/utils/services.py @@ -52,6 +52,7 @@ async def send_to_paste_service(contents: str, *, extension: str = "") -> Option if extension == '.py': return paste_link + return paste_link + "?noredirect" log.warning( -- cgit v1.2.3 From 8cc27e52735d03273267012c0344acc54c602ea9 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Sun, 28 Mar 2021 11:53:41 -0400 Subject: Improve `DMRelay` cog description. Co-authored-by: Joe Banks <20439493+jb3@users.noreply.github.com> --- bot/exts/moderation/dm_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index 1d57862d9..2b897c0ed 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) class DMRelay(Cog): - """Relay direct messages from the bot.""" + """Inspect messages sent to the bot.""" def __init__(self, bot: Bot): self.bot = bot -- cgit v1.2.3 From 14514d1ed15e9155ef8700473cd0953126421177 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sun, 28 Mar 2021 11:58:01 -0400 Subject: Account for requesting the bot's DMs with itself. --- bot/exts/moderation/dm_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index 1d57862d9..ed1c45292 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -22,7 +22,7 @@ class DMRelay(Cog): """Relays the direct message history between the bot and given user.""" log.trace(f"Relaying DMs with {user.name} ({user.id})") - if not user.dm_channel: + if self.bot.user == user or not user.dm_channel: await ctx.send(f"{Emojis.cross_mark} No direct message history with {user.mention}.") return -- cgit v1.2.3 From d36e179912242ea6c21a1d5e1a4485034a1b5343 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sun, 28 Mar 2021 12:12:41 -0400 Subject: Force cache to update for user history. Before, the user would have to send a DM to the bot after startup for the bot to realize there is DM history with that specific user. Now, we force a cache refresh when a moderator invokes `!dmrelay`, so this shouldn't be an issue anymore. --- bot/exts/moderation/dm_relay.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index ed1c45292..17316ff48 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -22,7 +22,14 @@ class DMRelay(Cog): """Relays the direct message history between the bot and given user.""" log.trace(f"Relaying DMs with {user.name} ({user.id})") - if self.bot.user == user or not user.dm_channel: + if self.bot.user == user: + await ctx.send(f"{Emojis.cross_mark} No direct message history with myself.") + return + + # Force cache to update + await user.history(limit=1).flatten() + + if not user.dm_channel: await ctx.send(f"{Emojis.cross_mark} No direct message history with {user.mention}.") return -- cgit v1.2.3 From 39d71b578b5f1cfaae2acd01743f8b7522e2c490 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sun, 28 Mar 2021 13:19:40 -0400 Subject: Reduce API calls in `!dmrelay`. --- bot/exts/moderation/dm_relay.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index cc63a80fe..a03230b3d 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -22,22 +22,11 @@ class DMRelay(Cog): """Relays the direct message history between the bot and given user.""" log.trace(f"Relaying DMs with {user.name} ({user.id})") - if self.bot.user == user: - await ctx.send(f"{Emojis.cross_mark} No direct message history with myself.") + if user.bot: + await ctx.send(f"{Emojis.cross_mark} No direct message history with bots.") return - # Force cache to update - await user.history(limit=1).flatten() - - if not user.dm_channel: - await ctx.send(f"{Emojis.cross_mark} No direct message history with {user.mention}.") - return - - output = textwrap.dedent(f"""\ - User: {user} ({user.id}) - Channel ID: {user.dm_channel.id}\n - """) - + output = "" async for msg in user.history(limit=limit, oldest_first=True): created_at = msg.created_at.strftime(r"%Y-%m-%d %H:%M") @@ -57,7 +46,16 @@ class DMRelay(Cog): if attachments: output += attachments + "\n" - paste_link = await send_to_paste_service(output, extension="txt") + if not output: + await ctx.send(f"{Emojis.cross_mark} No direct message history with {user.mention}.") + return + + metadata = textwrap.dedent(f"""\ + User: {user} ({user.id}) + Channel ID: {user.dm_channel.id}\n + """) + + paste_link = await send_to_paste_service(metadata + output, extension="txt") await ctx.send(paste_link) async def cog_check(self, ctx: Context) -> bool: -- cgit v1.2.3