From 795dea3c8030955736984cdab372595c4799f5e9 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 30 May 2020 22:36:34 +0200 Subject: Add multichannel !purge via commands.Greedy We can now pass in as many channel mentions as we want after any !purge command - for example `!purge all 5 #python-general #python-language` --- bot/cogs/clean.py | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index b5d9132cb..91e69ee89 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -1,9 +1,10 @@ import logging import random import re -from typing import Optional +from typing import Iterable, Optional from discord import Colour, Embed, Message, TextChannel, User +from discord.ext import commands from discord.ext.commands import Cog, Context, group from bot.bot import Bot @@ -41,10 +42,11 @@ class Clean(Cog): self, amount: int, ctx: Context, + channels: Iterable[TextChannel], bots_only: bool = False, user: User = None, regex: Optional[str] = None, - channel: Optional[TextChannel] = None + ) -> None: """A helper function that does the actual message cleaning.""" def predicate_bots_only(message: Message) -> bool: @@ -110,8 +112,8 @@ class Clean(Cog): predicate = None # Delete all messages # Default to using the invoking context's channel - if not channel: - channel = ctx.channel + if not channels: + channels = [ctx.channel] # Look through the history and retrieve message data messages = [] @@ -120,23 +122,24 @@ class Clean(Cog): invocation_deleted = False # To account for the invocation message, we index `amount + 1` messages. - async for message in channel.history(limit=amount + 1): + for channel in channels: + async for message in channel.history(limit=amount + 1): - # If at any point the cancel command is invoked, we should stop. - if not self.cleaning: - return + # If at any point the cancel command is invoked, we should stop. + if not self.cleaning: + return - # Always start by deleting the invocation - if not invocation_deleted: - self.mod_log.ignore(Event.message_delete, message.id) - await message.delete() - invocation_deleted = True - continue + # Always start by deleting the invocation + if not invocation_deleted: + self.mod_log.ignore(Event.message_delete, message.id) + await message.delete() + invocation_deleted = True + continue - # If the message passes predicate, let's save it. - if predicate is None or predicate(message): - message_ids.append(message.id) - messages.append(message) + # If the message passes predicate, let's save it. + if predicate is None or predicate(message): + message_ids.append(message.id) + messages.append(message) self.cleaning = False @@ -144,10 +147,11 @@ class Clean(Cog): self.mod_log.ignore(Event.message_delete, *message_ids) # Use bulk delete to actually do the cleaning. It's far faster. - await channel.purge( - limit=amount, - check=predicate - ) + for channel in channels: + await channel.purge( + limit=amount, + check=predicate + ) # Reverse the list to restore chronological order if messages: @@ -163,8 +167,12 @@ class Clean(Cog): return # Build the embed and send it + if len(channels) > 1: + target_channels = ", ".join([f"<#{channel.id}>" for channel in channels]) + else: + target_channels = f"<#{channels[0].id}>" message = ( - f"**{len(message_ids)}** messages deleted in <#{channel.id}> by **{ctx.author.name}**\n\n" + f"**{len(message_ids)}** messages deleted in {target_channels} by **{ctx.author.name}**\n\n" f"A log of the deleted messages can be found [here]({log_url})." ) @@ -189,10 +197,10 @@ class Clean(Cog): ctx: Context, user: User, amount: Optional[int] = 10, - channel: TextChannel = None + channels: commands.Greedy[TextChannel] = None ) -> None: """Delete messages posted by the provided user, stop cleaning after traversing `amount` messages.""" - await self._clean_messages(amount, ctx, user=user, channel=channel) + await self._clean_messages(amount, ctx, user=user, channels=channels) @clean_group.command(name="all", aliases=["everything"]) @with_role(*MODERATION_ROLES) @@ -200,10 +208,10 @@ class Clean(Cog): self, ctx: Context, amount: Optional[int] = 10, - channel: TextChannel = None + channels: commands.Greedy[TextChannel] = None ) -> None: """Delete all messages, regardless of poster, stop cleaning after traversing `amount` messages.""" - await self._clean_messages(amount, ctx, channel=channel) + await self._clean_messages(amount, ctx, channels=channels) @clean_group.command(name="bots", aliases=["bot"]) @with_role(*MODERATION_ROLES) @@ -211,10 +219,10 @@ class Clean(Cog): self, ctx: Context, amount: Optional[int] = 10, - channel: TextChannel = None + channels: commands.Greedy[TextChannel] = None ) -> None: """Delete all messages posted by a bot, stop cleaning after traversing `amount` messages.""" - await self._clean_messages(amount, ctx, bots_only=True, channel=channel) + await self._clean_messages(amount, ctx, bots_only=True, channels=channels) @clean_group.command(name="regex", aliases=["word", "expression"]) @with_role(*MODERATION_ROLES) @@ -223,10 +231,10 @@ class Clean(Cog): ctx: Context, regex: str, amount: Optional[int] = 10, - channel: TextChannel = None + channels: commands.Greedy[TextChannel] = None ) -> None: """Delete all messages that match a certain regex, stop cleaning after traversing `amount` messages.""" - await self._clean_messages(amount, ctx, regex=regex, channel=channel) + await self._clean_messages(amount, ctx, regex=regex, channels=channels) @clean_group.command(name="stop", aliases=["cancel", "abort"]) @with_role(*MODERATION_ROLES) -- cgit v1.2.3 From 5926f75c8d2d4b683139606bd0a39b07d28529e1 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 30 May 2020 22:43:22 +0200 Subject: Remove a completely unacceptable newline. --- bot/cogs/clean.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 91e69ee89..8b0b8ed05 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -46,7 +46,6 @@ class Clean(Cog): bots_only: bool = False, user: User = None, regex: Optional[str] = None, - ) -> None: """A helper function that does the actual message cleaning.""" def predicate_bots_only(message: Message) -> bool: -- cgit v1.2.3 From d7123487230d70b855c84fb5d99ec45f6bee6859 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 18:18:18 +0200 Subject: Better channel mentions Co-authored-by: Mark --- bot/cogs/clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 8b0b8ed05..571a5ced8 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -167,7 +167,7 @@ class Clean(Cog): # Build the embed and send it if len(channels) > 1: - target_channels = ", ".join([f"<#{channel.id}>" for channel in channels]) + target_channels = ", ".join(channel.mention for channel in channels) else: target_channels = f"<#{channels[0].id}>" message = ( -- cgit v1.2.3 From d637053eb19a6bf33e765b25b3dff9963d7b7735 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 18:19:36 +0200 Subject: Remove unnecessary conditional. Thanks @MarkKoz! --- bot/cogs/clean.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 571a5ced8..02216a4af 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -166,10 +166,8 @@ class Clean(Cog): return # Build the embed and send it - if len(channels) > 1: - target_channels = ", ".join(channel.mention for channel in channels) - else: - target_channels = f"<#{channels[0].id}>" + target_channels = ", ".join(channel.mention for channel in channels) + message = ( f"**{len(message_ids)}** messages deleted in {target_channels} by **{ctx.author.name}**\n\n" f"A log of the deleted messages can be found [here]({log_url})." -- cgit v1.2.3 From 0737b1a63ca359e88ef580143e8e4e6a879c482e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 18:34:36 +0200 Subject: Add a mod_log.ignore_all context manager. This new context manager makes it easier to make the mod_log ignore actions like message deletions. The only existing method is the `ignore()` method, which requires that you pass all the messages you want to ignore into it. This one just ignores everything inside its scope. This isn't the DRYest approach, but it's low-cost and improves the readability of clean.py quite a bit. Ideally we should go through and give modlog a proper cleanup, because it's kinda ugly right now. --- bot/cogs/moderation/modlog.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index 9d28030d9..b3ae8e215 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -3,6 +3,7 @@ import difflib import itertools import logging import typing as t +from contextlib import contextmanager from datetime import datetime from itertools import zip_longest @@ -40,6 +41,7 @@ class ModLog(Cog, name="ModLog"): def __init__(self, bot: Bot): self.bot = bot self._ignored = {event: [] for event in Event} + self._ignore_all = False self._cached_deletes = [] self._cached_edits = [] @@ -81,6 +83,15 @@ class ModLog(Cog, name="ModLog"): if item not in self._ignored[event]: self._ignored[event].append(item) + @contextmanager + def ignore_all(self) -> None: + """Ignore all events while inside this context scope.""" + self._ignore_all = True + try: + yield + finally: + self._ignore_all = False + async def send_log_message( self, icon_url: t.Optional[str], @@ -191,6 +202,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.guild_channel_update].remove(before.id) return + if self._ignore_all: + return + # Two channel updates are sent for a single edit: 1 for topic and 1 for category change. # TODO: remove once support is added for ignoring multiple occurrences for the same channel. help_categories = (Categories.help_available, Categories.help_dormant, Categories.help_in_use) @@ -386,6 +400,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_ban].remove(member.id) return + if self._ignore_all: + return + await self.send_log_message( Icons.user_ban, Colours.soft_red, "User banned", f"{member} (`{member.id}`)", @@ -426,6 +443,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_remove].remove(member.id) return + if self._ignore_all: + return + member_str = escape_markdown(str(member)) await self.send_log_message( Icons.sign_out, Colours.soft_red, @@ -444,6 +464,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_unban].remove(member.id) return + if self._ignore_all: + return + member_str = escape_markdown(str(member)) await self.send_log_message( Icons.user_unban, Colour.blurple(), @@ -462,6 +485,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_update].remove(before.id) return + if self._ignore_all: + return + diff = DeepDiff(before, after) changes = [] done = [] @@ -564,6 +590,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.message_delete].remove(message.id) return + if self._ignore_all: + return + if author.bot: return @@ -623,6 +652,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.message_delete].remove(event.message_id) return + if self._ignore_all: + return + channel = self.bot.get_channel(event.channel_id) if channel.category: @@ -797,6 +829,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.voice_state_update].remove(member.id) return + if self._ignore_all: + return + # Exclude all channel attributes except the name. diff = DeepDiff( before, -- cgit v1.2.3 From f344dd8a72024e05577a5aeba25e2f98501417af Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 18:37:56 +0200 Subject: Fix a bug with invocation deletion. This command was written to support only a single channel, and with the move to multi-channel purges, we need to rethink the way the invocation deletion happens. We may be invoking this command from a completely different channel, so we can't necessarily look inside the channels we're targeting for the invocation. So, we're solving this by just deleting the invocation by using ctx.message. We do this before we start iterating message history, and then we only need to iterate the number of messages that was passed into the command. A much cleaner approach, which solves the bug reported and identified by @MarkKoz. --- bot/cogs/clean.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 02216a4af..892c638b8 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -10,8 +10,7 @@ from discord.ext.commands import Cog, Context, group from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import ( - Channels, CleanMessages, Colours, Event, - Icons, MODERATION_ROLES, NEGATIVE_REPLIES + Channels, CleanMessages, Colours, Icons, MODERATION_ROLES, NEGATIVE_REPLIES ) from bot.decorators import with_role @@ -114,27 +113,23 @@ class Clean(Cog): if not channels: channels = [ctx.channel] + # Delete the invocation first + with self.mod_log.ignore_all(): + await ctx.message.delete() + # Look through the history and retrieve message data + # This is only done so we can create a log to upload. messages = [] message_ids = [] self.cleaning = True - invocation_deleted = False - # To account for the invocation message, we index `amount + 1` messages. for channel in channels: - async for message in channel.history(limit=amount + 1): + async for message in channel.history(limit=amount): # If at any point the cancel command is invoked, we should stop. if not self.cleaning: return - # Always start by deleting the invocation - if not invocation_deleted: - self.mod_log.ignore(Event.message_delete, message.id) - await message.delete() - invocation_deleted = True - continue - # If the message passes predicate, let's save it. if predicate is None or predicate(message): message_ids.append(message.id) @@ -142,15 +137,13 @@ class Clean(Cog): self.cleaning = False - # We should ignore the ID's we stored, so we don't get mod-log spam. - self.mod_log.ignore(Event.message_delete, *message_ids) - - # Use bulk delete to actually do the cleaning. It's far faster. - for channel in channels: - await channel.purge( - limit=amount, - check=predicate - ) + # Now let's delete the actual messages with purge. + with self.mod_log.ignore_all(): + for channel in channels: + await channel.purge( + limit=amount, + check=predicate + ) # Reverse the list to restore chronological order if messages: -- cgit v1.2.3 From 3139991c3dcf4ec981a49aefa3d3cd75eed93fd8 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 23:33:05 +0200 Subject: Revert "Add a mod_log.ignore_all context manager." This reverts commit 0737b1a6 This isn't gonna work, because async is a thing. --- bot/cogs/moderation/modlog.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index b3ae8e215..9d28030d9 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -3,7 +3,6 @@ import difflib import itertools import logging import typing as t -from contextlib import contextmanager from datetime import datetime from itertools import zip_longest @@ -41,7 +40,6 @@ class ModLog(Cog, name="ModLog"): def __init__(self, bot: Bot): self.bot = bot self._ignored = {event: [] for event in Event} - self._ignore_all = False self._cached_deletes = [] self._cached_edits = [] @@ -83,15 +81,6 @@ class ModLog(Cog, name="ModLog"): if item not in self._ignored[event]: self._ignored[event].append(item) - @contextmanager - def ignore_all(self) -> None: - """Ignore all events while inside this context scope.""" - self._ignore_all = True - try: - yield - finally: - self._ignore_all = False - async def send_log_message( self, icon_url: t.Optional[str], @@ -202,9 +191,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.guild_channel_update].remove(before.id) return - if self._ignore_all: - return - # Two channel updates are sent for a single edit: 1 for topic and 1 for category change. # TODO: remove once support is added for ignoring multiple occurrences for the same channel. help_categories = (Categories.help_available, Categories.help_dormant, Categories.help_in_use) @@ -400,9 +386,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_ban].remove(member.id) return - if self._ignore_all: - return - await self.send_log_message( Icons.user_ban, Colours.soft_red, "User banned", f"{member} (`{member.id}`)", @@ -443,9 +426,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_remove].remove(member.id) return - if self._ignore_all: - return - member_str = escape_markdown(str(member)) await self.send_log_message( Icons.sign_out, Colours.soft_red, @@ -464,9 +444,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_unban].remove(member.id) return - if self._ignore_all: - return - member_str = escape_markdown(str(member)) await self.send_log_message( Icons.user_unban, Colour.blurple(), @@ -485,9 +462,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_update].remove(before.id) return - if self._ignore_all: - return - diff = DeepDiff(before, after) changes = [] done = [] @@ -590,9 +564,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.message_delete].remove(message.id) return - if self._ignore_all: - return - if author.bot: return @@ -652,9 +623,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.message_delete].remove(event.message_id) return - if self._ignore_all: - return - channel = self.bot.get_channel(event.channel_id) if channel.category: @@ -829,9 +797,6 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.voice_state_update].remove(member.id) return - if self._ignore_all: - return - # Exclude all channel attributes except the name. diff = DeepDiff( before, -- cgit v1.2.3 From 345fda6b88fef50e9bc47298085a10d8acb4fdff Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 31 May 2020 23:36:28 +0200 Subject: Revert message ignore approach. We're removing the context manager due to async concerns, so we'll go back to the old approach again of ignoring specific messages and iterating history. --- bot/cogs/clean.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 892c638b8..b164cf232 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -10,7 +10,7 @@ from discord.ext.commands import Cog, Context, group from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import ( - Channels, CleanMessages, Colours, Icons, MODERATION_ROLES, NEGATIVE_REPLIES + Channels, CleanMessages, Colours, Event, Icons, MODERATION_ROLES, NEGATIVE_REPLIES ) from bot.decorators import with_role @@ -114,11 +114,10 @@ class Clean(Cog): channels = [ctx.channel] # Delete the invocation first - with self.mod_log.ignore_all(): - await ctx.message.delete() + self.mod_log.ignore(Event.message_delete, ctx.message.id) + await ctx.message.delete() # Look through the history and retrieve message data - # This is only done so we can create a log to upload. messages = [] message_ids = [] self.cleaning = True @@ -138,12 +137,12 @@ class Clean(Cog): self.cleaning = False # Now let's delete the actual messages with purge. - with self.mod_log.ignore_all(): - for channel in channels: - await channel.purge( - limit=amount, - check=predicate - ) + self.mod_log.ignore(Event.message_delete, *message_ids) + for channel in channels: + await channel.purge( + limit=amount, + check=predicate + ) # Reverse the list to restore chronological order if messages: -- cgit v1.2.3 From 196ce8a828a0fed7450cad1ee0bba25ef608214a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 31 May 2020 15:27:53 -0700 Subject: Use the messages returned by `purge` to upload message logs This ensures that only what was actually deleted will be uploaded. I managed to get a 400 response from our API when purging twice in quick succession. Searching the history manually for these messages is unreliable cause of some sort of race condition. --- bot/cogs/clean.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index b164cf232..368d91c85 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -117,11 +117,11 @@ class Clean(Cog): self.mod_log.ignore(Event.message_delete, ctx.message.id) await ctx.message.delete() - # Look through the history and retrieve message data messages = [] message_ids = [] self.cleaning = True + # Find the IDs of the messages to delete. IDs are needed in order to ignore mod log events. for channel in channels: async for message in channel.history(limit=amount): @@ -132,21 +132,17 @@ class Clean(Cog): # If the message passes predicate, let's save it. if predicate is None or predicate(message): message_ids.append(message.id) - messages.append(message) self.cleaning = False # Now let's delete the actual messages with purge. self.mod_log.ignore(Event.message_delete, *message_ids) for channel in channels: - await channel.purge( - limit=amount, - check=predicate - ) + messages += await channel.purge(limit=amount, check=predicate) # Reverse the list to restore chronological order if messages: - messages = list(reversed(messages)) + messages = reversed(messages) log_url = await self.mod_log.upload_log(messages, ctx.author.id) else: # Can't build an embed, nothing to clean! -- cgit v1.2.3