diff --git a/CHANGELOG.md b/CHANGELOG.md index 791df31a1f..a1a4edb7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#1916](https://github.com/Pycord-Development/pycord/pull/1916)) - Added the `@client.once()` decorator, which serves as a one-time event listener. ([#1940](https://github.com/Pycord-Development/pycord/pull/1940)) +- Added support for text-related features in `StageChannel` + ([#1936](https://github.com/Pycord-Development/pycord/pull/1936)) ### Fixed diff --git a/discord/abc.py b/discord/abc.py index 4ab10409c0..027ba96b18 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -77,6 +77,7 @@ DMChannel, GroupChannel, PartialMessageable, + StageChannel, TextChannel, VoiceChannel, ) @@ -97,7 +98,7 @@ from .user import ClientUser PartialMessageableChannel = Union[ - TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable + TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable ] MessageableChannel = Union[PartialMessageableChannel, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] @@ -1292,6 +1293,8 @@ class Messageable: The following implement this ABC: - :class:`~discord.TextChannel` + - :class:`~discord.VoiceChannel` + - :class:`~discord.StageChannel` - :class:`~discord.DMChannel` - :class:`~discord.GroupChannel` - :class:`~discord.User` diff --git a/discord/channel.py b/discord/channel.py index 2efca7be02..88ba717db5 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1358,6 +1358,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha "video_quality_mode", "last_message_id", "flags", + "nsfw", ) def __init__( @@ -1401,6 +1402,7 @@ def _update( self.bitrate: int = data.get("bitrate") self.user_limit: int = data.get("user_limit") self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) + self.nsfw: bool = data.get("nsfw", False) self._fill_overwrites(data) @property @@ -1510,11 +1512,8 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): .. versionadded:: 2.0 """ - __slots__ = "nsfw" - def _update(self, guild: Guild, data: VoiceChannelPayload): super()._update(guild, data) - self.nsfw: bool = data.get("nsfw", False) def __repr__(self) -> str: attrs = [ @@ -1944,7 +1943,7 @@ async def create_activity_invite( ) -class StageChannel(VocalGuildChannel): +class StageChannel(discord.abc.Messageable, VocalGuildChannel): """Represents a Discord guild stage channel. .. versionadded:: 1.7 @@ -1997,10 +1996,17 @@ class StageChannel(VocalGuildChannel): Extra features of the channel. .. 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.5 """ __slots__ = ("topic",) + def _update(self, guild: Guild, data: StageChannelPayload) -> None: + super()._update(guild, data) + self.topic = data.get("topic") + def __repr__(self) -> str: attrs = [ ("id", self.id), @@ -2016,10 +2022,6 @@ def __repr__(self) -> str: joined = " ".join("%s=%r" % t for t in attrs) return f"<{self.__class__.__name__} {joined}>" - def _update(self, guild: Guild, data: StageChannelPayload) -> None: - super()._update(guild, data) - self.topic = data.get("topic") - @property def requesting_to_speak(self) -> list[Member]: """A list of members who are requesting to speak in the stage channel.""" @@ -2053,6 +2055,263 @@ def listeners(self) -> list[Member]: member for member in self.members if member.voice and member.voice.suppress ] + async def _get_channel(self): + return self + + def is_nsfw(self) -> bool: + """Checks if the channel is NSFW.""" + return self.nsfw + + @property + def last_message(self) -> Message | None: + """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: str | None = 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: int | None = 100, + check: Callable[[Message], bool] = MISSING, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = False, + bulk: bool = True, + reason: str | None = 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. + + 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. + + Returns + ------- + List[:class:`.Message`] + The list of messages that were deleted. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + 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)') + """ + 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. + + Returns + ------- + List[:class:`Webhook`] + The webhooks for this channel. + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + """ + + 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: bytes | None = None, reason: str | None = 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. + + Returns + ------- + :class:`Webhook` + The created webhook. + + Raises + ------ + HTTPException + Creating the webhook failed. + Forbidden + You do not have permissions to create a 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 moderators(self) -> list[Member]: """A list of members who are moderating the stage channel. diff --git a/discord/message.py b/discord/message.py index e299113369..69c7a79753 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1798,6 +1798,8 @@ class PartialMessage(Hashable): - :meth:`TextChannel.get_partial_message` - :meth:`Thread.get_partial_message` - :meth:`DMChannel.get_partial_message` + - :meth:`VoiceChannel.get_partial_message` + - :meth:`StageChannel.get_partial_message` Note that this class is trimmed down and has no rich attributes. @@ -1819,7 +1821,7 @@ class PartialMessage(Hashable): Attributes ---------- - channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`] + channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`VoiceChannel`, :class:`StageChannel`] The channel associated with this partial message. id: :class:`int` The message ID. @@ -1844,6 +1846,7 @@ def __init__(self, *, channel: PartialMessageableChannel, id: int): if channel.type not in ( ChannelType.text, ChannelType.voice, + ChannelType.stage_voice, ChannelType.news, ChannelType.private, ChannelType.news_thread, @@ -1851,7 +1854,7 @@ def __init__(self, *, channel: PartialMessageableChannel, id: int): ChannelType.private_thread, ): raise TypeError( - "Expected TextChannel, VoiceChannel, DMChannel or Thread not" + "Expected TextChannel, VoiceChannel, StageChannel, DMChannel or Thread not" f" {type(channel)!r}" ) diff --git a/discord/state.py b/discord/state.py index 744f63cabf..90fd97ab53 100644 --- a/discord/state.py +++ b/discord/state.py @@ -672,8 +672,13 @@ 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, VoiceChannel, or Thread - if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread): + # we ensure that the channel is either a TextChannel, VoiceChannel, StageChannel, or Thread + if channel and channel.__class__ in ( + TextChannel, + VoiceChannel, + StageChannel, + Thread, + ): channel.last_message_id = message.id # type: ignore def parse_message_delete(self, data) -> None: