1+ import asyncio
12import contextlib
23import itertools
34import re
45import time
56from collections import defaultdict
6- from collections .abc import Callable , Iterable
7+ from collections .abc import Callable , Collection
78from datetime import datetime
89from itertools import takewhile
910from typing import Literal , TYPE_CHECKING
3738# How many ongoing API requests a clean operation can have at the same time.
3839CONCURRENT_REQUESTS_POOL = 10
3940
41+ BULK_DELETE_LIMIT = 100
42+
4043
4144class CleanChannels (Converter ):
4245 """A converter to turn the string into a list of channels to clean, or the literal `*` for all public channels."""
@@ -239,48 +242,66 @@ def _get_messages_from_cache(
239242 channels : set [TextChannel ],
240243 to_delete : Predicate ,
241244 lower_limit : datetime
242- ) -> tuple [ defaultdict [TextChannel , list ], list [ int ] ]:
245+ ) -> defaultdict [TextChannel , list ]:
243246 """Helper function for getting messages from the cache."""
244247 message_mappings = defaultdict (list )
245- message_ids = []
246248 for message in takewhile (lambda m : m .created_at > lower_limit , reversed (self .bot .cached_messages )):
247249 if not self .cleaning :
248250 # Cleaning was canceled
249- return message_mappings , message_ids
251+ return message_mappings
250252
251253 if message .channel in channels and to_delete (message ):
252254 message_mappings [message .channel ].append (message )
253- message_ids .append (message .id )
254255
255- return message_mappings , message_ids
256+ return message_mappings
256257
257- async def _get_messages_from_channels (
258+ async def _get_messages_from_channels_and_delete (
258259 self ,
259- channels : Iterable [TextChannel ],
260+ executor : AsyncExecutor ,
261+ * ,
262+ channels : Collection [TextChannel ],
260263 to_delete : Predicate ,
261264 after : datetime ,
262265 before : datetime | None = None
263- ) -> tuple [defaultdict [ TextChannel , list ] , list ]:
266+ ) -> tuple [list [ Message ], dict [ TextChannel , list [ Message ]] ]:
264267 """
265- Collect the messages for deletion by iterating over the histories of the appropriate channels.
268+ Collect and delete the messages for deletion by iterating over the histories of the appropriate channels.
266269
267270 The clean cog enforces an upper limit on message age through `_validate_input`.
268271 """
269- message_mappings = defaultdict ( list )
270- message_ids = []
272+ deleted = []
273+ old_messages = { channel : [] for channel in channels }
271274
272275 for channel in channels :
276+ messages_to_delete = []
277+ message_ids = []
273278 async for message in channel .history (limit = CleanMessages .message_limit , before = before , after = after ):
274279
275280 if not self .cleaning :
276- # Cleaning was canceled, return empty containers .
277- return defaultdict ( list ), []
281+ # Cleaning was canceled, return any messages already sent for deletion .
282+ return deleted , {}
278283
279284 if to_delete (message ):
280- message_mappings [message .channel ].append (message )
285+ if self .is_older_than_14d (message ):
286+ old_messages [channel ].append (message )
287+ continue
288+
289+ messages_to_delete .append (message )
281290 message_ids .append (message .id )
282291
283- return message_mappings , message_ids
292+ if len (message_ids ) == BULK_DELETE_LIMIT :
293+ self .mod_log .ignore (Event .message_delete , * message_ids )
294+ executor .submit (channel .delete_messages (messages_to_delete ))
295+ deleted .extend (messages_to_delete )
296+ messages_to_delete = []
297+ message_ids = []
298+
299+ if message_ids : # Remaining messages not deleted.
300+ self .mod_log .ignore (Event .message_delete , * message_ids )
301+ executor .submit (channel .delete_messages (messages_to_delete ))
302+ deleted .extend (messages_to_delete )
303+
304+ return deleted , old_messages
284305
285306 @staticmethod
286307 def is_older_than_14d (message : Message ) -> bool :
@@ -296,6 +317,9 @@ def is_older_than_14d(message: Message) -> bool:
296317
297318 async def _delete_messages_individually (self , channel_messages : dict [TextChannel , list [Message ]]) -> list [Message ]:
298319 """Delete each message unless cleaning is cancelled. Return the deleted messages."""
320+ message_ids = [m .id for messages in channel_messages .values () for m in messages ]
321+ self .mod_log .ignore (Event .message_delete , * message_ids )
322+
299323 deleted = []
300324 for messages in channel_messages .values ():
301325 for message in messages :
@@ -308,7 +332,7 @@ async def _delete_messages_individually(self, channel_messages: dict[TextChannel
308332 return deleted
309333
310334 async def _delete_bulk (
311- self , message_mappings : dict [TextChannel , list [Message ]], executor :AsyncExecutor
335+ self , messages_per_channel : dict [TextChannel , list [Message ]], executor : AsyncExecutor
312336 ) -> tuple [list [Message ], dict [TextChannel , list [Message ]]]:
313337 """
314338 Delete the detected messages.
@@ -320,11 +344,13 @@ async def _delete_bulk(
320344 If cleaning was cancelled in the middle, return messages already deleted.
321345 """
322346 deleted = []
323- old_messages = {}
324- for channel , messages in message_mappings .items ():
347+ old_messages = {channel : [] for channel in messages_per_channel }
348+
349+ for channel , messages in messages_per_channel .items ():
325350 to_delete = []
326351
327352 for current_index , message in enumerate (messages ):
353+ await asyncio .sleep (0 ) # Avoid hogging the event loop, and allow the clean to be cancelled.
328354 if not self .cleaning :
329355 # Means that the cleaning was canceled
330356 return deleted , {}
@@ -336,8 +362,8 @@ async def _delete_bulk(
336362
337363 to_delete .append (message )
338364
339- if len (to_delete ) == 100 :
340- # Only up to 100 messages can be deleted in a bulk
365+ if len (to_delete ) == BULK_DELETE_LIMIT :
366+ self . mod_log . ignore ( Event . message_delete , * ( message . id for message in to_delete ))
341367 executor .submit (channel .delete_messages (to_delete ))
342368 deleted .extend (to_delete )
343369 to_delete = []
@@ -346,11 +372,10 @@ async def _delete_bulk(
346372 return deleted , {}
347373 if len (to_delete ) > 0 :
348374 # Deleting any leftover messages if there are any
375+ self .mod_log .ignore (Event .message_delete , * (message .id for message in to_delete ))
349376 executor .submit (channel .delete_messages (to_delete ))
350377 deleted .extend (to_delete )
351378
352- await executor .gather (return_exceptions = True )
353-
354379 return deleted , old_messages
355380
356381 async def _modlog_cleaned_messages (
@@ -440,27 +465,23 @@ async def _clean_messages(
440465
441466 if self ._use_cache (second_limit ):
442467 log .trace (f"Messages for cleaning by { ctx .author .id } will be searched in the cache." )
443- message_mappings , message_ids = self ._get_messages_from_cache (
468+ messages_per_channel = self ._get_messages_from_cache (
444469 channels = deletion_channels , to_delete = predicate , lower_limit = first_limit
445470 )
446- self .mod_log .ignore (Event .message_delete , * message_ids )
447- deleted_messages , old_messages = await self ._delete_bulk (message_mappings , executor )
471+ deleted_messages , old_messages = await self ._delete_bulk (messages_per_channel , executor )
448472 second_limit = self ._earliest_cache_datetime ()
449473
450474 if self ._use_api (first_limit ):
451475 log .trace (f"Messages for cleaning by { ctx .author .id } will be searched in channel histories." )
452- message_mappings , message_ids = await self ._get_messages_from_channels (
453- channels = deletion_channels ,
454- to_delete = predicate ,
455- after = first_limit , # Remember first is the earlier datetime (the "older" time).
456- before = second_limit
476+ api_deleted_messages , api_old_messages = await self ._get_messages_from_channels_and_delete (
477+ executor , channels = deletion_channels , to_delete = predicate , after = first_limit , before = second_limit
457478 )
458- self .mod_log .ignore (Event .message_delete , * message_ids )
459- api_deleted_messages , api_old_messages = await self ._delete_bulk (message_mappings , executor )
460479 deleted_messages .extend (api_deleted_messages )
461480 for channel , messages in api_old_messages .items ():
462481 old_messages [channel ].extend (messages )
463482
483+ await executor .gather (return_exceptions = True ) # Ignore NotFound errors etc.
484+
464485 if not self .cleaning :
465486 # Means that the cleaning was canceled
466487 return None
0 commit comments