diff --git a/changelog/1050.feature.rst b/changelog/1050.feature.rst new file mode 100644 index 0000000000..7a93fee4a7 --- /dev/null +++ b/changelog/1050.feature.rst @@ -0,0 +1,7 @@ +- Add support for media channels. + - Add :class:`MediaChannel`. + - Unless otherwise noted, media channels behave just like forum channels. + - Add :attr:`ChannelType.media`. + - Add :attr:`CategoryChannel.media_channels`, :attr:`Guild.media_channels`. + - Add :attr:`CategoryChannel.create_media_channel`, :attr:`Guild.create_media_channel`. + - Add :attr:`ChannelFlags.hide_media_download_options`. diff --git a/disnake/abc.py b/disnake/abc.py index 55c9ee3f78..64608875df 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -234,6 +234,7 @@ class GuildChannel(ABC): - :class:`.CategoryChannel` - :class:`.StageChannel` - :class:`.ForumChannel` + - :class:`.MediaChannel` This ABC must also implement :class:`.abc.Snowflake`. diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py index 256aaa04dc..c5ec6caf79 100644 --- a/disnake/audit_logs.py +++ b/disnake/audit_logs.py @@ -176,18 +176,18 @@ def _transform_tag_id( return None # cyclic imports - from .channel import ForumChannel + from .channel import ThreadOnlyGuildChannel tag: Optional[ForumTag] = None tag_id = int(data) thread = entry.target # try thread parent first - if isinstance(thread, Thread) and isinstance(thread.parent, ForumChannel): + if isinstance(thread, Thread) and isinstance(thread.parent, ThreadOnlyGuildChannel): tag = thread.parent.get_tag(tag_id) else: - # if not found (possibly deleted thread), search all forum channels - for forum in entry.guild.forum_channels: - if tag := forum.get_tag(tag_id): + # if not found (possibly deleted thread), search all forum/media channels + for channel in entry.guild._channels.values(): + if isinstance(channel, ThreadOnlyGuildChannel) and (tag := channel.get_tag(tag_id)): break return tag or Object(id=tag_id) diff --git a/disnake/channel.py b/disnake/channel.py index 042611b563..779edd36ec 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -58,6 +58,7 @@ "NewsChannel", "ThreadWithMessage", "ForumChannel", + "MediaChannel", "GroupChannel", "PartialMessageable", ) @@ -82,6 +83,7 @@ DMChannel as DMChannelPayload, ForumChannel as ForumChannelPayload, GroupDMChannel as GroupChannelPayload, + MediaChannel as MediaChannelPayload, StageChannel as StageChannelPayload, TextChannel as TextChannelPayload, VoiceChannel as VoiceChannelPayload, @@ -3010,7 +3012,10 @@ def channels(self) -> List[GuildChannelType]: """ def comparator(channel): - return (not isinstance(channel, (TextChannel, ForumChannel)), channel.position) + return ( + not isinstance(channel, (TextChannel, ThreadOnlyGuildChannel)), + channel.position, + ) ret = [c for c in self.guild.channels if c.category_id == self.id] ret.sort(key=comparator) @@ -3066,6 +3071,20 @@ def forum_channels(self) -> List[ForumChannel]: ret.sort(key=lambda c: (c.position, c.id)) return ret + @property + def media_channels(self) -> List[MediaChannel]: + """List[:class:`MediaChannel`]: Returns the media channels that are under this category. + + .. versionadded:: 2.10 + """ + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, MediaChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + async def create_text_channel(self, name: str, **options: Any) -> TextChannel: """|coro| @@ -3126,6 +3145,22 @@ async def create_forum_channel(self, name: str, **options: Any) -> ForumChannel: raise TypeError("got an unexpected keyword argument 'category'") return await self.guild.create_forum_channel(name, category=self, **options) + async def create_media_channel(self, name: str, **options: Any) -> MediaChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_media_channel` to create a :class:`MediaChannel` in the category. + + .. versionadded:: 2.10 + + Returns + ------- + :class:`MediaChannel` + The newly created media channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_media_channel(name, category=self, **options) + class NewsChannel(TextChannel): """Represents a Discord news channel @@ -3141,88 +3176,7 @@ class ThreadWithMessage(NamedTuple): message: Message -class ForumChannel(disnake.abc.GuildChannel, Hashable): - """Represents a Discord Forum channel. - - .. versionadded:: 2.5 - - .. collapse:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the channel's hash. - - .. describe:: str(x) - - Returns the channel's name. - - Attributes - ---------- - id: :class:`int` - The channel's ID. - name: :class:`str` - The channel's name. - guild: :class:`Guild` - The guild the channel belongs to. - topic: Optional[:class:`str`] - The channel's topic. ``None`` if it isn't set. - category_id: Optional[:class:`int`] - The category channel ID this channel belongs to, if applicable. - position: :class:`int` - The position in the channel list. This is a number that starts at 0. e.g. the - top channel is position 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. - last_thread_id: Optional[:class:`int`] - The ID of the last created thread in this channel. It may - *not* point to an existing or valid thread. - default_auto_archive_duration: :class:`int` - The default auto archive duration in minutes for threads created in this channel. - slowmode_delay: :class:`int` - The number of seconds a member must wait between creating threads - 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. - - See also :attr:`default_thread_slowmode_delay`. - - default_thread_slowmode_delay: :class:`int` - The default number of seconds a member must wait between sending messages - in newly created threads 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.6 - - default_sort_order: Optional[:class:`ThreadSortOrder`] - The default sort order of threads in this channel. - Members will still be able to change this locally. - - .. versionadded:: 2.6 - - default_layout: :class:`ThreadLayout` - The default layout of threads in this channel. - Members will still be able to change this locally. - - .. versionadded:: 2.8 - """ - +class ThreadOnlyGuildChannel(disnake.abc.GuildChannel, Hashable): __slots__ = ( "id", "name", @@ -3237,7 +3191,6 @@ class ForumChannel(disnake.abc.GuildChannel, Hashable): "slowmode_delay", "default_thread_slowmode_delay", "default_sort_order", - "default_layout", "_available_tags", "_default_reaction_emoji_id", "_default_reaction_emoji_name", @@ -3246,7 +3199,13 @@ class ForumChannel(disnake.abc.GuildChannel, Hashable): "_overwrites", ) - def __init__(self, *, state: ConnectionState, guild: Guild, data: ForumChannelPayload) -> None: + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: Union[ForumChannelPayload, MediaChannelPayload], + ) -> None: self._state: ConnectionState = state self.id: int = int(data["id"]) self._type: int = data["type"] @@ -3266,7 +3225,7 @@ def __repr__(self) -> str: joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) return f"<{type(self).__name__} {joined}>" - def _update(self, guild: Guild, data: ForumChannelPayload) -> None: + def _update(self, guild: Guild, data: Union[ForumChannelPayload, MediaChannelPayload]) -> None: self.guild: Guild = guild # apparently this can be nullable in the case of a bad api deploy self.name: str = data.get("name") or "" @@ -3301,25 +3260,11 @@ def _update(self, guild: Guild, data: ForumChannelPayload) -> None: else None ) - self.default_layout: ThreadLayout = ( - try_enum(ThreadLayout, layout) - if (layout := data.get("default_forum_layout")) is not None - else ThreadLayout.not_set - ) - self._fill_overwrites(data) - async def _get_channel(self) -> ForumChannel: + async def _get_channel(self) -> Self: return self - @property - def type(self) -> Literal[ChannelType.forum]: - """:class:`ChannelType`: The channel's Discord type. - - This always returns :attr:`ChannelType.forum`. - """ - return ChannelType.forum - @property def _sorting_bucket(self) -> int: return ChannelType.text.value @@ -3335,7 +3280,7 @@ def permissions_for( base = super().permissions_for(obj, ignore_timeout=ignore_timeout) self._apply_implict_permissions(base) - # forum channels do not have voice related permissions + # thread-only channels do not have voice related permissions denied = Permissions.voice() base.value &= ~denied.value return base @@ -3405,15 +3350,15 @@ def last_thread(self) -> Optional[Thread]: @property def available_tags(self) -> List[ForumTag]: - """List[:class:`ForumTag`]: The available tags for threads in this forum channel. + """List[:class:`ForumTag`]: The available tags for threads in this channel. - To create/edit/delete tags, use :func:`~ForumChannel.edit`. + To create/edit/delete tags, use :func:`edit`. .. versionadded:: 2.6 """ return list(self._available_tags.values()) - # both of these are re-implemented due to forum channels not being messageables + # both of these are re-implemented due to thread-only channels not being messageables async def trigger_typing(self) -> None: """|coro| @@ -3428,758 +3373,1244 @@ async def trigger_typing(self) -> None: def typing(self) -> Typing: return Typing(self) - # if only these parameters are passed, `_move` is called and no channel will be returned + def get_thread(self, thread_id: int, /) -> Optional[Thread]: + """Returns a thread with the given ID. + + Parameters + ---------- + thread_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`Thread`] + The returned thread of ``None`` if not found. + """ + return self.guild.get_thread(thread_id) + @overload - async def edit( + async def create_thread( self, *, - position: int, - category: Optional[Snowflake] = ..., - sync_permissions: bool = ..., - reason: Optional[str] = ..., - ) -> None: + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embed: Embed = ..., + file: File = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: Components = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... - # only passing `sync_permissions` may or may not return a channel, - # depending on whether the channel is in a category @overload - async def edit( + async def create_thread( self, *, - sync_permissions: bool, - reason: Optional[str] = ..., - ) -> Optional[ForumChannel]: + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embed: Embed = ..., + files: List[File] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: Components = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... @overload - async def edit( + async def create_thread( self, *, - name: str = ..., - topic: Optional[str] = ..., - position: int = ..., - nsfw: bool = ..., - sync_permissions: bool = ..., - category: Optional[Snowflake] = ..., + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., slowmode_delay: Optional[int] = ..., - default_thread_slowmode_delay: Optional[int] = ..., - default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., - flags: ChannelFlags = ..., - require_tag: bool = ..., - available_tags: Sequence[ForumTag] = ..., - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., - default_sort_order: Optional[ThreadSortOrder] = ..., - default_layout: ThreadLayout = ..., - reason: Optional[str] = ..., - ) -> ForumChannel: + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embeds: List[Embed] = ..., + file: File = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: Components = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... - async def edit( + @overload + async def create_thread( self, *, - name: str = MISSING, - topic: Optional[str] = MISSING, - position: int = MISSING, - nsfw: bool = MISSING, - sync_permissions: bool = MISSING, - category: Optional[Snowflake] = MISSING, - slowmode_delay: Optional[int] = MISSING, - default_thread_slowmode_delay: Optional[int] = MISSING, - default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, - flags: ChannelFlags = MISSING, - require_tag: bool = MISSING, - available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, - default_sort_order: Optional[ThreadSortOrder] = MISSING, - default_layout: ThreadLayout = MISSING, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embeds: List[Embed] = ..., + files: List[File] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: Components = ..., reason: Optional[str] = None, - **kwargs: Never, - ) -> Optional[ForumChannel]: - """|coro| - - Edits the channel. + ) -> ThreadWithMessage: + ... - You must have :attr:`~Permissions.manage_channels` permission to - do this. + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = MISSING, + slowmode_delay: Optional[int] = MISSING, + applied_tags: Sequence[Snowflake] = MISSING, + content: str = MISSING, + embed: Embed = MISSING, + embeds: List[Embed] = MISSING, + file: File = MISSING, + files: List[File] = MISSING, + suppress_embeds: bool = MISSING, + flags: MessageFlags = MISSING, + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: View = MISSING, + components: Components[MessageUIComponent] = MISSING, + reason: Optional[str] = None, + ) -> ThreadWithMessage: + """|coro| + + Creates a thread in this channel. + + You must have the :attr:`~Permissions.create_forum_threads` permission to do this. + + At least one of ``content``, ``embed``/``embeds``, ``file``/``files``, + ``stickers``, ``components``, or ``view`` must be provided. .. versionchanged:: 2.6 Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + .. versionchanged:: 2.6 + The ``content`` parameter is no longer required. + Parameters ---------- name: :class:`str` - The channel's new name. - topic: Optional[:class:`str`] - The channel's new topic. - position: :class:`int` - The channel's new position. - nsfw: :class:`bool` - Whether to mark the channel as NSFW. - sync_permissions: :class:`bool` - Whether to sync permissions with the channel's new or pre-existing - category. Defaults to ``False``. - category: Optional[:class:`abc.Snowflake`] - The new category for this channel. Can be ``None`` to remove the - category. - slowmode_delay: Optional[:class:`int`] - Specifies the slowmode rate limit at which users can create - threads in this channel, in seconds. - A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. - default_thread_slowmode_delay: Optional[:class:`int`] - Specifies the slowmode rate limit at which users can send messages - in newly created threads in this channel, in seconds. - This does not apply retroactively to existing threads. - A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. - - .. versionadded:: 2.6 - - overwrites: :class:`Mapping` - A :class:`Mapping` of target (either a role or a member) to - :class:`PermissionOverwrite` to apply to the channel. - default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] - The new default auto archive duration in minutes for threads created in this channel. + The name of the thread. + auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] + The duration in minutes before the thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. Must be one of ``60``, ``1440``, ``4320``, or ``10080``. - flags: :class:`ChannelFlags` - The new flags to set for this channel. This will overwrite any existing flags set on this channel. - If parameter ``require_tag`` is provided, that will override the setting of :attr:`ChannelFlags.require_tag`. - - .. versionadded:: 2.6 - - require_tag: :class:`bool` - Whether all newly created threads are required to have a tag. - - .. versionadded:: 2.6 - - available_tags: Sequence[:class:`ForumTag`] - The new :class:`ForumTag`\\s available for threads in this channel. - Can be used to create new tags and edit/reorder/delete existing tags. - Maximum of 20. - - Note that this overwrites all tags, removing existing tags unless they're passed as well. - - See :class:`ForumTag` for examples regarding creating/editing tags. - - .. versionadded:: 2.6 - - default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] - The new default emoji shown for reacting to threads. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + If set to ``None`` or not provided, slowmode is inherited from the parent's + :attr:`default_thread_slowmode_delay`. + applied_tags: Sequence[:class:`abc.Snowflake`] + The tags to apply to the new thread. Maximum of 5. .. versionadded:: 2.6 - default_sort_order: Optional[:class:`ThreadSortOrder`] - The new default sort order of threads in this channel. - - .. versionadded:: 2.6 + content: :class:`str` + The content of the message to send. + embed: :class:`.Embed` + The rich embed for the content to send. This cannot be mixed with the + ``embeds`` parameter. + embeds: List[:class:`.Embed`] + A list of embeds to send with the content. Must be a maximum of 10. + This cannot be mixed with the ``embed`` parameter. + suppress_embeds: :class:`bool` + Whether to suppress embeds for the message. This hides + all the embeds from the UI if set to ``True``. + flags: :class:`MessageFlags` + The flags to set for this message. + Only :attr:`~MessageFlags.suppress_embeds` is supported. - default_layout: :class:`ThreadLayout` - The new default layout of threads in this channel. + If parameter ``suppress_embeds`` is provided, + that will override the setting of :attr:`MessageFlags.suppress_embeds`. - .. versionadded:: 2.8 + .. versionadded:: 2.9 + file: :class:`.File` + The file to upload. This cannot be mixed with the ``files`` parameter. + files: List[:class:`.File`] + A list of files to upload. Must be a maximum of 10. + This cannot be mixed with the ``file`` parameter. + stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]] + A list of stickers to upload. Must be a maximum of 3. + allowed_mentions: :class:`.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`.Client.allowed_mentions` + are used instead. + view: :class:`.ui.View` + A Discord UI View to add to the message. This cannot be mixed with ``components``. + components: |components_type| + A list of components to include in the message. This cannot be mixed with ``view``. reason: Optional[:class:`str`] - The reason for editing this channel. Shows up on the audit log. + The reason for creating the thread. Shows up on the audit log. Raises ------ Forbidden - You do not have permissions to edit the channel. + You do not have permissions to create a thread. HTTPException - Editing the channel failed. + Starting the thread failed. TypeError - The permission overwrite information is not in proper form. + Specified both ``file`` and ``files``, + or you specified both ``embed`` and ``embeds``, + or you specified both ``view`` and ``components``. + or you have passed an object that is not :class:`File` to ``file`` or ``files``. ValueError - The position is less than 0. + Specified more than 10 embeds, + or more than 10 files. Returns ------- - Optional[:class:`ForumChannel`] - The newly edited forum channel. If the edit was only positional - then ``None`` is returned instead. + Tuple[:class:`Thread`, :class:`Message`] + A :class:`~typing.NamedTuple` with the newly created thread and the message sent in it. + + These values can also be accessed through the ``thread`` and ``message`` fields. """ - if require_tag is not MISSING: - # create base flags if flags are provided, otherwise use the internal flags. - flags = ChannelFlags._from_value(self._flags if flags is MISSING else flags.value) - flags.require_tag = require_tag + from .message import Message + from .webhook.async_ import handle_message_parameters_dict - payload = await self._edit( - name=name, - topic=topic, - position=position, - nsfw=nsfw, - sync_permissions=sync_permissions, - category=category, - slowmode_delay=slowmode_delay, - default_thread_slowmode_delay=default_thread_slowmode_delay, - default_auto_archive_duration=default_auto_archive_duration, - overwrites=overwrites, + params = handle_message_parameters_dict( + content, + embed=embed, + embeds=embeds, + file=file, + files=files, + suppress_embeds=suppress_embeds, flags=flags, - available_tags=available_tags, - default_reaction=default_reaction, - default_sort_order=default_sort_order, - default_layout=default_layout, - reason=reason, - **kwargs, # type: ignore + view=view, + components=components, + allowed_mentions=allowed_mentions, + stickers=stickers, ) - if payload is not None: - # the payload will always be the proper channel payload - return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore - async def clone( - self, - *, - name: Optional[str] = None, - topic: Optional[str] = MISSING, - position: int = MISSING, - nsfw: bool = MISSING, - category: Optional[Snowflake] = MISSING, - slowmode_delay: Optional[int] = MISSING, - default_thread_slowmode_delay: Optional[int] = MISSING, - default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, - available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, - default_sort_order: Optional[ThreadSortOrder] = MISSING, - default_layout: ThreadLayout = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, - reason: Optional[str] = None, - ) -> ForumChannel: - """|coro| + if auto_archive_duration not in (MISSING, None): + auto_archive_duration = cast( + "ThreadArchiveDurationLiteral", try_enum_to_int(auto_archive_duration) + ) - Clones this channel. This creates a channel with the same properties - as this channel. + tag_ids = [t.id for t in applied_tags] if applied_tags else [] - You must have :attr:`.Permissions.manage_channels` permission to - do this. + if params.files and len(params.files) > 10: + raise ValueError("files parameter must be a list of up to 10 elements") + elif params.files and not all(isinstance(file, File) for file in params.files): + raise TypeError("files parameter must be a list of File") - .. versionchanged:: 2.9 - Added new ``topic``, ``position``, ``nsfw``, ``category``, ``slowmode_delay``, - ``default_thread_slowmode_delay``, ``default_auto_archive_duration``, - ``available_tags``, ``default_reaction``, ``default_sort_order`` - and ``overwrites`` keyword-only parameters. + channel_data = { + "name": name, + "auto_archive_duration": auto_archive_duration or self.default_auto_archive_duration, + "applied_tags": tag_ids, + } - .. versionchanged:: 2.10 - Added ``default_layout`` parameter. + if slowmode_delay not in (MISSING, None): + channel_data["rate_limit_per_user"] = slowmode_delay - .. note:: - The current :attr:`ForumChannel.flags` value won't be cloned. - This is a Discord limitation. + try: + data = await self._state.http.start_thread_in_forum_channel( + self.id, + **channel_data, + files=params.files, + reason=reason, + **params.payload, + ) + finally: + if params.files: + for f in params.files: + f.close() - Parameters + thread = Thread(guild=self.guild, data=data, state=self._state) + message = Message(channel=thread, data=data["message"], state=self._state) + + if view: + self._state.store_view(view, message.id) + + return ThreadWithMessage(thread, message) + + def archived_threads( + self, + *, + limit: Optional[int] = 50, + before: Optional[Union[Snowflake, datetime.datetime]] = None, + ) -> ArchivedThreadIterator: + """Returns an :class:`~disnake.AsyncIterator` that iterates over all archived threads in the channel. + + You must have :attr:`~Permissions.read_message_history` permission to use this. + + Parameters ---------- - name: Optional[:class:`str`] - The name of the new channel. If not provided, defaults to this channel's name. - topic: Optional[:class:`str`] - The topic of the new channel. If not provided, defaults to this channel's topic. - position: :class:`int` - The position of the new channel. If not provided, defaults to this channel's position. - nsfw: :class:`bool` - Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. - category: Optional[:class:`abc.Snowflake`] - The category where the new channel should be grouped. If not provided, defaults to this channel's category. - slowmode_delay: Optional[:class:`int`] - The slowmode delay of the new channel. If not provided, defaults to this channel's slowmode delay. - default_thread_slowmode_delay: Optional[:class:`int`] - The default thread slowmode delay of the new channel. If not provided, defaults to this channel's default thread slowmode delay. - default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] - The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. - available_tags: Sequence[:class:`ForumTag`] - The applicable tags of the new channel. If not provided, defaults to this channel's available tags. - default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] - The default reaction of the new channel. If not provided, defaults to this channel's default reaction. - default_sort_order: Optional[:class:`ThreadSortOrder`] - The default sort order of the new channel. If not provided, defaults to this channel's default sort order. - default_layout: :class:`ThreadLayout` - The default layout of threads in the new channel. If not provided, defaults to this channel's default layout. - overwrites: :class:`Mapping` - A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` - to apply to the channel. If not provided, defaults to this channel's overwrites. + limit: Optional[:class:`int`] + The number of threads to retrieve. + If ``None``, retrieves every archived thread in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve archived channels before the given date or ID. + + Raises + ------ + Forbidden + You do not have permissions to get archived threads. + HTTPException + The request to get the archived threads failed. + + Yields + ------ + :class:`Thread` + The archived threads. + """ + return ArchivedThreadIterator( + self.id, self.guild, limit=limit, joined=False, private=False, before=before + ) + + 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.6 + + 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.6 + + 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 cloning this channel. Shows up on the audit log. + 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 the proper permissions to create this channel. + You do not have permissions to create a webhook. HTTPException - Creating the channel failed. + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). Returns ------- - :class:`ForumChannel` - The channel that was created. + :class:`Webhook` + The newly created webhook. """ - default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING - if default_reaction is MISSING: - default_reaction = self.default_reaction + from .webhook import Webhook - if default_reaction is not None: - emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) - default_reaction_emoji_payload = { - "emoji_name": emoji_name, - "emoji_id": emoji_id, - } - else: - default_reaction_emoji_payload = None + avatar_data = await utils._assetbytes_to_base64_data(avatar) - return await self._clone_impl( - { - "topic": topic if topic is not MISSING else self.topic, - "position": position if position is not MISSING else self.position, - "nsfw": nsfw if nsfw is not MISSING else self.nsfw, - "rate_limit_per_user": ( - slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay - ), - "default_thread_rate_limit_per_user": ( - default_thread_slowmode_delay - if default_thread_slowmode_delay is not MISSING - else self.default_thread_slowmode_delay - ), - "default_auto_archive_duration": ( - try_enum_to_int(default_auto_archive_duration) - if default_auto_archive_duration is not MISSING - else self.default_auto_archive_duration - ), - "available_tags": ( - [tag.to_dict() for tag in available_tags] - if available_tags is not MISSING - else [tag.to_dict() for tag in self.available_tags] - ), - "default_reaction_emoji": default_reaction_emoji_payload, - "default_sort_order": ( - try_enum_to_int(default_sort_order) - if default_sort_order is not MISSING - else try_enum_to_int(self.default_sort_order) - ), - "default_forum_layout": ( - try_enum_to_int(default_layout) - if default_layout is not MISSING - else try_enum_to_int(self.default_layout) - ), - }, - name=name, - category=category, - reason=reason, - overwrites=overwrites, + 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) - def get_thread(self, thread_id: int, /) -> Optional[Thread]: - """Returns a thread with the given ID. + def get_tag(self, tag_id: int, /) -> Optional[ForumTag]: + """Returns a thread tag with the given ID. + + .. versionadded:: 2.6 Parameters ---------- - thread_id: :class:`int` + tag_id: :class:`int` The ID to search for. Returns ------- - Optional[:class:`Thread`] - The returned thread of ``None`` if not found. + Optional[:class:`ForumTag`] + The tag with the given ID, or ``None`` if not found. """ - return self.guild.get_thread(thread_id) + return self._available_tags.get(tag_id) - @overload - async def create_thread( - self, - *, - name: str, - auto_archive_duration: AnyThreadArchiveDuration = ..., - slowmode_delay: Optional[int] = ..., - applied_tags: Sequence[Snowflake] = ..., - content: str = ..., - embed: Embed = ..., - file: File = ..., - suppress_embeds: bool = ..., - flags: MessageFlags = ..., - stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., - allowed_mentions: AllowedMentions = ..., - view: View = ..., - components: Components = ..., - reason: Optional[str] = None, - ) -> ThreadWithMessage: - ... + def get_tag_by_name(self, name: str, /) -> Optional[ForumTag]: + """Returns a thread tag with the given name. - @overload - async def create_thread( - self, - *, - name: str, - auto_archive_duration: AnyThreadArchiveDuration = ..., - slowmode_delay: Optional[int] = ..., - applied_tags: Sequence[Snowflake] = ..., - content: str = ..., - embed: Embed = ..., - files: List[File] = ..., - suppress_embeds: bool = ..., - flags: MessageFlags = ..., - stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., - allowed_mentions: AllowedMentions = ..., - view: View = ..., - components: Components = ..., - reason: Optional[str] = None, - ) -> ThreadWithMessage: - ... + Tags can be uniquely identified based on the name, as tag names + in a channel must be unique. - @overload - async def create_thread( - self, - *, - name: str, - auto_archive_duration: AnyThreadArchiveDuration = ..., - slowmode_delay: Optional[int] = ..., - applied_tags: Sequence[Snowflake] = ..., - content: str = ..., - embeds: List[Embed] = ..., - file: File = ..., - suppress_embeds: bool = ..., - flags: MessageFlags = ..., - stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., - allowed_mentions: AllowedMentions = ..., - view: View = ..., - components: Components = ..., - reason: Optional[str] = None, - ) -> ThreadWithMessage: - ... + .. versionadded:: 2.6 - @overload - async def create_thread( - self, - *, - name: str, - auto_archive_duration: AnyThreadArchiveDuration = ..., - slowmode_delay: Optional[int] = ..., - applied_tags: Sequence[Snowflake] = ..., - content: str = ..., - embeds: List[Embed] = ..., - files: List[File] = ..., - suppress_embeds: bool = ..., - flags: MessageFlags = ..., - stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., - allowed_mentions: AllowedMentions = ..., - view: View = ..., - components: Components = ..., - reason: Optional[str] = None, - ) -> ThreadWithMessage: - ... + Parameters + ---------- + name: :class:`str` + The name to search for. - async def create_thread( - self, - *, - name: str, - auto_archive_duration: AnyThreadArchiveDuration = MISSING, - slowmode_delay: Optional[int] = MISSING, - applied_tags: Sequence[Snowflake] = MISSING, - content: str = MISSING, - embed: Embed = MISSING, - embeds: List[Embed] = MISSING, - file: File = MISSING, - files: List[File] = MISSING, - suppress_embeds: bool = MISSING, - flags: MessageFlags = MISSING, - stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING, - allowed_mentions: AllowedMentions = MISSING, - view: View = MISSING, - components: Components[MessageUIComponent] = MISSING, - reason: Optional[str] = None, - ) -> ThreadWithMessage: - """|coro| + Returns + ------- + Optional[:class:`ForumTag`] + The tag with the given name, or ``None`` if not found. + """ + return utils.get(self._available_tags.values(), name=name) - Creates a thread in this forum channel. - You must have the :attr:`~Permissions.create_forum_threads` permission to do this. +class ForumChannel(ThreadOnlyGuildChannel): + """Represents a Discord guild forum channel. - At least one of ``content``, ``embed``/``embeds``, ``file``/``files``, - ``stickers``, ``components``, or ``view`` must be provided. + .. versionadded:: 2.5 - .. versionchanged:: 2.6 - Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + .. collapse:: operations - .. versionchanged:: 2.6 - The ``content`` parameter is no longer required. + .. describe:: x == y - Parameters - ---------- - name: :class:`str` - The name of the thread. - auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] - The duration in minutes before the thread is automatically archived for inactivity. - If not provided, the channel's default auto archive duration is used. - Must be one of ``60``, ``1440``, ``4320``, or ``10080``. - slowmode_delay: Optional[:class:`int`] - Specifies the slowmode rate limit for users in this thread, in seconds. - A value of ``0`` disables slowmode. The maximum value possible is ``21600``. - If set to ``None`` or not provided, slowmode is inherited from the parent's - :attr:`~ForumChannel.default_thread_slowmode_delay`. - applied_tags: Sequence[:class:`abc.Snowflake`] - The tags to apply to the new thread. Maximum of 5. + Checks if two channels are equal. - .. versionadded:: 2.6 + .. describe:: x != y - content: :class:`str` - The content of the message to send. - embed: :class:`.Embed` - The rich embed for the content to send. This cannot be mixed with the - ``embeds`` parameter. - embeds: List[:class:`.Embed`] - A list of embeds to send with the content. Must be a maximum of 10. - This cannot be mixed with the ``embed`` parameter. - suppress_embeds: :class:`bool` - Whether to suppress embeds for the message. This hides - all the embeds from the UI if set to ``True``. - flags: :class:`MessageFlags` - The flags to set for this message. - Only :attr:`~MessageFlags.suppress_embeds` is supported. + Checks if two channels are not equal. - If parameter ``suppress_embeds`` is provided, - that will override the setting of :attr:`MessageFlags.suppress_embeds`. + .. describe:: hash(x) - .. versionadded:: 2.9 + Returns the channel's hash. - file: :class:`.File` - The file to upload. This cannot be mixed with the ``files`` parameter. - files: List[:class:`.File`] - A list of files to upload. Must be a maximum of 10. - This cannot be mixed with the ``file`` parameter. - stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]] - A list of stickers to upload. Must be a maximum of 3. - allowed_mentions: :class:`.AllowedMentions` - Controls the mentions being processed in this message. If this is - passed, then the object is merged with :attr:`.Client.allowed_mentions`. - The merging behaviour only overrides attributes that have been explicitly passed - to the object, otherwise it uses the attributes set in :attr:`.Client.allowed_mentions`. - If no object is passed at all then the defaults given by :attr:`.Client.allowed_mentions` - are used instead. - view: :class:`.ui.View` - A Discord UI View to add to the message. This cannot be mixed with ``components``. - components: |components_type| - A list of components to include in the message. This cannot be mixed with ``view``. - reason: Optional[:class:`str`] - The reason for creating the thread. Shows up on the audit log. + .. describe:: str(x) - Raises - ------ - Forbidden - You do not have permissions to create a thread. - HTTPException - Starting the thread failed. - TypeError - Specified both ``file`` and ``files``, - or you specified both ``embed`` and ``embeds``, - or you specified both ``view`` and ``components``. - or you have passed an object that is not :class:`File` to ``file`` or ``files``. - ValueError - Specified more than 10 embeds, - or more than 10 files. + Returns the channel's name. - Returns - ------- - Tuple[:class:`Thread`, :class:`Message`] - A :class:`~typing.NamedTuple` with the newly created thread and the message sent in it. + Attributes + ---------- + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". - These values can also be accessed through the ``thread`` and ``message`` fields. - """ - from .message import Message - from .webhook.async_ import handle_message_parameters_dict + .. note:: - params = handle_message_parameters_dict( - content, - embed=embed, - embeds=embeds, - file=file, - files=files, - suppress_embeds=suppress_embeds, - flags=flags, - view=view, - components=components, - allowed_mentions=allowed_mentions, - stickers=stickers, - ) + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + last_thread_id: Optional[:class:`int`] + The ID of the last created thread in this channel. It may + *not* point to an existing or valid thread. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between creating threads + in this channel. - if auto_archive_duration not in (MISSING, None): - auto_archive_duration = cast( - "ThreadArchiveDurationLiteral", try_enum_to_int(auto_archive_duration) - ) + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. - tag_ids = [t.id for t in applied_tags] if applied_tags else [] + See also :attr:`default_thread_slowmode_delay`. - if params.files and len(params.files) > 10: - raise ValueError("files parameter must be a list of up to 10 elements") - elif params.files and not all(isinstance(file, File) for file in params.files): - raise TypeError("files parameter must be a list of File") + default_thread_slowmode_delay: :class:`int` + The default number of seconds a member must wait between sending messages + in newly created threads in this channel. - channel_data = { - "name": name, - "auto_archive_duration": auto_archive_duration or self.default_auto_archive_duration, - "applied_tags": tag_ids, - } + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. - if slowmode_delay not in (MISSING, None): - channel_data["rate_limit_per_user"] = slowmode_delay + .. versionadded:: 2.6 - try: - data = await self._state.http.start_thread_in_forum_channel( - self.id, - **channel_data, - files=params.files, - reason=reason, - **params.payload, - ) - finally: - if params.files: - for f in params.files: - f.close() + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of threads in this channel. + Members will still be able to change this locally. - thread = Thread(guild=self.guild, data=data, state=self._state) - message = Message(channel=thread, data=data["message"], state=self._state) + .. versionadded:: 2.6 - if view: - self._state.store_view(view, message.id) + default_layout: :class:`ThreadLayout` + The default layout of threads in this channel. + Members will still be able to change this locally. - return ThreadWithMessage(thread, message) + .. versionadded:: 2.8 + """ - def archived_threads( - self, - *, - limit: Optional[int] = 50, - before: Optional[Union[Snowflake, datetime.datetime]] = None, - ) -> ArchivedThreadIterator: - """Returns an :class:`~disnake.AsyncIterator` that iterates over all archived threads in the channel. + __slots__ = ("default_layout",) - You must have :attr:`~Permissions.read_message_history` permission to use this. + def _update(self, guild: Guild, data: ForumChannelPayload) -> None: + super()._update(guild=guild, data=data) + self.default_layout: ThreadLayout = ( + try_enum(ThreadLayout, layout) + if (layout := data.get("default_forum_layout")) is not None + else ThreadLayout.not_set + ) - Parameters - ---------- - limit: Optional[:class:`int`] - The number of threads to retrieve. - If ``None``, retrieves every archived thread in the channel. Note, however, - that this would make it a slow operation. - before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] - Retrieve archived channels before the given date or ID. + @property + def type(self) -> Literal[ChannelType.forum]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.forum`. + """ + return ChannelType.forum + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: + ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[ForumChannel]: + ... + + @overload + async def edit( + self, + *, + name: str = ..., + topic: Optional[str] = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + slowmode_delay: Optional[int] = ..., + default_thread_slowmode_delay: Optional[int] = ..., + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + require_tag: bool = ..., + available_tags: Sequence[ForumTag] = ..., + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_sort_order: Optional[ThreadSortOrder] = ..., + default_layout: ThreadLayout = ..., + reason: Optional[str] = ..., + ) -> ForumChannel: + ... + + async def edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + require_tag: bool = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + default_layout: ThreadLayout = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[ForumChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The channel's new name. + topic: Optional[:class:`str`] + The channel's new topic. + position: :class:`int` + The channel's new position. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can create + threads in this channel, in seconds. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.6 + + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + If parameter ``require_tag`` is provided, that will override the setting of :attr:`ChannelFlags.require_tag`. + + .. versionadded:: 2.6 + + require_tag: :class:`bool` + Whether all newly created threads are required to have a tag. + + .. versionadded:: 2.6 + + available_tags: Sequence[:class:`ForumTag`] + The new :class:`ForumTag`\\s available for threads in this channel. + Can be used to create new tags and edit/reorder/delete existing tags. + Maximum of 20. + + Note that this overwrites all tags, removing existing tags unless they're passed as well. + + See :class:`ForumTag` for examples regarding creating/editing tags. + + .. versionadded:: 2.6 + + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The new default emoji shown for reacting to threads. + + .. versionadded:: 2.6 + + default_sort_order: Optional[:class:`ThreadSortOrder`] + The new default sort order of threads in this channel. + + .. versionadded:: 2.6 + + default_layout: :class:`ThreadLayout` + The new default layout of threads in this channel. + + .. versionadded:: 2.8 + + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. Raises ------ Forbidden - You do not have permissions to get archived threads. + You do not have permissions to edit the channel. HTTPException - The request to get the archived threads failed. + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. - Yields + Returns + ------- + Optional[:class:`ForumChannel`] + The newly edited forum channel. If the edit was only positional + then ``None`` is returned instead. + """ + if require_tag is not MISSING: + # create base flags if flags are provided, otherwise use the internal flags. + flags = ChannelFlags._from_value(self._flags if flags is MISSING else flags.value) + flags.require_tag = require_tag + + payload = await self._edit( + name=name, + topic=topic, + position=position, + nsfw=nsfw, + sync_permissions=sync_permissions, + category=category, + slowmode_delay=slowmode_delay, + default_thread_slowmode_delay=default_thread_slowmode_delay, + default_auto_archive_duration=default_auto_archive_duration, + overwrites=overwrites, + flags=flags, + available_tags=available_tags, + default_reaction=default_reaction, + default_sort_order=default_sort_order, + default_layout=default_layout, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def clone( + self, + *, + name: Optional[str] = None, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + default_layout: ThreadLayout = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> ForumChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added new ``topic``, ``position``, ``nsfw``, ``category``, ``slowmode_delay``, + ``default_thread_slowmode_delay``, ``default_auto_archive_duration``, + ``available_tags``, ``default_reaction``, ``default_sort_order`` + and ``overwrites`` keyword-only parameters. + + .. versionchanged:: 2.10 + Added ``default_layout`` parameter. + + .. note:: + The current :attr:`ForumChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + topic: Optional[:class:`str`] + The topic of the new channel. If not provided, defaults to this channel's topic. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: Optional[:class:`int`] + The slowmode delay of the new channel. If not provided, defaults to this channel's slowmode delay. + default_thread_slowmode_delay: Optional[:class:`int`] + The default thread slowmode delay of the new channel. If not provided, defaults to this channel's default thread slowmode delay. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. + available_tags: Sequence[:class:`ForumTag`] + The applicable tags of the new channel. If not provided, defaults to this channel's available tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The default reaction of the new channel. If not provided, defaults to this channel's default reaction. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of the new channel. If not provided, defaults to this channel's default sort order. + default_layout: :class:`ThreadLayout` + The default layout of threads in the new channel. If not provided, defaults to this channel's default layout. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises ------ - :class:`Thread` - The archived threads. + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`MediaChannel` + The channel that was created. """ - return ArchivedThreadIterator( - self.id, self.guild, limit=limit, joined=False, private=False, before=before + default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING + if default_reaction is MISSING: + default_reaction = self.default_reaction + + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + default_reaction_emoji_payload = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + else: + default_reaction_emoji_payload = None + + return await self._clone_impl( + { + "topic": topic if topic is not MISSING else self.topic, + "position": position if position is not MISSING else self.position, + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "default_thread_rate_limit_per_user": ( + default_thread_slowmode_delay + if default_thread_slowmode_delay is not MISSING + else self.default_thread_slowmode_delay + ), + "default_auto_archive_duration": ( + try_enum_to_int(default_auto_archive_duration) + if default_auto_archive_duration is not MISSING + else self.default_auto_archive_duration + ), + "available_tags": ( + [tag.to_dict() for tag in available_tags] + if available_tags is not MISSING + else [tag.to_dict() for tag in self.available_tags] + ), + "default_reaction_emoji": default_reaction_emoji_payload, + "default_sort_order": ( + try_enum_to_int(default_sort_order) + if default_sort_order is not MISSING + else try_enum_to_int(self.default_sort_order) + ), + "default_forum_layout": ( + try_enum_to_int(default_layout) + if default_layout is not MISSING + else try_enum_to_int(self.default_layout) + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, ) - async def webhooks(self) -> List[Webhook]: + +class MediaChannel(ThreadOnlyGuildChannel): + """Represents a Discord guild media channel. + + Media channels are very similar to forum channels - only threads can be created in them, + with only minor differences in functionality. + + .. versionadded:: 2.10 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 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. + last_thread_id: Optional[:class:`int`] + The ID of the last created thread in this channel. It may + *not* point to an existing or valid thread. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between creating threads + 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. + + See also :attr:`default_thread_slowmode_delay`. + + default_thread_slowmode_delay: :class:`int` + The default number of seconds a member must wait between sending messages + in newly created threads 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. + + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of threads in this channel. + Members will still be able to change this locally. + """ + + __slots__ = () + + @property + def type(self) -> Literal[ChannelType.media]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.media`. + """ + return ChannelType.media + + def hides_media_download_options(self) -> bool: + """Whether the channel hides the embedded media download options. + + This is a shortcut to :attr:`self.flags.hide_media_download_options `. + + :return type: :class:`bool` + """ + return self.flags.hide_media_download_options + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: + ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[MediaChannel]: + ... + + @overload + async def edit( + self, + *, + name: str = ..., + topic: Optional[str] = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + slowmode_delay: Optional[int] = ..., + default_thread_slowmode_delay: Optional[int] = ..., + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + require_tag: bool = ..., + available_tags: Sequence[ForumTag] = ..., + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_sort_order: Optional[ThreadSortOrder] = ..., + reason: Optional[str] = ..., + ) -> MediaChannel: + ... + + async def edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + require_tag: bool = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[MediaChannel]: """|coro| - Retrieves the list of webhooks this channel has. + Edits the channel. - You must have :attr:`~.Permissions.manage_webhooks` permission to - use this. + You must have :attr:`~Permissions.manage_channels` permission to + do this. - .. versionadded:: 2.6 + Parameters + ---------- + name: :class:`str` + The channel's new name. + topic: Optional[:class:`str`] + The channel's new topic. + position: :class:`int` + The channel's new position. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can create + threads in this channel, in seconds. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + If parameter ``require_tag`` is provided, that will override the setting of :attr:`ChannelFlags.require_tag`. + require_tag: :class:`bool` + Whether all newly created threads are required to have a tag. + available_tags: Sequence[:class:`ForumTag`] + The new :class:`ForumTag`\\s available for threads in this channel. + Can be used to create new tags and edit/reorder/delete existing tags. + Maximum of 20. + + Note that this overwrites all tags, removing existing tags unless they're passed as well. + + See :class:`ForumTag` for examples regarding creating/editing tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The new default emoji shown for reacting to threads. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The new default sort order of threads in this channel. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. Raises ------ Forbidden - You don't have permissions to get the webhooks. + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. Returns ------- - List[:class:`Webhook`] - The list of webhooks this channel has. + Optional[:class:`MediaChannel`] + The newly edited media channel. If the edit was only positional + then ``None`` is returned instead. """ - from .webhook import Webhook + if require_tag is not MISSING: + # create base flags if flags are provided, otherwise use the internal flags. + flags = ChannelFlags._from_value(self._flags if flags is MISSING else flags.value) + flags.require_tag = require_tag - data = await self._state.http.channel_webhooks(self.id) - return [Webhook.from_state(d, state=self._state) for d in data] + payload = await self._edit( + name=name, + topic=topic, + position=position, + nsfw=nsfw, + sync_permissions=sync_permissions, + category=category, + slowmode_delay=slowmode_delay, + default_thread_slowmode_delay=default_thread_slowmode_delay, + default_auto_archive_duration=default_auto_archive_duration, + overwrites=overwrites, + flags=flags, + available_tags=available_tags, + default_reaction=default_reaction, + default_sort_order=default_sort_order, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore - async def create_webhook( - self, *, name: str, avatar: Optional[bytes] = None, reason: Optional[str] = None - ) -> Webhook: + async def clone( + self, + *, + name: Optional[str] = None, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> MediaChannel: """|coro| - Creates a webhook for this channel. + Clones this channel. This creates a channel with the same properties + as this channel. - You must have :attr:`~.Permissions.manage_webhooks` permission to + You must have :attr:`.Permissions.manage_channels` permission to do this. - .. versionadded:: 2.6 + .. note:: + The current :attr:`MediaChannel.flags` value won't be cloned. + This is a Discord limitation. Parameters ---------- - name: :class:`str` - The webhook's name. - avatar: Optional[:class:`bytes`] - The webhook's default avatar. - This operates similarly to :meth:`~ClientUser.edit`. + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + topic: Optional[:class:`str`] + The topic of the new channel. If not provided, defaults to this channel's topic. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: Optional[:class:`int`] + The slowmode delay of the new channel. If not provided, defaults to this channel's slowmode delay. + default_thread_slowmode_delay: Optional[:class:`int`] + The default thread slowmode delay of the new channel. If not provided, defaults to this channel's default thread slowmode delay. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. + available_tags: Sequence[:class:`ForumTag`] + The applicable tags of the new channel. If not provided, defaults to this channel's available tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The default reaction of the new channel. If not provided, defaults to this channel's default reaction. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of the new channel. If not provided, defaults to this channel's default sort order. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. reason: Optional[:class:`str`] - The reason for creating this webhook. Shows up in the audit logs. + The reason for cloning this channel. Shows up on the audit log. Raises ------ - NotFound - The ``avatar`` asset couldn't be found. Forbidden - You do not have permissions to create a webhook. + You do not have the proper permissions to create this channel. 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) - - def get_tag(self, tag_id: int, /) -> Optional[ForumTag]: - """Returns a thread tag with the given ID. - - .. versionadded:: 2.6 - - Parameters - ---------- - tag_id: :class:`int` - The ID to search for. + Creating the channel failed. Returns ------- - Optional[:class:`ForumTag`] - The tag with the given ID, or ``None`` if not found. + :class:`MediaChannel` + The channel that was created. """ - return self._available_tags.get(tag_id) - - def get_tag_by_name(self, name: str, /) -> Optional[ForumTag]: - """Returns a thread tag with the given name. - - Tags can be uniquely identified based on the name, as tag names - in a forum channel must be unique. + default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING + if default_reaction is MISSING: + default_reaction = self.default_reaction - .. versionadded:: 2.6 + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + default_reaction_emoji_payload = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + else: + default_reaction_emoji_payload = None - Parameters - ---------- - name: :class:`str` - The name to search for. + if default_sort_order is MISSING: + default_sort_order = self.default_sort_order - Returns - ------- - Optional[:class:`ForumTag`] - The tag with the given name, or ``None`` if not found. - """ - return utils.get(self._available_tags.values(), name=name) + return await self._clone_impl( + { + "topic": topic if topic is not MISSING else self.topic, + "position": position if position is not MISSING else self.position, + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "default_thread_rate_limit_per_user": ( + default_thread_slowmode_delay + if default_thread_slowmode_delay is not MISSING + else self.default_thread_slowmode_delay + ), + "default_auto_archive_duration": ( + try_enum_to_int(default_auto_archive_duration) + if default_auto_archive_duration is not MISSING + else self.default_auto_archive_duration + ), + "available_tags": ( + [tag.to_dict() for tag in available_tags] + if available_tags is not MISSING + else [tag.to_dict() for tag in self.available_tags] + ), + "default_reaction_emoji": default_reaction_emoji_payload, + "default_sort_order": ( + try_enum_to_int(default_sort_order) if default_sort_order is not None else None + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) class DMChannel(disnake.abc.Messageable, Hashable): @@ -4572,6 +5003,8 @@ def _guild_channel_factory(channel_type: int): return StageChannel, value elif value is ChannelType.forum: return ForumChannel, value + elif value is ChannelType.media: + return MediaChannel, value else: return None, value @@ -4616,4 +5049,5 @@ def _channel_type_factory( Thread: [ChannelType.news_thread, ChannelType.public_thread, ChannelType.private_thread], StageChannel: [ChannelType.stage_voice], ForumChannel: [ChannelType.forum], + MediaChannel: [ChannelType.media], }.get(cls, []) diff --git a/disnake/context_managers.py b/disnake/context_managers.py index 64f409183c..120c75ffa9 100644 --- a/disnake/context_managers.py +++ b/disnake/context_managers.py @@ -11,7 +11,7 @@ from typing_extensions import Self from .abc import Messageable - from .channel import ForumChannel + from .channel import ThreadOnlyGuildChannel __all__ = ("Typing",) @@ -25,9 +25,9 @@ def _typing_done_callback(fut: asyncio.Future) -> None: class Typing: - def __init__(self, messageable: Union[Messageable, ForumChannel]) -> None: + def __init__(self, messageable: Union[Messageable, ThreadOnlyGuildChannel]) -> None: self.loop: asyncio.AbstractEventLoop = messageable._state.loop - self.messageable: Union[Messageable, ForumChannel] = messageable + self.messageable: Union[Messageable, ThreadOnlyGuildChannel] = messageable async def do_typing(self) -> None: try: diff --git a/disnake/enums.py b/disnake/enums.py index 066584aac2..155053173e 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -216,6 +216,7 @@ class ChannelType(Enum): stage_voice = 13 guild_directory = 14 forum = 15 + media = 16 def __str__(self) -> str: return self.name diff --git a/disnake/ext/commands/converter.py b/disnake/ext/commands/converter.py index 29672b2e54..422416fd33 100644 --- a/disnake/ext/commands/converter.py +++ b/disnake/ext/commands/converter.py @@ -71,6 +71,7 @@ "StageChannelConverter", "CategoryChannelConverter", "ForumChannelConverter", + "MediaChannelConverter", "ThreadConverter", "ColourConverter", "ColorConverter", @@ -631,6 +632,27 @@ async def convert(self, ctx: AnyContext, argument: str) -> disnake.ForumChannel: ) +class MediaChannelConverter(IDConverter[disnake.MediaChannel]): + """Converts to a :class:`~disnake.MediaChannel`. + + .. versionadded:: 2.10 + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.MediaChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "media_channels", disnake.MediaChannel + ) + + class ThreadConverter(IDConverter[disnake.Thread]): """Coverts to a :class:`~disnake.Thread`. @@ -1186,6 +1208,7 @@ def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool: disnake.PartialEmoji: PartialEmojiConverter, disnake.CategoryChannel: CategoryChannelConverter, disnake.ForumChannel: ForumChannelConverter, + disnake.MediaChannel: MediaChannelConverter, disnake.Thread: ThreadConverter, disnake.abc.GuildChannel: GuildChannelConverter, disnake.GuildSticker: GuildStickerConverter, diff --git a/disnake/flags.py b/disnake/flags.py index 5345fd3a27..8e319bda51 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -2057,14 +2057,20 @@ class ChannelFlags(BaseFlags): if TYPE_CHECKING: @_generated - def __init__(self, *, pinned: bool = ..., require_tag: bool = ...) -> None: + def __init__( + self, + *, + hide_media_download_options: bool = ..., + pinned: bool = ..., + require_tag: bool = ..., + ) -> None: ... @flag_value def pinned(self): """:class:`bool`: Returns ``True`` if the thread is pinned. - This only applies to channels of type :class:`Thread`. + This only applies to threads that are part of a :class:`ForumChannel` or :class:`MediaChannel`. """ return 1 << 1 @@ -2072,12 +2078,22 @@ def pinned(self): def require_tag(self): """:class:`bool`: Returns ``True`` if the channel requires all newly created threads to have a tag. - This only applies to channels of type :class:`ForumChannel`. + This only applies to channels of types :class:`ForumChannel` or :class:`MediaChannel`. .. versionadded:: 2.6 """ return 1 << 4 + @flag_value + def hide_media_download_options(self): + """:class:`bool`: Returns ``True`` if the channel hides the embedded media download options. + + This only applies to channels of type :class:`MediaChannel`. + + .. versionadded:: 2.10 + """ + return 1 << 15 + class AutoModKeywordPresets(ListBaseFlags): """Wraps up the pre-defined auto moderation keyword lists, provided by Discord. diff --git a/disnake/guild.py b/disnake/guild.py index 449f303c27..1d992ed15d 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -31,6 +31,7 @@ from .channel import ( CategoryChannel, ForumChannel, + MediaChannel, StageChannel, TextChannel, VoiceChannel, @@ -114,7 +115,9 @@ from .webhook import Webhook GuildMessageable = Union[TextChannel, Thread, VoiceChannel, StageChannel] - GuildChannel = Union[VoiceChannel, StageChannel, TextChannel, CategoryChannel, ForumChannel] + GuildChannel = Union[ + VoiceChannel, StageChannel, TextChannel, CategoryChannel, ForumChannel, MediaChannel + ] ByCategoryItem = Tuple[Optional[CategoryChannel], List[GuildChannel]] @@ -697,6 +700,18 @@ def forum_channels(self) -> List[ForumChannel]: r.sort(key=lambda c: (c.position, c.id)) return r + @property + def media_channels(self) -> List[MediaChannel]: + """List[:class:`MediaChannel`]: A list of media channels that belong to this guild. + + This is sorted by the position and are in UI order from top to bottom. + + .. versionadded:: 2.10 + """ + r = [ch for ch in self._channels.values() if isinstance(ch, MediaChannel)] + r.sort(key=lambda c: (c.position, c.id)) + return r + @property def me(self) -> Member: """:class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`. @@ -1763,6 +1778,132 @@ async def create_forum_channel( self._channels[channel.id] = channel return channel + async def create_media_channel( + self, + name: str, + *, + topic: Optional[str] = None, + category: Optional[CategoryChannel] = None, + position: int = MISSING, + slowmode_delay: int = MISSING, + default_thread_slowmode_delay: int = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = None, + nsfw: bool = MISSING, + overwrites: Dict[Union[Role, Member], PermissionOverwrite] = MISSING, + available_tags: Optional[Sequence[ForumTag]] = None, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = None, + default_sort_order: Optional[ThreadSortOrder] = None, + reason: Optional[str] = None, + ) -> MediaChannel: + """|coro| + + This is similar to :meth:`create_text_channel` except makes a :class:`MediaChannel` instead. + + .. versionadded:: 2.10 + + Parameters + ---------- + name: :class:`str` + The channel's name. + topic: Optional[:class:`str`] + The channel's topic. + category: Optional[:class:`CategoryChannel`] + The category to place the newly created channel under. + The permissions will be automatically synced to category if no + overwrites are provided. + position: :class:`int` + The position in the channel list. This is a number that starts + at 0. e.g. the top channel is position 0. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit at which users can create + threads in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + If not provided, slowmode is disabled. + default_thread_slowmode_delay: :class:`int` + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + A value of ``0`` disables slowmode by default. The maximum value possible is ``21600``. + If not provided, slowmode is disabled. + default_auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] + The default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + overwrites: Dict[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`] + A :class:`dict` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply upon creation of a channel. + Useful for creating secret channels. + available_tags: Optional[Sequence[:class:`ForumTag`]] + The tags available for threads in this channel. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The default emoji shown for reacting to threads. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of threads in this channel. + reason: Optional[:class:`str`] + The reason for creating this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + TypeError + The permission overwrite information is not in proper form. + + Returns + ------- + :class:`MediaChannel` + The channel that was just created. + """ + options = {} + if position is not MISSING: + options["position"] = position + + if topic is not MISSING: + options["topic"] = topic + + if slowmode_delay is not MISSING: + options["rate_limit_per_user"] = slowmode_delay + + if default_thread_slowmode_delay is not MISSING: + options["default_thread_rate_limit_per_user"] = default_thread_slowmode_delay + + if nsfw is not MISSING: + options["nsfw"] = nsfw + + if default_auto_archive_duration is not None: + options["default_auto_archive_duration"] = cast( + "ThreadArchiveDurationLiteral", try_enum_to_int(default_auto_archive_duration) + ) + + if available_tags is not None: + options["available_tags"] = [tag.to_dict() for tag in available_tags] + + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + options["default_reaction_emoji"] = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + + if default_sort_order is not None: + options["default_sort_order"] = try_enum_to_int(default_sort_order) + + data = await self._create_channel( + name, + overwrites=overwrites, + channel_type=ChannelType.media, + category=category, + reason=reason, + **options, + ) + channel = MediaChannel(state=self._state, guild=self, data=data) + + # temporarily add to the cache + self._channels[channel.id] = channel + return channel + async def create_category( self, name: str, diff --git a/disnake/message.py b/disnake/message.py index e3967e1160..1bfb62cd43 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -1385,7 +1385,7 @@ def system_content(self) -> Optional[str]: if ( self.channel.type is ChannelType.public_thread and (parent := getattr(self.channel, "parent", None)) - and parent.type is ChannelType.forum + and parent.type in (ChannelType.forum, ChannelType.media) ): return f"{self.author.name} changed the post title: **{self.content}**" return f"{self.author.name} changed the channel name: **{self.content}**" diff --git a/disnake/permissions.py b/disnake/permissions.py index edad50e84d..4760f51bcf 100644 --- a/disnake/permissions.py +++ b/disnake/permissions.py @@ -716,7 +716,7 @@ def read_messages(self) -> int: @flag_value def send_messages(self) -> int: """:class:`bool`: Returns ``True`` if a user can send messages from all or specific text channels - and create threads in forum channels. + and create threads in forum/media channels. """ return 1 << 11 diff --git a/disnake/state.py b/disnake/state.py index 17a5c16e6f..ad57f062b1 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -37,6 +37,7 @@ DMChannel, ForumChannel, GroupChannel, + MediaChannel, PartialMessageable, StageChannel, TextChannel, @@ -766,7 +767,7 @@ def parse_message_create(self, data: gateway.MessageCreateEvent) -> None: if channel.__class__ is Thread and not ( message.type is MessageType.thread_starter_message or ( - type(channel.parent) is ForumChannel # type: ignore + type(channel.parent) in (ForumChannel, MediaChannel) # type: ignore and channel.id == message.id ) ): @@ -1111,8 +1112,8 @@ def parse_thread_create(self, data: gateway.ThreadCreateEvent) -> None: guild._add_thread(thread) if not has_thread: if data.get("newly_created"): - if isinstance(thread.parent, ForumChannel): - thread.parent.last_thread_id = thread.id + if type(thread.parent) in (ForumChannel, MediaChannel): + thread.parent.last_thread_id = thread.id # type: ignore self.dispatch("thread_create", thread) else: diff --git a/disnake/threads.py b/disnake/threads.py index 52810b9f64..8095bbc9a3 100644 --- a/disnake/threads.py +++ b/disnake/threads.py @@ -27,7 +27,7 @@ from typing_extensions import Self from .abc import Snowflake, SnowflakeTime - from .channel import CategoryChannel, ForumChannel, TextChannel + from .channel import CategoryChannel, ForumChannel, MediaChannel, TextChannel from .emoji import Emoji from .guild import Guild from .member import Member @@ -83,7 +83,7 @@ class Thread(Messageable, Hashable): id: :class:`int` The thread ID. parent_id: :class:`int` - The parent :class:`TextChannel` or :class:`ForumChannel` ID this thread belongs to. + The parent :class:`TextChannel`, :class:`ForumChannel`, or :class:`MediaChannel` ID this thread belongs to. owner_id: Optional[:class:`int`] The user's ID that created this thread. last_message_id: Optional[:class:`int`] @@ -242,8 +242,8 @@ def type(self) -> ThreadType: return self._type @property - def parent(self) -> Optional[Union[TextChannel, ForumChannel]]: - """Optional[Union[:class:`TextChannel`, :class:`ForumChannel`]]: The parent channel this thread belongs to.""" + def parent(self) -> Optional[Union[TextChannel, ForumChannel, MediaChannel]]: + """Optional[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`MediaChannel`]]: The parent channel this thread belongs to.""" return self.guild.get_channel(self.parent_id) # type: ignore @property @@ -384,7 +384,7 @@ def is_nsfw(self) -> bool: return parent is not None and parent.is_nsfw() def is_pinned(self) -> bool: - """Whether the thread is pinned in a :class:`ForumChannel` + """Whether the thread is pinned in a :class:`ForumChannel` or :class:`MediaChannel`. Pinned threads are not affected by the auto archive duration. @@ -399,14 +399,14 @@ def is_pinned(self) -> bool: @property def applied_tags(self) -> List[ForumTag]: """List[:class:`ForumTag`]: The tags currently applied to this thread. - Only applicable to threads in :class:`ForumChannel`\\s. + Only applicable to threads in channels of type :class:`ForumChannel` or :class:`MediaChannel`. .. versionadded:: 2.6 """ - from .channel import ForumChannel # cyclic import + from .channel import ThreadOnlyGuildChannel # cyclic import parent = self.parent - if not isinstance(parent, ForumChannel): + if not isinstance(parent, ThreadOnlyGuildChannel): return [] # threads may have tag IDs for tags that don't exist anymore @@ -697,7 +697,7 @@ async def edit( Specifies the slowmode rate limit for users in this thread, in seconds. A value of ``0`` disables slowmode. The maximum value possible is ``21600``. pinned: :class:`bool` - Whether to pin the thread or not. This is only available for threads created in a :class:`ForumChannel`. + Whether to pin the thread or not. This is only available for threads created in a :class:`ForumChannel` or :class:`MediaChannel`. .. versionadded:: 2.5 @@ -711,7 +711,7 @@ async def edit( The new tags of the thread. Maximum of 5. Can also be used to reorder existing tags. - This is only available for threads in a :class:`ForumChannel`. + This is only available for threads in a :class:`ForumChannel` or :class:`MediaChannel`. If :attr:`~ForumTag.moderated` tags are edited, :attr:`Permissions.manage_threads` permissions are required. @@ -894,7 +894,7 @@ async def delete(self, *, reason: Optional[str] = None) -> None: Deletes this thread. You must have :attr:`~Permissions.manage_threads` to delete threads. - Alternatively, you may delete a thread if it's in a :class:`ForumChannel`, + Alternatively, you may delete a thread if it's in a :class:`ForumChannel` or :class:`MediaChannel`, you are the thread creator, and there are no messages other than the initial message. Parameters @@ -918,7 +918,7 @@ async def add_tags(self, *tags: Snowflake, reason: Optional[str] = None) -> None Adds the given tags to this thread, up to 5 in total. - The thread must be in a :class:`ForumChannel`. + The thread must be in a :class:`ForumChannel` or :class:`MediaChannel`. Adding tags requires you to have :attr:`.Permissions.manage_threads` permissions, or be the owner of the thread. @@ -955,7 +955,7 @@ async def remove_tags(self, *tags: Snowflake, reason: Optional[str] = None) -> N Removes the given tags from this thread. - The thread must be in a :class:`ForumChannel`. + The thread must be in a :class:`ForumChannel` or :class:`MediaChannel`. Removing tags requires you to have :attr:`.Permissions.manage_threads` permissions, or be the owner of the thread. @@ -1090,7 +1090,7 @@ def thread(self) -> Thread: class ForumTag(Hashable): - """Represents a tag for threads in forum channels. + """Represents a tag for threads in forum/media channels. .. collapse:: operations @@ -1229,7 +1229,7 @@ def with_changes( moderated: bool = MISSING, ) -> Self: """Returns a new instance with the given changes applied, - for easy use with :func:`ForumChannel.edit`. + for easy use with :func:`ForumChannel.edit` or :func:`MediaChannel.edit`. All other fields will be kept intact. Returns diff --git a/disnake/types/channel.py b/disnake/types/channel.py index 238d88d141..242a4b4695 100644 --- a/disnake/types/channel.py +++ b/disnake/types/channel.py @@ -18,7 +18,7 @@ class PermissionOverwrite(TypedDict): deny: str -ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15] +ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16] class _BaseChannel(TypedDict): @@ -118,8 +118,7 @@ class DefaultReaction(TypedDict): ThreadLayout = Literal[0, 1, 2] -class ForumChannel(_BaseGuildChannel): - type: Literal[15] +class _BaseThreadOnlyGuildChannel(_BaseGuildChannel): topic: NotRequired[Optional[str]] last_message_id: NotRequired[Optional[Snowflake]] default_auto_archive_duration: NotRequired[ThreadArchiveDurationLiteral] @@ -127,9 +126,17 @@ class ForumChannel(_BaseGuildChannel): default_reaction_emoji: NotRequired[Optional[DefaultReaction]] default_thread_rate_limit_per_user: NotRequired[int] default_sort_order: NotRequired[Optional[ThreadSortOrder]] + + +class ForumChannel(_BaseThreadOnlyGuildChannel): + type: Literal[15] default_forum_layout: NotRequired[ThreadLayout] +class MediaChannel(_BaseThreadOnlyGuildChannel): + type: Literal[16] + + GuildChannel = Union[ TextChannel, NewsChannel, @@ -138,6 +145,7 @@ class ForumChannel(_BaseGuildChannel): StageChannel, ThreadChannel, ForumChannel, + MediaChannel, ] diff --git a/disnake/webhook/async_.py b/disnake/webhook/async_.py index cf25ca8fd6..7e1228a529 100644 --- a/disnake/webhook/async_.py +++ b/disnake/webhook/async_.py @@ -56,7 +56,7 @@ from ..abc import Snowflake from ..asset import AssetBytes - from ..channel import ForumChannel, StageChannel, TextChannel, VoiceChannel + from ..channel import ForumChannel, MediaChannel, StageChannel, TextChannel, VoiceChannel from ..embeds import Embed from ..file import File from ..guild import Guild @@ -511,7 +511,7 @@ def handle_message_parameters_dict( allowed_mentions: Optional[AllowedMentions] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING, - # these parameters are exclusive to webhooks in forum channels + # these parameters are exclusive to webhooks in forum/media channels thread_name: str = MISSING, applied_tags: Sequence[Snowflake] = MISSING, ) -> DictPayloadParameters: @@ -602,7 +602,7 @@ def handle_message_parameters( allowed_mentions: Optional[AllowedMentions] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING, - # these parameters are exclusive to webhooks in forum channels + # these parameters are exclusive to webhooks in forum/media channels thread_name: str = MISSING, applied_tags: Sequence[Snowflake] = MISSING, ) -> PayloadParameters: @@ -1008,13 +1008,15 @@ 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, StageChannel]]: - """Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`ForumChannel`, :class:`StageChannel`]]: The channel this webhook belongs to. + def channel( + self, + ) -> Optional[Union[TextChannel, VoiceChannel, StageChannel, ForumChannel, MediaChannel]]: + """Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`ForumChannel`, :class:`MediaChannel`]]: The channel this webhook belongs to. If this is a partial webhook, then this will always return ``None``. - Webhooks in :class:`ForumChannel`\\s can not send messages directly, - they can only create new threads (see ``thread_name`` for :attr:`Webhook.send`) + Webhooks in a :class:`ForumChannel` or :class:`MediaChannel` can not send messages directly. + They can only create new threads (see ``thread_name`` for :attr:`Webhook.send`) and interact with existing threads. """ guild = self.guild @@ -1046,7 +1048,7 @@ 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`, - :meth:`.ForumChannel.webhooks`, :meth:`.VoiceChannel.webhooks`, + :meth:`.VoiceChannel.webhooks`, :meth:`.ForumChannel.webhooks`, :meth:`MediaChannel.webhooks` and :meth:`.StageChannel.webhooks`. The ones received by the library will automatically be bound using the library's internal HTTP session. @@ -1564,7 +1566,7 @@ async def send( ``embeds`` parameter, which must be a :class:`list` of :class:`Embed` objects to send. To send a message in a thread, provide the ``thread`` parameter. - If this webhook is in a :class:`ForumChannel`, the ``thread_name`` parameter can + If this webhook is in a :class:`ForumChannel`/:class:`MediaChannel`, the ``thread_name`` parameter can be used to create a new thread instead (optionally with ``applied_tags``). .. versionchanged:: 2.6 @@ -1630,7 +1632,7 @@ async def send( .. versionadded:: 2.0 thread_name: :class:`str` - If in a forum channel, and ``thread`` is not specified, + If in a forum/media channel, and ``thread`` is not specified, the name of the newly created thread. .. note:: @@ -1639,7 +1641,7 @@ async def send( .. versionadded:: 2.6 applied_tags: Sequence[:class:`abc.Snowflake`] - If in a forum channel and creating a new thread (see ``thread_name`` above), + If in a forum/media channel and creating a new thread (see ``thread_name`` above), the tags to apply to the new thread. Maximum of 5. .. versionadded:: 2.10 diff --git a/disnake/webhook/sync.py b/disnake/webhook/sync.py index 410016a68b..bd9779db43 100644 --- a/disnake/webhook/sync.py +++ b/disnake/webhook/sync.py @@ -967,7 +967,7 @@ def send( ``embeds`` parameter, which must be a :class:`list` of :class:`Embed` objects to send. To send a message in a thread, provide the ``thread`` parameter. - If this webhook is in a :class:`ForumChannel`, the ``thread_name`` parameter can + If this webhook is in a :class:`ForumChannel`/:class:`MediaChannel`, the ``thread_name`` parameter can be used to create a new thread instead (optionally with ``applied_tags``). .. versionchanged:: 2.6 @@ -1007,13 +1007,13 @@ def send( .. versionadded:: 2.0 thread_name: :class:`str` - If in a forum channel, and ``thread`` is not specified, + If in a forum/media channel, and ``thread`` is not specified, the name of the newly created thread. .. versionadded:: 2.6 applied_tags: Sequence[:class:`abc.Snowflake`] - If in a forum channel and creating a new thread (see ``thread_name`` above), + If in a forum/media channel and creating a new thread (see ``thread_name`` above), the tags to apply to the new thread. Maximum of 5. .. versionadded:: 2.10 diff --git a/docs/api/audit_logs.rst b/docs/api/audit_logs.rst index e4cb0573d2..f1a65e434c 100644 --- a/docs/api/audit_logs.rst +++ b/docs/api/audit_logs.rst @@ -282,10 +282,10 @@ AuditLogDiff .. attribute:: topic - The topic of a :class:`TextChannel`, :class:`StageChannel`, :class:`StageInstance` or :class:`ForumChannel`. + The topic of a :class:`TextChannel`, :class:`StageChannel`, :class:`StageInstance`, :class:`ForumChannel` or :class:`MediaChannel`. See also :attr:`TextChannel.topic`, :attr:`StageChannel.topic`, - :attr:`StageInstance.topic` or :attr:`ForumChannel.topic`. + :attr:`StageInstance.topic`, :attr:`ForumChannel.topic` or :attr:`MediaChannel.topic`. :type: :class:`str` @@ -467,7 +467,7 @@ AuditLogDiff See also :attr:`TextChannel.slowmode_delay`, :attr:`VoiceChannel.slowmode_delay`, :attr:`StageChannel.slowmode_delay`, :attr:`ForumChannel.slowmode_delay`, - or :attr:`Thread.slowmode_delay`. + :attr:`MediaChannel.slowmode_delay` or :attr:`Thread.slowmode_delay`. :type: :class:`int` @@ -476,8 +476,9 @@ AuditLogDiff The default number of seconds members have to wait before sending another message in new threads created in the channel. - See also :attr:`TextChannel.default_thread_slowmode_delay` or - :attr:`ForumChannel.default_thread_slowmode_delay`. + See also :attr:`TextChannel.default_thread_slowmode_delay`, + :attr:`ForumChannel.default_thread_slowmode_delay` or + :attr:`MediaChannel.default_thread_slowmode_delay`. :type: :class:`int` @@ -510,7 +511,7 @@ AuditLogDiff Whether the channel is marked as "not safe for work". - See also :attr:`TextChannel.nsfw`, :attr:`VoiceChannel.nsfw`, :attr:`StageChannel.nsfw`, or :attr:`ForumChannel.nsfw`. + See also :attr:`TextChannel.nsfw`, :attr:`VoiceChannel.nsfw`, :attr:`StageChannel.nsfw`, :attr:`ForumChannel.nsfw` or :attr:`MediaChannel.nsfw`. :type: :class:`bool` @@ -687,7 +688,7 @@ AuditLogDiff .. attribute:: applied_tags - The tags applied to a thread in a forum channel being changed. + The tags applied to a thread in a forum/media channel being changed. If a tag is not found, then it is an :class:`Object` with the ID being set. @@ -696,13 +697,13 @@ AuditLogDiff .. attribute:: available_tags - The available tags for threads in a forum channel being changed. + The available tags for threads in a forum/media channel being changed. :type: List[:class:`ForumTag`] .. attribute:: default_reaction - The default emoji shown for reacting to threads in a forum channel being changed. + The default emoji shown for reacting to threads in a forum/media channel being changed. Due to a Discord limitation, this will have an empty :attr:`~PartialEmoji.name` if it is a custom :class:`PartialEmoji`. @@ -711,7 +712,7 @@ AuditLogDiff .. attribute:: default_sort_order - The default sort order of threads in a forum channel being changed. + The default sort order of threads in a forum/media channel being changed. :type: Optional[:class:`ThreadSortOrder`] diff --git a/docs/api/channels.rst b/docs/api/channels.rst index 5d19d5b3c6..faa709aa19 100644 --- a/docs/api/channels.rst +++ b/docs/api/channels.rst @@ -92,6 +92,19 @@ ForumChannel .. automethod:: typing :async-with: +MediaChannel +~~~~~~~~~~~~ + +.. attributetable:: MediaChannel + +.. autoclass:: MediaChannel() + :members: + :inherited-members: + :exclude-members: typing + + .. automethod:: typing + :async-with: + DMChannel ~~~~~~~~~ @@ -234,6 +247,12 @@ ChannelType .. versionadded:: 2.5 + .. attribute:: media + + A channel of only threads but with a focus on media, similar to forum channels. + + .. versionadded:: 2.10 + ThreadArchiveDuration ~~~~~~~~~~~~~~~~~~~~~ @@ -281,7 +300,7 @@ ThreadSortOrder .. class:: ThreadSortOrder - Represents the sort order of threads in :class:`ForumChannel`\s. + Represents the sort order of threads in a :class:`ForumChannel` or :class:`MediaChannel`. .. versionadded:: 2.6 diff --git a/docs/api/events.rst b/docs/api/events.rst index 168151bbee..22eca0b77b 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1397,18 +1397,12 @@ 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`. + The ``channel`` parameter can be a :class:`abc.Messageable` instance, or a :class:`ForumChannel` or :class:`MediaChannel`. 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` - - .. versionchanged:: 2.9 - ``channel`` may be a type :class:`StageChannel` - - 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 not a :class:`DMChannel`, + 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, then this event will not be called. Consider using :func:`on_raw_typing` instead. @@ -1426,7 +1420,7 @@ This section documents events related to Discord chat messages. to enable the members intent. :param channel: The location where the typing originated from. - :type channel: Union[:class:`abc.Messageable`, :class:`ForumChannel`] + :type channel: Union[:class:`abc.Messageable`, :class:`ForumChannel`, :class:`MediaChannel`] :param user: The user that started typing. :type user: Union[:class:`User`, :class:`Member`] :param when: When the typing started as an aware datetime in UTC. diff --git a/docs/ext/commands/api/converters.rst b/docs/ext/commands/api/converters.rst index 67bd6dd14c..012dca1261 100644 --- a/docs/ext/commands/api/converters.rst +++ b/docs/ext/commands/api/converters.rst @@ -48,6 +48,9 @@ Classes .. autoclass:: ForumChannelConverter :members: +.. autoclass:: MediaChannelConverter + :members: + .. autoclass:: ThreadConverter :members: diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index c54f3b21ae..18024647ca 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -387,6 +387,7 @@ A lot of Discord models work out of the gate as a parameter: - :class:`StageChannel` (since v1.7) - :class:`CategoryChannel` - :class:`ForumChannel` (since v2.5) +- :class:`MediaChannel` (since v2.10) - :class:`Thread` (since v2.0) - :class:`Colour` - :class:`Role` @@ -430,6 +431,8 @@ converter is given below: +------------------------------+--------------------------------------------------------+ | :class:`ForumChannel` | :class:`~ext.commands.ForumChannelConverter` | +------------------------------+--------------------------------------------------------+ +| :class:`MediaChannel` | :class:`~ext.commands.MediaChannelConverter` | ++------------------------------+--------------------------------------------------------+ | :class:`Thread` | :class:`~ext.commands.ThreadConverter` | +------------------------------+--------------------------------------------------------+ | :class:`Colour` | :class:`~ext.commands.ColourConverter` |