Repository: python-discord/bot

Issue:
!purge should take an optional set of channels, for mass-purging multi-channel spam.
some people will spam several channels. Let's make all !purge commands default to current channel only, but optionally take a list of channels to purge.


Discussion:
An option to clear messages from all channels would also be helpful, so if somebody joins just to spam in a ton of channels we can easily clear it without having to specify all channels individually.
Only issue I foresee with multiple channels is that we need to fetch history from multiple channels or store a cache or the last X messages, though I agree it would be useful.

Resolving patch:
diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py
--- a/bot/cogs/clean.py
+++ b/bot/cogs/clean.py
@@ -1,16 +1,16 @@
 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
 from bot.cogs.moderation import ModLog
 from bot.constants import (
-    Channels, CleanMessages, Colours, Event,
-    Icons, MODERATION_ROLES, NEGATIVE_REPLIES
+    Channels, CleanMessages, Colours, Event, Icons, MODERATION_ROLES, NEGATIVE_REPLIES
 )
 from bot.decorators import with_role
 
@@ -41,10 +41,10 @@ async def _clean_messages(
         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,48 +110,39 @@ def predicate_regex(message: Message) -> bool:
             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]
+
+        # Delete the invocation first
+        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
-        invocation_deleted = False
-
-        # To account for the invocation message, we index `amount + 1` messages.
-        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
+        # 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):
 
-            # 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 at any point the cancel command is invoked, we should stop.
+                if not self.cleaning:
+                    return
 
-            # 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)
 
         self.cleaning = False
 
-        # We should ignore the ID's we stored, so we don't get mod-log spam.
+        # Now let's delete the actual messages with purge.
         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:
+            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!
@@ -163,8 +154,10 @@ def predicate_regex(message: Message) -> bool:
             return
 
         # Build the embed and send it
+        target_channels = ", ".join(channel.mention for channel in channels)
+
         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 +182,10 @@ async def clean_user(
         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 +193,10 @@ async def clean_all(
         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 +204,10 @@ async def clean_bots(
         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 +216,10 @@ async def clean_regex(
         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)


Test patch: