From 9786b17c4e7aa07bfb5ee620bf35c74437746cf2 Mon Sep 17 00:00:00 2001 From: BobDotCom <71356958+BobDotCom@users.noreply.github.com> Date: Tue, 5 Apr 2022 13:57:37 -0500 Subject: [PATCH 1/2] Text in voice --- discord/abc.py | 74 ++++++++++- discord/channel.py | 309 +++++++++++++++++++++++++++++++++++++-------- discord/message.py | 2 +- discord/state.py | 12 +- discord/threads.py | 80 +++--------- 5 files changed, 356 insertions(+), 121 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 2dc2602bd2..7dc1f60d7b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -27,6 +27,7 @@ import asyncio import copy +import time from typing import ( TYPE_CHECKING, Any, @@ -41,6 +42,7 @@ Union, overload, runtime_checkable, + Iterable, ) from . import utils @@ -79,6 +81,7 @@ GroupChannel, PartialMessageable, TextChannel, + VoiceChannel, ) from .client import Client from .embeds import Embed @@ -95,13 +98,82 @@ from .ui.view import View from .user import ClientUser - PartialMessageableChannel = Union[TextChannel, Thread, DMChannel, PartialMessageable] + PartialMessageableChannel = Union[TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable] MessageableChannel = Union[PartialMessageableChannel, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] MISSING = utils.MISSING +async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): + for m in messages: + await m.delete(reason=reason) + + +async def _purge_messages_helper( + channel: Union[TextChannel, Thread, VoiceChannel], + *, + limit: Optional[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, + reason: Optional[str] = None +) -> List[Message]: + if check is MISSING: + check = lambda m: True + + iterator = channel.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 + strategy = channel.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete, reason=reason) + 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(reason=reason) + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete, reason=reason) + + 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, reason=reason) + elif count == 1: + # delete a single message + await ret[-1].delete(reason=reason) + + return ret + + class _Undefined: def __repr__(self) -> str: return "see-below" diff --git a/discord/channel.py b/discord/channel.py index 704cffb58c..6626f151ee 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -97,11 +97,6 @@ from .webhook import Webhook -async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): - for m in messages: - await m.delete(reason=reason) - - class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord text channel. @@ -494,57 +489,17 @@ def is_me(m): List[:class:`.Message`] The list of messages that were deleted. """ - - if check is MISSING: - check = lambda m: True - - iterator = self.history( + return await discord.abc._purge_messages_helper( + self, limit=limit, + check=check, before=before, after=after, - oldest_first=oldest_first, around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason ) - ret: List[Message] = [] - count = 0 - - minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 - 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, reason=reason) - 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(reason=reason) - elif count >= 2: - to_delete = ret[-count:] - await strategy(to_delete, reason=reason) - - 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, reason=reason) - elif count == 1: - # delete a single message - await ret[-1].delete(reason=reason) - - return ret async def webhooks(self) -> List[Webhook]: """|coro| @@ -833,6 +788,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha "category_id", "rtc_region", "video_quality_mode", + 'last_message_id', ) def __init__( @@ -859,6 +815,7 @@ def _update(self, guild: Guild, data: Union[VoiceChannelPayload, StageChannelPay self.rtc_region: Optional[VoiceRegion] = try_enum(VoiceRegion, rtc) if rtc is not None else None self.video_quality_mode: VideoQualityMode = try_enum(VideoQualityMode, data.get("video_quality_mode", 1)) self.category_id: Optional[int] = utils._get_as_snowflake(data, "parent_id") + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') self.position: int = data["position"] self.bitrate: int = data.get("bitrate") self.user_limit: int = data.get("user_limit") @@ -914,7 +871,7 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: return base -class VoiceChannel(VocalGuildChannel): +class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): """Represents a Discord guild voice channel. .. container:: operations @@ -960,6 +917,9 @@ class VoiceChannel(VocalGuildChannel): video_quality_mode: :class:`VideoQualityMode` The camera video quality for the voice channel's participants. + .. versionadded:: 2.0 + last_message_id: Optional[:class:`int`] + The ID of the last message sent to this channel. It may not always point to an existing or valid message. .. versionadded:: 2.0 """ @@ -979,6 +939,251 @@ def __repr__(self) -> str: joined = " ".join("%s=%r" % t for t in attrs) return f"<{self.__class__.__name__} {joined}>" + async def _get_channel(self): + return self + + @property + def last_message(self) -> Optional[Message]: + """Fetches the last message from this channel in cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + Returns + --------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return self._state._get_message(self.last_message_id) if self.last_message_id else None + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ------------ + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + --------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + async def delete_messages(self, messages: Iterable[Snowflake], *, reason: Optional[str] = None) -> 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. + + Parameters + ----------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + 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: int = messages[0].id + await self._state.http.delete_message(self.id, message_id, reason=reason) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids, reason=reason) + + async def purge( + self, + *, + limit: Optional[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, + reason: Optional[str] = None, + ) -> 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. + 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 channel.purge(limit=100, check=is_me) + await channel.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. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + 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. + """ + return await discord.abc._purge_messages_helper( + self, + limit=limit, + check=check, + before=before, + after=after, + around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason + ) + + async def webhooks(self) -> List[Webhook]: + """|coro| + + Gets the list of webhooks from this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + Raises + ------- + Forbidden + You don't have permissions to get the webhooks. + + Returns + -------- + List[:class:`Webhook`] + The webhooks for this channel. + """ + + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: Optional[bytes] = None, reason: Optional[str] = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + .. versionchanged:: 1.1 + Added the ``reason`` keyword-only parameter. + + Parameters + ------------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Raises + ------- + HTTPException + Creating the webhook failed. + Forbidden + You do not have permissions to create a webhook. + + Returns + -------- + :class:`Webhook` + The created webhook. + """ + + from .webhook import Webhook + + if avatar is not None: + avatar = utils._bytes_to_base64_data(avatar) # type: ignore + + data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason) + return Webhook.from_state(data, state=self._state) + @property def type(self) -> ChannelType: """:class:`ChannelType`: The channel's Discord type.""" diff --git a/discord/message.py b/discord/message.py index e40e152b1f..5c992cdf5d 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1758,7 +1758,7 @@ def __init__(self, *, channel: PartialMessageableChannel, id: int): ChannelType.public_thread, ChannelType.private_thread, ): - raise TypeError(f"Expected TextChannel, DMChannel or Thread not {type(channel)!r}") + raise TypeError(f"Expected TextChannel, VoiceChannel, DMChannel or Thread not {type(channel)!r}") self.channel: PartialMessageableChannel = channel self._state: ConnectionState = channel._state diff --git a/discord/state.py b/discord/state.py index 0ca6504095..7fb713bdca 100644 --- a/discord/state.py +++ b/discord/state.py @@ -620,8 +620,8 @@ def parse_message_create(self, data) -> None: self.dispatch("message", message) if self._messages is not None: self._messages.append(message) - # we ensure that the channel is either a TextChannel or Thread - if channel and channel.__class__ in (TextChannel, Thread): + # we ensure that the channel is either a TextChannel, VoiceChannel, or Thread + if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread): channel.last_message_id = message.id # type: ignore def parse_message_delete(self, data) -> None: @@ -1655,10 +1655,10 @@ def get_channel(self, id: Optional[int]) -> Optional[Union[Channel, Thread]]: return channel def create_message( - self, - *, - channel: Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable], - data: MessagePayload, + self, + *, + channel: MessageableChannel, + data: MessagePayload, ) -> Message: return Message(state=self, channel=channel, data=data) diff --git a/discord/threads.py b/discord/threads.py index 4b4381582b..9aff03e6c1 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -29,7 +29,7 @@ import time from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Union -from .abc import Messageable +from .abc import Messageable, _purge_messages_helper from .enums import ChannelType, try_enum from .errors import ClientException from .mixins import Hashable @@ -404,15 +404,16 @@ async def delete_messages(self, messages: Iterable[Snowflake]) -> None: await self._state.http.delete_messages(self.id, message_ids) async def purge( - self, - *, - limit: Optional[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, + self, + *, + limit: Optional[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, + reason: Optional[str] = None, ) -> List[Message]: """|coro| @@ -456,6 +457,8 @@ def is_me(m): 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. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. Raises ------- @@ -469,62 +472,17 @@ def is_me(m): List[:class:`.Message`] The list of messages that were deleted. """ - - if check is MISSING: - check = lambda m: True - - iterator = self.history( + return await _purge_messages_helper( + self, limit=limit, + check=check, before=before, after=after, - oldest_first=oldest_first, around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason, ) - 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, From 6b6933e5c9c4c81da73927a4b274972b465cb67f Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Thu, 7 Apr 2022 16:26:09 +0300 Subject: [PATCH 2/2] Add trailing commas --- discord/abc.py | 2 +- discord/channel.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 7dc1f60d7b..a0e1f1744d 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -120,7 +120,7 @@ async def _purge_messages_helper( around: Optional[SnowflakeTime] = None, oldest_first: Optional[bool] = False, bulk: bool = True, - reason: Optional[str] = None + reason: Optional[str] = None, ) -> List[Message]: if check is MISSING: check = lambda m: True diff --git a/discord/channel.py b/discord/channel.py index 5f5d97047f..339a5a12ef 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -498,7 +498,7 @@ def is_me(m): around=around, oldest_first=oldest_first, bulk=bulk, - reason=reason + reason=reason, ) async def webhooks(self) -> List[Webhook]: @@ -1115,7 +1115,7 @@ def is_me(m): around=around, oldest_first=oldest_first, bulk=bulk, - reason=reason + reason=reason, ) async def webhooks(self) -> List[Webhook]: