From 36520cd6f733b063fdd15bfff820a50cc036811b Mon Sep 17 00:00:00 2001 From: Jonathan Rubenstein Date: Mon, 20 Jan 2025 14:41:32 +0200 Subject: [PATCH] Add !archive message command This command takes up to two messages and archives any messages between them If provided with one message it archives that one Uses PartialMessage instead of Message for argument conversion, so you can archive deleted messages if you still have the link. This also means that without a full message link including Channel ID the command may archive incorrect messages from the current channel. Notices have been set up to inform about this. --- GearBot/Cogs/Moderation.py | 38 +++++++++++++++++++++++++++++++++++++ GearBot/Util/Archive.py | 4 ++-- GearBot/database/DBUtils.py | 8 +++++++- lang/en_US.json | 6 +++++- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/GearBot/Cogs/Moderation.py b/GearBot/Cogs/Moderation.py index 7af6f7dd..3ffd3445 100644 --- a/GearBot/Cogs/Moderation.py +++ b/GearBot/Cogs/Moderation.py @@ -1223,6 +1223,44 @@ async def channel(self, ctx, channel: typing.Union[disnake.TextChannel, disnake. else: await ctx.send(f"{Emoji.get_chat_emoji('NO')} {Translator.translate('archive_no_edit_logs', ctx)}") + @archive.command(aliases=['messages','range']) + async def message(self, ctx, first: disnake.PartialMessage, last: disnake.PartialMessage = None): + """archive_messages_help""" + if not await Configuration.get_var(ctx.guild.id, "MESSAGE_LOGS", "ENABLED"): + await MessageUtils.send_to(ctx, 'NO', 'archive_no_edit_logs') + return + permissions = first.channel.permissions_for(ctx.author) + if not permissions.read_messages: + await MessageUtils.send_to(ctx, 'NO', 'archive_leak_denied') + return + query = LoggedMessage.filter(server=ctx.guild.id, channel=first.channel.id) \ + .order_by("-messageid") \ + .prefetch_related("attachments") + if last is not None: + if first.channel.id != last.channel.id: + await MessageUtils.send_to(ctx, 'NO', 'archive_message_mismatched_channels', \ + first=first.channel.id, last=last.channel.id) + return + if last.id < first.id: + # Last is older than first, swap them + last, first = first, last + query = query.filter(messageid__gte=first.id, messageid__lte=last.id) + # COUNT to see if we are about to request more than 5000 rows + if await query.count() > 5000: + await MessageUtils.send_to(ctx, 'NO', 'archive_too_much') + return + # Even though we are not over 5000, let's be safe about it + query = query.limit(5000) + else: + # Not .first() because we still want to be an iterable + query = query.filter(messageid=first.id).limit(1) + await MessageUtils.send_to(ctx, 'SEARCH', 'searching_archives') + messages = await query + messages += DBUtils.get_messages_in_range(first.channel.id, first.id, last.id if last is not None else None) + await Archive.ship_messages(ctx, messages, "message", plural='yes' if last is not None else 'no') + if first.channel.id == ctx.channel.id and '/' not in ctx.message.content and '-' not in ctx.message.content: + await MessageUtils.send_to(ctx, 'WARNING', 'archive_message_no_channel_id') + @archive.command() async def user(self, ctx, user: DiscordUser, amount=100): """archive_user_help""" diff --git a/GearBot/Util/Archive.py b/GearBot/Util/Archive.py index c88902fc..7ef8e9bf 100644 --- a/GearBot/Util/Archive.py +++ b/GearBot/Util/Archive.py @@ -33,7 +33,7 @@ async def pack_messages(messages, guild_id): out += f"{timestamp} {message.server} - {message.channel} - {message.messageid} | {name} ({message.author}) | {message.content}{reply} | {(', '.join(Utils.assemble_attachment(message.channel, attachment.id, attachment.name) for attachment in message.attachments))}\r\n" return out -async def ship_messages(ctx, messages, t, filename="Message archive", filtered=False): +async def ship_messages(ctx, messages, t, filename="Message archive", filtered=False, **kwargs): addendum = "" if filtered: addendum = f"\n{Emoji.get_chat_emoji('WARNING')} {Translator.translate('archive_message_filtered', ctx)}" @@ -53,4 +53,4 @@ async def ship_messages(ctx, messages, t, filename="Message archive", filtered=F await ctx.send(f"{Emoji.get_chat_emoji('YES')} {Translator.translate('archived_count', ctx, count=len(messages))} {addendum}", file=disnake.File(fp=buffer, filename=f"{filename}.txt")) else: - await ctx.send(f"{Emoji.get_chat_emoji('WARNING')} {Translator.translate(f'archive_empty_{t}', ctx)} {addendum}") \ No newline at end of file + await ctx.send(f"{Emoji.get_chat_emoji('WARNING')} {Translator.translate(f'archive_empty_{t}', ctx, **kwargs)} {addendum}") diff --git a/GearBot/database/DBUtils.py b/GearBot/database/DBUtils.py index cf5a06ed..acff836e 100644 --- a/GearBot/database/DBUtils.py +++ b/GearBot/database/DBUtils.py @@ -103,8 +103,14 @@ async def do_flush(): raise e +def get_messages_in_range(channel_id, first_id, last_id=None): + if last_id is not None: + return [message for message in batch.values() if message.channel == channel_id and message.messageid >= first_id and message.messageid <= last_id] + else: + return [message for message in batch.values() if message.channel == channel_id and message.messageid == first_id] + def get_messages_for_channel(channel_id): return [message for message in batch.values() if message.channel == channel_id] def get_messages_for_user_in_guild(user_id, guild_id): - return [message for message in batch.values() if message.server == guild_id and message.author == user_id] \ No newline at end of file + return [message for message in batch.values() if message.server == guild_id and message.author == user_id] diff --git a/lang/en_US.json b/lang/en_US.json index b802e6eb..0f0459be 100644 --- a/lang/en_US.json +++ b/lang/en_US.json @@ -202,8 +202,11 @@ "purged_log": "Archived {count, plural, one {1 purged message} other {# purged messages}} from {channel}.", "archived_count": "Gear minions have returned with {count, plural, one {1 message} other {# messages}}.", "archive_empty_user": "My gear minions have returned from their quest to the archives with empty hands. It appears they didn't send any messages in the last 6 weeks in this server.", - "archive_empty_channel": "My gear minions have returned from their quest to the archives with empty hands. It appears nobody send any messages in that channel in the last 6 weeks.", + "archive_empty_message": "My gear minions have returned from their quest to the archives with empty hands. It appears the {plural, select, no {message is} yes {messages are}} older than 6 weeks, or {plural, select, no {it is} yes {they are}} in another channel. \n (Try using '`Copy Message Link`' or holding Shift when pressing '`Copy Message ID`')", + "archive_empty_channel": "My gear minions have returned from their quest to the archives with empty hands. It appears nobody sent any messages in that channel in the last 6 weeks.", "archive_denied_read_perms": "Trying to archive a channel you don't have access to? Sorry, leek denied.", + "archive_message_mismatched_channels": "Both messages must be in the same channel. Received messages from <#{first}> and <#{last}>.", + "archive_message_no_channel_id": "No channel information was provided, so our minions got what they could find. If these messages are incorrect, try including Channel IDs. \n (Use '`Copy Message Link`' or hold Shift when pressing '`Copy Message ID`')", "archive_no_subcommand": "Instructions unclear, search quest denied. Please read through `{prefix}help archive` and return with a new quest assignment when ready", "archive_no_edit_logs": "Please enable edit logs to be able to use archiving", "archive_too_much": "I get it, you like reading old conversations, good for you. But do you really need more than 5000 messages? You might want to try a good book instead.", @@ -804,6 +807,7 @@ "cf_help": "Base command to pull mod info from CurseForge, still WIP", "archive_help": "Base command for archiving, use the subcommands to actually make archives", "archive_channel_help": "Archive messages from a channel", + "archive_messages_help": "Archive a single message or a range of messages", "archive_user_help": "Archive messages from a user", "mban_help": "Bans multiple users with the same reason", "mwarn_help": "Warns multiple users with the same reason",