44import time
55from collections import defaultdict
66from collections .abc import Callable , Iterable
7- from contextlib import suppress
87from datetime import datetime
98from itertools import takewhile
109from typing import Literal , TYPE_CHECKING
1918from bot .converters import Age , ISODateTime
2019from bot .exts .moderation .modlog import ModLog
2120from bot .log import get_logger
21+ from bot .utils .async_utils import AsyncExecutor
2222from bot .utils .channel import is_mod_channel
2323from bot .utils .messages import upload_log
2424from bot .utils .modlog import send_log_message
3333# Type alias for message lookup ranges.
3434CleanLimit = Message | Age | ISODateTime
3535
36+ # How many ongoing API requests a clean operation can have at the same time.
37+ CONCURRENT_REQUESTS_POOL = 10
38+
3639
3740class CleanChannels (Converter ):
3841 """A converter to turn the string into a list of channels to clean, or the literal `*` for all public channels."""
@@ -89,11 +92,11 @@ def mod_log(self) -> ModLog:
8992
9093 @staticmethod
9194 def _validate_input (
92- channels : CleanChannels | None ,
93- bots_only : bool ,
94- users : list [User ] | None ,
95- first_limit : CleanLimit | None ,
96- second_limit : CleanLimit | None ,
95+ channels : CleanChannels | None ,
96+ bots_only : bool ,
97+ users : list [User ] | None ,
98+ first_limit : CleanLimit | None ,
99+ second_limit : CleanLimit | None ,
97100 ) -> None :
98101 """Raise errors if an argument value or a combination of values is invalid."""
99102 if first_limit is None :
@@ -295,7 +298,9 @@ async def _delete_messages_individually(self, channel_messages: dict[TextChannel
295298 deleted .append (message )
296299 return deleted
297300
298- async def _delete_found (self , message_mappings : dict [TextChannel , list [Message ]]) -> list [Message ]:
301+ async def _delete_found (
302+ self , message_mappings : dict [TextChannel , list [Message ]], executor :AsyncExecutor
303+ ) -> list [Message ]:
299304 """
300305 Delete the detected messages.
301306
@@ -322,18 +327,19 @@ async def _delete_found(self, message_mappings: dict[TextChannel, list[Message]]
322327
323328 if len (to_delete ) == 100 :
324329 # Only up to 100 messages can be deleted in a bulk
325- await channel .delete_messages (to_delete )
330+ executor . submit ( channel .delete_messages (to_delete ) )
326331 deleted .extend (to_delete )
327- to_delete . clear ()
332+ to_delete = []
328333
329334 if not self .cleaning :
330335 return deleted
331336 if len (to_delete ) > 0 :
332337 # Deleting any leftover messages if there are any
333- with suppress (NotFound ):
334- await channel .delete_messages (to_delete )
338+ executor .submit (channel .delete_messages (to_delete ))
335339 deleted .extend (to_delete )
336340
341+ await executor .gather (return_exceptions = True )
342+
337343 if old_messages :
338344 if not self .cleaning :
339345 return deleted
@@ -418,9 +424,11 @@ async def _clean_messages(
418424 # Needs to be called after standardizing the input.
419425 predicate = self ._build_predicate (first_limit , second_limit , bots_only , users , regex )
420426
427+ executor = AsyncExecutor (limit = CONCURRENT_REQUESTS_POOL )
428+
421429 if attempt_delete_invocation :
422430 # Delete the invocation first
423- await self ._delete_invocation (ctx )
431+ executor . submit ( self ._delete_invocation (ctx ) )
424432
425433 if self ._use_cache (first_limit ):
426434 log .trace (f"Messages for cleaning by { ctx .author .id } will be searched in the cache." )
@@ -442,8 +450,9 @@ async def _clean_messages(
442450
443451 # Now let's delete the actual messages with purge.
444452 self .mod_log .ignore (Event .message_delete , * message_ids )
445- deleted_messages = await self ._delete_found (message_mappings )
453+ deleted_messages = await self ._delete_found (message_mappings , executor )
446454 self .cleaning = False
455+ log .trace ("Cleaning completed, wrapping up" )
447456
448457 if not channels :
449458 channels = deletion_channels
0 commit comments