diff --git a/discord/threads.py b/discord/threads.py index cf6d92aa8..4e8e1cc2d 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -23,12 +23,14 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from discord.types.threads import ThreadMember -from typing import Dict, Optional, TYPE_CHECKING +from typing import Callable, Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING +import time +import asyncio from .mixins import Hashable from .abc import Messageable from .enums import ChannelType, try_enum +from .errors import ClientException, NoMoreItems from .utils import MISSING, parse_time, _get_as_snowflake __all__ = ( @@ -47,7 +49,7 @@ if TYPE_CHECKING: from .channel import TextChannel from .member import Member from .message import Message - from .abc import Snowflake + from .abc import Snowflake, SnowflakeTime from .state import ConnectionState @@ -232,6 +234,175 @@ class Thread(Messageable, Hashable): """ return self._type is ChannelType.news_thread + async def delete_messages(self, messages: Iterable[Snowflake]) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days old. + + You must have the :attr:`~Permissions.manage_messages` permission to + use this. + + Usable only by bot accounts. + + Parameters + ----------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages or + you're not using a bot account. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id = messages[0].id + await self._state.http.delete_message(self.id, message_id) + return + + if len(messages) > 100: + raise ClientException('Can only bulk delete messages up to 100 messages') + + message_ids = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids) + + async def purge( + self, + *, + limit: int = 100, + check: Callable[[Message], bool] = MISSING, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + around: Optional[SnowflakeTime] = None, + oldest_first: Optional[bool] = False, + bulk: bool = True, + ) -> List[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have the :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own (unless you are a user + account). The :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + Examples + --------- + + Deleting bot's messages :: + + def is_me(m): + return m.author == client.user + + deleted = await thread.purge(limit=100, check=is_me) + await thread.send(f'Deleted {len(deleted)} message(s)') + + Parameters + ----------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + + Raises + ------- + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Returns + -------- + List[:class:`.Message`] + The list of messages that were deleted. + """ + + if check is MISSING: + check = lambda m: True + + iterator = self.history(limit=limit, before=before, after=after, oldest_first=oldest_first, around=around) + ret: List[Message] = [] + count = 0 + + minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 + + async def _single_delete_strategy(messages: Iterable[Message]): + for m in messages: + await m.delete() + + strategy = self.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete) + count = 0 + await asyncio.sleep(1) + + if not check(message): + continue + + if message.id < minimum_time: + # older than 14 days old + if count == 1: + await ret[-1].delete() + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete) + + count = 0 + strategy = _single_delete_strategy + + count += 1 + ret.append(message) + + # SOme messages remaining to poll + if count >= 2: + # more than 2 messages -> bulk delete + to_delete = ret[-count:] + await strategy(to_delete) + elif count == 1: + # delete a single message + await ret[-1].delete() + + return ret + async def edit( self, *, @@ -386,6 +557,7 @@ class Thread(Messageable, Hashable): def _pop_member(self, member_id: int) -> Optional[ThreadMember]: return self._members.pop(member_id, None) + class ThreadMember(Hashable): """Represents a Discord thread member.