From e7db0343a2613345c8995e59b80cbe4b223a1d75 Mon Sep 17 00:00:00 2001 From: arl Date: Sun, 26 Feb 2023 15:38:11 -0500 Subject: [PATCH] feat: implement text in stage (#942) Signed-off-by: arl Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com> --- changelog/942.bugfix.0.rst | 1 + changelog/942.feature.0.rst | 9 + changelog/942.feature.1.rst | 2 + changelog/942.feature.2.rst | 1 + changelog/942.feature.3.rst | 1 + disnake/abc.py | 3 +- disnake/channel.py | 397 +++++++++++++++++++++++++++++++++-- disnake/enums.py | 4 + disnake/ext/commands/core.py | 10 +- disnake/guild.py | 35 ++- disnake/interactions/base.py | 2 +- disnake/message.py | 20 +- disnake/state.py | 7 +- disnake/webhook/async_.py | 9 +- docs/api.rst | 31 ++- 15 files changed, 493 insertions(+), 39 deletions(-) create mode 100644 changelog/942.bugfix.0.rst create mode 100644 changelog/942.feature.0.rst create mode 100644 changelog/942.feature.1.rst create mode 100644 changelog/942.feature.2.rst create mode 100644 changelog/942.feature.3.rst diff --git a/changelog/942.bugfix.0.rst b/changelog/942.bugfix.0.rst new file mode 100644 index 0000000000..384dee6de5 --- /dev/null +++ b/changelog/942.bugfix.0.rst @@ -0,0 +1 @@ +Fix :meth:`.VoiceChannel.permissions_for` not disabling :attr:`Permissions.manage_webhooks` when the user cannot connect to the channel. diff --git a/changelog/942.feature.0.rst b/changelog/942.feature.0.rst new file mode 100644 index 0000000000..67903ee4ff --- /dev/null +++ b/changelog/942.feature.0.rst @@ -0,0 +1,9 @@ +Messages can now be sent within :class:`StageChannel` instances. +- :class:`StageChannel` now inherits from :class:`abc.Messageable` +- New :class:`StageChannel` properties: + :attr:`.nsfw `, :attr:`.slowmode_delay `, :attr:`.last_message_id `, :attr:`.last_message ` +- New :class:`StageChannel` methods: + :func:`.is_nsfw `, :func:`.get_partial_message `, :func:`.delete_messages `, :func:`.purge `, :func:`.webhooks `, :func:`.create_webhook ` +- Add ``nsfw`` and ``slowmode_delay`` parameters to :func:`Guild.create_stage_channel` and :func:`CategoryChannel.create_stage_channel` +- Add ``nsfw`` and ``slowmode_delay`` parameters to :func:`StageChannel.edit` +- Add text related permission support to :func:`StageChannel.permissions_for`. diff --git a/changelog/942.feature.1.rst b/changelog/942.feature.1.rst new file mode 100644 index 0000000000..46dda4fb33 --- /dev/null +++ b/changelog/942.feature.1.rst @@ -0,0 +1,2 @@ +New message types that are sent within :class:`StageChannel` instances: +- :attr:`MessageType.stage_start`, :attr:`MessageType.stage_end`, :attr:`MessageType.stage_speaker`, and :attr:`MessageType.stage_topic`. diff --git a/changelog/942.feature.2.rst b/changelog/942.feature.2.rst new file mode 100644 index 0000000000..34a93dde40 --- /dev/null +++ b/changelog/942.feature.2.rst @@ -0,0 +1 @@ +Add edit support for ``user_limit`` to :meth:`StageChannel.edit`. diff --git a/changelog/942.feature.3.rst b/changelog/942.feature.3.rst new file mode 100644 index 0000000000..ddba6addd0 --- /dev/null +++ b/changelog/942.feature.3.rst @@ -0,0 +1 @@ +Add support for setting ``user_limit`` and ``video_quality_mode`` when creating a :class:`StageChannel` with :func:`Guild.create_stage_channel`. diff --git a/disnake/abc.py b/disnake/abc.py index 8c5925dadc..5294b51462 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -1331,9 +1331,10 @@ class Messageable: - :class:`~disnake.GroupChannel` - :class:`~disnake.User` - :class:`~disnake.Member` - - :class:`~disnake.ext.commands.Context` - :class:`~disnake.Thread` - :class:`~disnake.VoiceChannel` + - :class:`~disnake.StageChannel` + - :class:`~disnake.ext.commands.Context` - :class:`~disnake.PartialMessageable` """ diff --git a/disnake/channel.py b/disnake/channel.py index c27d1326d7..8cb2047cda 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -1276,7 +1276,13 @@ def permissions_for( denied = Permissions.voice() # voice channels also deny all text related permissions denied.value |= Permissions.text().value - denied.update(manage_channels=True, manage_roles=True) + + denied.update( + manage_channels=True, + manage_roles=True, + manage_events=True, + manage_webhooks=True, + ) base.value &= ~denied.value return base @@ -1360,13 +1366,13 @@ async def edit( Parameters ---------- name: :class:`str` - The new channel's name. + The channel's new name. bitrate: :class:`int` - The new channel's bitrate. + The channel's new bitrate. user_limit: :class:`int` - The new channel's user limit. + The channel's new user limit. position: :class:`int` - The new channel's position. + The channel's new position. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -1674,7 +1680,7 @@ async def create_webhook( return Webhook.from_state(data, state=self._state) -class StageChannel(VocalGuildChannel): +class StageChannel(disnake.abc.Messageable, VocalGuildChannel): """Represents a Discord guild stage channel. .. versionadded:: 1.7 @@ -1727,9 +1733,36 @@ class StageChannel(VocalGuildChannel): The camera video quality for the stage channel's participants. .. versionadded:: 2.0 + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + + .. versionadded:: 2.9 + + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + .. versionadded:: 2.9 + + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + + .. versionadded:: 2.9 """ - __slots__ = ("topic",) + __slots__ = ( + "topic", + "nsfw", + "slowmode_delay", + "last_message_id", + ) def __repr__(self) -> str: attrs = ( @@ -1742,6 +1775,7 @@ def __repr__(self) -> str: ("video_quality_mode", self.video_quality_mode), ("user_limit", self.user_limit), ("category_id", self.category_id), + ("nsfw", self.nsfw), ("flags", self.flags), ) joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) @@ -1750,6 +1784,12 @@ def __repr__(self) -> str: def _update(self, guild: Guild, data: StageChannelPayload) -> None: super()._update(guild, data) self.topic: Optional[str] = data.get("topic") + self.nsfw: bool = data.get("nsfw", False) + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, "last_message_id") + + async def _get_channel(self): + return self @property def requesting_to_speak(self) -> List[Member]: @@ -1809,6 +1849,60 @@ async def clone( ) -> StageChannel: return await self._clone_impl({}, name=name, reason=reason) + def is_nsfw(self) -> bool: + """Whether the channel is marked as NSFW. + + .. versionadded:: 2.9 + + :return type: :class:`bool` + """ + return self.nsfw + + @property + def last_message(self) -> Optional[Message]: + """Gets the last message in this channel from the 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. + + .. versionadded:: 2.9 + + 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 given 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:: 2.9 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + @property def instance(self) -> Optional[StageInstance]: """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. @@ -1828,10 +1922,17 @@ def permissions_for( base = super().permissions_for(obj, ignore_timeout=ignore_timeout) # voice channels cannot be edited by people who can't connect to them - # It also implicitly denies all other voice perms + # It also implicitly denies all other channel permissions. if not base.connect: denied = Permissions.voice() - denied.update(manage_channels=True, manage_roles=True) + denied.value |= Permissions.text().value + denied.value |= Permissions.stage().value + denied.update( + manage_channels=True, + manage_roles=True, + manage_events=True, + manage_webhooks=True, + ) base.value &= ~denied.value return base @@ -1954,13 +2055,16 @@ async def edit( self, *, name: str = ..., + bitrate: int = ..., + user_limit: int = ..., position: int = ..., sync_permissions: bool = ..., category: Optional[Snowflake] = ..., overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., rtc_region: Optional[Union[str, VoiceRegion]] = ..., video_quality_mode: VideoQualityMode = ..., - bitrate: int = ..., + nsfw: bool = ..., + slowmode_delay: Optional[int] = ..., flags: ChannelFlags = ..., reason: Optional[str] = ..., ) -> StageChannel: @@ -1970,13 +2074,16 @@ async def edit( self, *, name: str = MISSING, + bitrate: int = MISSING, + user_limit: int = MISSING, position: int = MISSING, sync_permissions: bool = MISSING, category: Optional[Snowflake] = MISSING, overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - bitrate: int = MISSING, + nsfw: bool = MISSING, + slowmode_delay: Optional[int] = MISSING, flags: ChannelFlags = MISSING, reason: Optional[str] = None, **kwargs: Never, @@ -1997,12 +2104,26 @@ async def edit( .. versionchanged:: 2.6 Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + .. versionchanged:: 2.9 + The ``user_limit``, ``nsfw``, and ``slowmode_delay`` + keyword-only parameters were added. + Parameters ---------- name: :class:`str` - The new channel's name. + The channel's new name. + bitrate: :class:`int` + The channel's new bitrate. + + .. versionadded:: 2.6 + + user_limit: :class:`int` + The channel's new user limit. + + .. versionadded:: 2.9 + position: :class:`int` - The new channel's position. + The channel's new position. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -2018,12 +2139,18 @@ async def edit( video_quality_mode: :class:`VideoQualityMode` The camera video quality for the stage channel's participants. - .. versionadded:: 2.0 + .. versionadded:: 2.9 - bitrate: :class:`int` - The new channel's bitrate. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. - .. versionadded:: 2.6 + .. versionadded:: 2.9 + + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.9 flags: :class:`ChannelFlags` The new flags to set for this channel. This will overwrite any existing flags set on this channel. @@ -2052,14 +2179,17 @@ async def edit( """ payload = await self._edit( name=name, + bitrate=bitrate, position=position, + user_limit=user_limit, sync_permissions=sync_permissions, category=category, overwrites=overwrites, rtc_region=rtc_region, video_quality_mode=video_quality_mode, - bitrate=bitrate, + nsfw=nsfw, flags=flags, + slowmode_delay=slowmode_delay, reason=reason, **kwargs, ) @@ -2067,6 +2197,237 @@ async def edit( # the payload will always be the proper channel payload return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + 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. + + You must have :attr:`~Permissions.manage_messages` permission to + do this. + + .. versionadded:: 2.9 + + 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. + 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) + 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) + + 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, + ) -> 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 :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + .. versionadded:: 2.9 + + .. note:: + + See :meth:`TextChannel.purge` for examples. + + 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`] + A 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 + 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 webhooks(self) -> List[Webhook]: + """|coro| + + Retrieves the list of webhooks this channel has. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + use this. + + .. versionadded:: 2.9 + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + + Returns + ------- + List[:class:`Webhook`] + The list of webhooks this channel has. + """ + 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. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + do this. + + .. versionadded:: 2.9 + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + 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 + ------ + NotFound + The ``avatar`` asset couldn't be found. + Forbidden + You do not have permissions to create a webhook. + HTTPException + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + from .webhook import Webhook + + avatar_data = await utils._assetbytes_to_base64_data(avatar) + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar_data, reason=reason + ) + return Webhook.from_state(data, state=self._state) + class CategoryChannel(disnake.abc.GuildChannel, Hashable): """Represents a Discord channel category. diff --git a/disnake/enums.py b/disnake/enums.py index d6a587cd40..4158ce2837 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -240,6 +240,10 @@ class MessageType(Enum): auto_moderation_action = 24 role_subscription_purchase = 25 interaction_premium_upsell = 26 + stage_start = 27 + stage_end = 28 + stage_speaker = 29 + stage_topic = 31 guild_application_premium_subscription = 32 diff --git a/disnake/ext/commands/core.py b/disnake/ext/commands/core.py index 6bdd417c6a..8575d29bcd 100644 --- a/disnake/ext/commands/core.py +++ b/disnake/ext/commands/core.py @@ -2471,7 +2471,15 @@ def is_nsfw() -> Callable[[T], T]: def pred(ctx: AnyContext) -> bool: ch = ctx.channel if ctx.guild is None or ( - isinstance(ch, (disnake.TextChannel, disnake.VoiceChannel, disnake.Thread)) + isinstance( + ch, + ( + disnake.TextChannel, + disnake.VoiceChannel, + disnake.Thread, + disnake.StageChannel, + ), + ) and ch.is_nsfw() ): return True diff --git a/disnake/guild.py b/disnake/guild.py index d051eb8de1..6b8f588279 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -111,7 +111,7 @@ from .voice_client import VoiceProtocol from .webhook import Webhook - GuildMessageable = Union[TextChannel, Thread, VoiceChannel] + GuildMessageable = Union[TextChannel, Thread, VoiceChannel, StageChannel] GuildChannel = Union[VoiceChannel, StageChannel, TextChannel, CategoryChannel, ForumChannel] ByCategoryItem = Tuple[Optional[CategoryChannel], List[GuildChannel]] @@ -1346,7 +1346,6 @@ async def create_voice_channel( self, name: str, *, - reason: Optional[str] = None, category: Optional[CategoryChannel] = None, position: int = MISSING, bitrate: int = MISSING, @@ -1356,6 +1355,7 @@ async def create_voice_channel( nsfw: bool = MISSING, slowmode_delay: int = MISSING, overwrites: Dict[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, ) -> VoiceChannel: """|coro| @@ -1466,9 +1466,13 @@ async def create_stage_channel( topic: Optional[str] = MISSING, position: int = MISSING, bitrate: int = MISSING, + user_limit: int = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, overwrites: Dict[Union[Role, Member], PermissionOverwrite] = MISSING, category: Optional[CategoryChannel] = None, - rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + nsfw: bool = MISSING, + slowmode_delay: int = MISSING, reason: Optional[str] = None, ) -> StageChannel: """|coro| @@ -1512,6 +1516,19 @@ async def create_stage_channel( .. versionadded:: 2.5 + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + + .. versionadded:: 2.9 + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for users in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + If not provided, slowmode is disabled. + + .. versionadded:: 2.9 + + reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. @@ -1537,12 +1554,24 @@ async def create_stage_channel( if bitrate is not MISSING: options["bitrate"] = bitrate + if user_limit is not MISSING: + options["user_limit"] = user_limit + if position is not MISSING: options["position"] = position if rtc_region is not MISSING: options["rtc_region"] = None if rtc_region is None else str(rtc_region) + if video_quality_mode is not MISSING: + options["video_quality_mode"] = video_quality_mode.value + + if nsfw is not MISSING: + options["nsfw"] = nsfw + + if slowmode_delay is not MISSING: + options["rate_limit_per_user"] = slowmode_delay + data = await self._create_channel( name, overwrites=overwrites, diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 68b87c6cb0..8aca068c86 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -1420,7 +1420,7 @@ class InteractionMessage(Message): The actual contents of the message. embeds: List[:class:`Embed`] A list of embeds the message has. - channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] + channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] The channel that the message was sent from. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~disnake.MessageReference`] diff --git a/disnake/message.py b/disnake/message.py index 97b337f904..b4f8438e66 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -731,7 +731,7 @@ class Message(Hashable): This is not stored long term within Discord's servers and is only used ephemerally. embeds: List[:class:`Embed`] A list of embeds the message has. - channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] + channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] The channel that the message was sent from. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. position: Optional[:class:`int`] @@ -1406,6 +1406,18 @@ def system_content(self) -> Optional[str]: if self.type is MessageType.interaction_premium_upsell: return self.content + if self.type is MessageType.stage_start: + return f"{self.author.name} started {self.content}" + + if self.type is MessageType.stage_end: + return f"{self.author.name} ended {self.content}" + + if self.type is MessageType.stage_speaker: + return f"{self.author.name} is now a speaker." + + if self.type is MessageType.stage_topic: + return f"{self.author.name} changed the Stage topic: {self.content}" + if self.type is MessageType.guild_application_premium_subscription: application_name = ( self.application["name"] @@ -2057,6 +2069,7 @@ class PartialMessage(Hashable): - :meth:`TextChannel.get_partial_message` - :meth:`VoiceChannel.get_partial_message` + - :meth:`StageChannel.get_partial_message` - :meth:`Thread.get_partial_message` - :meth:`DMChannel.get_partial_message` - :meth:`PartialMessageable.get_partial_message` @@ -2081,7 +2094,7 @@ class PartialMessage(Hashable): Attributes ---------- - channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`VoiceChannel`, :class:`PartialMessageable`] + channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`PartialMessageable`] The channel associated with this partial message. id: :class:`int` The message ID. @@ -2111,9 +2124,10 @@ def __init__(self, *, channel: MessageableChannel, id: int) -> None: ChannelType.public_thread, ChannelType.private_thread, ChannelType.voice, + ChannelType.stage_voice, ): raise TypeError( - f"Expected TextChannel, DMChannel, VoiceChannel, Thread, or PartialMessageable " + f"Expected TextChannel, VoiceChannel, DMChannel, StageChannel, Thread, or PartialMessageable " f"with a valid type, not {type(channel)!r} (type: {channel.type!r})" ) diff --git a/disnake/state.py b/disnake/state.py index d979608a7d..064c620e19 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -39,6 +39,7 @@ ForumChannel, GroupChannel, PartialMessageable, + StageChannel, TextChannel, VoiceChannel, _guild_channel_factory, @@ -760,7 +761,7 @@ def parse_message_create(self, data: gateway.MessageCreateEvent) -> None: if channel: # we ensure that the channel is a type that implements last_message_id - if channel.__class__ in (TextChannel, Thread, VoiceChannel): + if channel.__class__ in (TextChannel, Thread, VoiceChannel, StageChannel): channel.last_message_id = message.id # type: ignore # Essentially, messages *don't* count towards message_count, if: # - they're the thread starter message @@ -1906,7 +1907,7 @@ def parse_guild_audit_log_entry_create(self, data: gateway.AuditLogEntryCreate) def _get_reaction_user( self, channel: MessageableChannel, user_id: int ) -> Optional[Union[User, Member]]: - if isinstance(channel, (TextChannel, VoiceChannel, Thread)): + if isinstance(channel, (TextChannel, VoiceChannel, Thread, StageChannel)): return channel.guild.get_member(user_id) return self.get_user(user_id) @@ -2093,7 +2094,7 @@ def _update_guild_channel_references(self) -> None: if new_guild is not None and new_guild is not msg.guild: channel_id = msg.channel.id channel = new_guild._resolve_channel(channel_id) or Object(id=channel_id) - # channel will either be a TextChannel, VoiceChannel, Thread or Object + # channel will either be a TextChannel, VoiceChannel, Thread, StageChannel, or Object msg._rebind_cached_references(new_guild, channel) # type: ignore # these generally get deallocated once the voice reconnect times out diff --git a/disnake/webhook/async_.py b/disnake/webhook/async_.py index 80e6c91be5..1c64327415 100644 --- a/disnake/webhook/async_.py +++ b/disnake/webhook/async_.py @@ -55,7 +55,7 @@ from ..abc import Snowflake from ..asset import AssetBytes - from ..channel import ForumChannel, TextChannel, VoiceChannel + from ..channel import ForumChannel, StageChannel, TextChannel, VoiceChannel from ..embeds import Embed from ..file import File from ..guild import Guild @@ -975,8 +975,8 @@ def guild(self) -> Optional[Guild]: return self._state and self._state._get_guild(self.guild_id) @property - def channel(self) -> Optional[Union[TextChannel, VoiceChannel, ForumChannel]]: - """Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`ForumChannel`]]: The channel this webhook belongs to. + def channel(self) -> Optional[Union[TextChannel, VoiceChannel, ForumChannel, StageChannel]]: + """Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`ForumChannel`, :class:`StageChannel`]]: The channel this webhook belongs to. If this is a partial webhook, then this will always return ``None``. @@ -1013,7 +1013,8 @@ class Webhook(BaseWebhook): There are two main ways to use Webhooks. The first is through the ones received by the library such as :meth:`.Guild.webhooks`, :meth:`.TextChannel.webhooks`, - and :meth:`.VoiceChannel.webhooks`. The ones received by the library will + :meth:`.ForumChannel.webhooks`, :meth:`.VoiceChannel.webhooks`, + and :meth:`.StageChannel.webhooks`. The ones received by the library will automatically be bound using the library's internal HTTP session. The second form involves creating a webhook object manually using the diff --git a/docs/api.rst b/docs/api.rst index 8b635e0bce..8b3c519baf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1511,13 +1511,13 @@ This section documents events related to Discord chat messages. Called when someone begins typing a message. The ``channel`` parameter can be a :class:`abc.Messageable` instance, or a :class:`ForumChannel`. - If channel is an :class:`abc.Messageable` instance, it could be a :class:`TextChannel`, :class:`VoiceChannel`, :class:`GroupChannel`, + If channel is an :class:`abc.Messageable` instance, it could be a :class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`GroupChannel`, or :class:`DMChannel`. .. versionchanged:: 2.5 ``channel`` may be a type :class:`ForumChannel` - If the ``channel`` is a :class:`TextChannel`, :class:`ForumChannel`, or :class:`VoiceChannel` then the + If the ``channel`` is a :class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`, or :class:`StageChannel` then the ``user`` parameter is a :class:`Member`, otherwise it is a :class:`User`. If the ``channel`` is a :class:`DMChannel` and the user is not found in the internal user/member cache, @@ -1795,6 +1795,26 @@ of :class:`enum.Enum`. The system message for an application premium subscription upsell. .. versionadded:: 2.8 + .. attribute:: stage_start + + The system message denoting the stage has been started. + + .. versionadded:: 2.9 + .. attribute:: stage_end + + The system message denoting the stage has ended. + + .. versionadded:: 2.9 + .. attribute:: stage_speaker + + The system message denoting a user has become a speaker. + + .. versionadded:: 2.9 + .. attribute:: stage_topic + + The system message denoting the stage topic has been changed. + + .. versionadded:: 2.9 .. attribute:: guild_application_premium_subscription The system message denoting that a guild member has subscribed to an application. @@ -4505,7 +4525,8 @@ AuditLogDiff sending another message or creating another thread in the channel. See also :attr:`TextChannel.slowmode_delay`, :attr:`VoiceChannel.slowmode_delay`, - :attr:`ForumChannel.slowmode_delay` or :attr:`Thread.slowmode_delay`. + :attr:`StageChannel.slowmode_delay`, :attr:`ForumChannel.slowmode_delay`, + or :attr:`Thread.slowmode_delay`. :type: :class:`int` @@ -4540,7 +4561,7 @@ AuditLogDiff The voice channel's user limit. - See also :attr:`VoiceChannel.user_limit`. + See also :attr:`VoiceChannel.user_limit`, or :attr:`StageChannel.user_limit`. :type: :class:`int` @@ -4548,7 +4569,7 @@ AuditLogDiff Whether the channel is marked as "not safe for work". - See also :attr:`TextChannel.nsfw`, :attr:`VoiceChannel.nsfw` or :attr:`ForumChannel.nsfw`. + See also :attr:`TextChannel.nsfw`, :attr:`VoiceChannel.nsfw`, :attr:`StageChannel.nsfw`, or :attr:`ForumChannel.nsfw`. :type: :class:`bool`