diff --git a/CHANGELOG.md b/CHANGELOG.md index c42ff5f256..41fb4bdc5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `Member.vr_status` property. ([#3328](https://github.com/Pycord-Development/pycord/pull/3328)) +- Added spoiler channels. + ([#3252](https://github.com/Pycord-Development/pycord/pull/3252)) ### Changed diff --git a/discord/channel.py b/discord/channel.py index dab70c4a92..a39754a7c2 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -30,10 +30,12 @@ from typing import ( TYPE_CHECKING, Any, + Literal, NamedTuple, TypeVar, overload, ) +from warnings import warn from typing_extensions import deprecated @@ -317,6 +319,17 @@ def is_nsfw(self) -> bool: """Checks if the channel is NSFW.""" return self.nsfw + @property + def spoiler(self) -> bool: + """Checks if the channel is a spoiler channel. + + .. note:: + This is an alias for :attr:`flags.is_spoiler_channel`. + + .. versionadded:: 2.9 + """ + return self.flags.is_spoiler_channel + @property def last_message(self) -> Message | None: """Fetches the last message from this channel in cache. @@ -809,6 +822,26 @@ async def edit( default_thread_slowmode_delay: int = ..., type: ChannelType = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: Literal[False] = ..., + ) -> TextChannel | None: ... + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: Literal[False] = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + type: ChannelType = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: bool = ..., ) -> TextChannel | None: ... @overload @@ -841,6 +874,10 @@ async def edit(self, *, reason=None, **options): The new channel's position. nsfw: :class:`bool` Whether the channel is marked as NSFW. + + .. note:: + This setting is mutually exclusive with :attr:`spoiler`. Applying this to a spoiler channel will convert it to an NSFW channel. + Passing both as ``True`` will make the channel an NSFW channel. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -865,6 +902,14 @@ async def edit(self, *, reason=None, **options): The new default slowmode delay in seconds for threads created in this channel. .. versionadded:: 2.3 + spoiler: :class:`bool` + Whether the channel should be a spoiler channel. + + .. note:: + This setting is mutually exclusive with :attr:`nsfw`. Applying this to an NSFW channel will convert it to a spoiler channel. + Passing both as ``True`` will make the channel an NSFW channel. + + .. versionadded:: 2.9 Returns ------- @@ -882,6 +927,16 @@ async def edit(self, *, reason=None, **options): HTTPException Editing the channel failed. """ + if "spoiler" in options: + options["flags"] = ChannelFlags._from_value(self.flags.value) + options["flags"].is_spoiler_channel = options["spoiler"] + + if options.get("nsfw") and options.get("spoiler"): + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + options.pop("spoiler") + payload = await self._edit(options, reason=reason) if payload is not None: # the payload will always be the proper channel payload @@ -1123,6 +1178,31 @@ async def edit( available_tags: list[ForumTag] = ..., require_tag: bool = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: Literal[False] = ..., + ) -> ForumChannel | None: ... + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: Literal[False] = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ( + ThreadArchiveDuration | ThreadArchiveDurationEnum + ) = ..., + default_thread_slowmode_delay: int = ..., + default_sort_order: SortOrder = ..., + default_reaction_emoji: GuildEmoji | int | str | None = ..., + available_tags: list[ForumTag] = ..., + require_tag: bool = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: bool = ..., ) -> ForumChannel | None: ... @overload @@ -1185,6 +1265,10 @@ async def edit(self, *, reason=None, **options): Whether a tag should be required to be specified when creating a thread in this channel. .. versionadded:: 2.3 + spoiler: :class:`bool` + Whether the channel should be a spoiler channel. Mutually exclusive with :attr:`nsfw`. + + .. versionadded:: 2.9 Returns ------- @@ -1205,6 +1289,13 @@ async def edit(self, *, reason=None, **options): if "require_tag" in options: options["flags"] = ChannelFlags._from_value(self.flags.value) options["flags"].require_tag = options.pop("require_tag") + if options.get("nsfw") and options.get("spoiler"): + warn( + "The NSFW setting is mutually exclusive with the spoiler setting. The channel will become an NSFW channel." + ) + if "flags" not in options: + options["flags"] = ChannelFlags._from_value(self.flags.value) + options["flags"].is_spoiler_channel = options.pop("spoiler") payload = await self._edit(options, reason=reason) if payload is not None: @@ -1504,6 +1595,30 @@ async def edit( require_tag: bool = ..., hide_media_download_options: bool = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: Literal[False] = ..., + ) -> ForumChannel | None: ... + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: Literal[False] = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + default_sort_order: SortOrder = ..., + default_reaction_emoji: GuildEmoji | int | str | None = ..., + available_tags: list[ForumTag] = ..., + require_tag: bool = ..., + hide_media_download_options: bool = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + spoiler: bool = ..., ) -> ForumChannel | None: ... async def edit(self, *, reason=None, **options): @@ -1560,6 +1675,11 @@ async def edit(self, *, reason=None, **options): hide_media_download_options: :class:`bool` Whether media download options should be hidden in this media channel. + spoiler: :class:`bool` + Whether the channel should be a spoiler channel. Mutually exclusive with :attr:`nsfw`. + + .. versionadded:: 2.9 + Returns ------- Optional[:class:`.MediaChannel`] @@ -1577,14 +1697,18 @@ async def edit(self, *, reason=None, **options): Editing the channel failed. """ - if "require_tag" in options or "hide_media_download_options" in options: + if {"require_tag", "hide_media_download_options", "spoiler"} & options.keys(): flags = ChannelFlags._from_value(self.flags.value) flags.require_tag = options.pop("require_tag", flags.require_tag) flags.hide_media_download_options = options.pop( "hide_media_download_options", flags.hide_media_download_options ) + if options.get("nsfw") and options.get("spoiler"): + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + flags.is_spoiler_channel = options.pop("spoiler", flags.is_spoiler_channel) options["flags"] = flags - payload = await self._edit(options, reason=reason) if payload is not None: # the payload will always be the proper channel payload @@ -1815,6 +1939,17 @@ def is_nsfw(self) -> bool: """Checks if the channel is NSFW.""" return self.nsfw + @property + def spoiler(self) -> bool: + """Checks if the channel is a spoiler channel. + + .. note:: + This is an alias for :attr:`flags.is_spoiler_channel`. + + .. versionadded:: 2.9 + """ + return self.flags.is_spoiler_channel + @property def last_message(self) -> Message | None: """Fetches the last message from this channel in cache. @@ -2096,6 +2231,26 @@ async def edit( slowmode_delay: int = ..., nsfw: bool = ..., reason: str | None = ..., + spoiler: Literal[False] = ..., + ) -> VoiceChannel | None: ... + + @overload + async def edit( + self, + *, + name: str = ..., + bitrate: int = ..., + user_limit: int = ..., + position: int = ..., + sync_permissions: int = ..., + category: CategoryChannel | None = ..., + overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., + nsfw: Literal[False] = ..., + reason: str | None = ..., + spoiler: bool = ..., ) -> VoiceChannel | None: ... @overload @@ -2154,6 +2309,11 @@ async def edit(self, *, reason=None, **options): .. versionadded:: 2.7 + spoiler: :class:`bool` + Whether the channel should be a spoiler channel. Mutually exclusive with :attr:`nsfw`. + + .. versionadded:: 2.9 + Returns ------- Optional[:class:`.VoiceChannel`] @@ -2169,6 +2329,15 @@ async def edit(self, *, reason=None, **options): HTTPException Editing the channel failed. """ + if "spoiler" in options: + options["flags"] = ChannelFlags._from_value(self.flags.value) + options["flags"].is_spoiler_channel = options["spoiler"] + + if options.get("nsfw") and options.get("spoiler"): + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + options.pop("spoiler") payload = await self._edit(options, reason=reason) if payload is not None: @@ -2403,6 +2572,17 @@ def is_nsfw(self) -> bool: """Checks if the channel is NSFW.""" return self.nsfw + @property + def spoiler(self) -> bool: + """Checks if the channel is a spoiler channel. + + .. note:: + This is an alias for :attr:`flags.is_spoiler_channel`. + + .. versionadded:: 2.9 + """ + return self.flags.is_spoiler_channel + @property def last_message(self) -> Message | None: """Fetches the last message from this channel in cache. @@ -2781,6 +2961,7 @@ async def edit( rtc_region: VoiceRegion | None = ..., video_quality_mode: VideoQualityMode = ..., reason: str | None = ..., + spoiler: bool = ..., ) -> StageChannel | None: ... @overload diff --git a/discord/flags.py b/discord/flags.py index ace2060a59..74eb1e435e 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -1587,6 +1587,14 @@ def hide_media_download_options(self): """ return 1 << 15 + @flag_value + def is_spoiler_channel(self): + """:class:`bool`: Returns ``True`` if the channel is a spoiler channel. + + .. versionadded:: 2.9 + """ + return 1 << 21 + @fill_with_flags() class AttachmentFlags(BaseFlags): diff --git a/discord/guild.py b/discord/guild.py index b485123b52..2c8e35e009 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -28,11 +28,13 @@ import copy import datetime import unicodedata +from _warnings import warn from collections.abc import Sequence from typing import ( TYPE_CHECKING, Any, ClassVar, + Literal, NamedTuple, Optional, TypeVar, @@ -70,7 +72,7 @@ ) from .errors import ClientException, HTTPException, InvalidArgument, InvalidData from .file import File -from .flags import SystemChannelFlags +from .flags import ChannelFlags, SystemChannelFlags from .incidents import IncidentsData from .integrations import Integration, _integration_factory from .invite import Invite @@ -1429,6 +1431,9 @@ def _create_channel( elif not isinstance(overwrites, dict): raise InvalidArgument("overwrites parameter expects a dict.") + if "flags" in options: + options["flags"] = options.pop("flags").value + perms = [] for target, perm in overwrites.items(): if not isinstance(perm, PermissionOverwrite): @@ -1460,6 +1465,40 @@ def _create_channel( **options, ) + @overload + async def create_text_channel( + self, + name: str, + *, + reason: str | None = ..., + category: CategoryChannel | None = ..., + position: int = ..., + topic: str = ..., + slowmode_delay: int = ..., + nsfw: bool = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + default_thread_slowmode_delay: int | None = ..., + default_auto_archive_duration: int = ..., + spoiler: Literal[False] = ..., + ) -> TextChannel: ... + + @overload + async def create_text_channel( + self, + name: str, + *, + reason: str | None = ..., + category: CategoryChannel | None = ..., + position: int = ..., + topic: str = ..., + slowmode_delay: int = ..., + nsfw: Literal[False] = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + default_thread_slowmode_delay: int | None = ..., + default_auto_archive_duration: int = ..., + spoiler: bool = ..., + ) -> TextChannel: ... + async def create_text_channel( self, name: str, @@ -1473,6 +1512,7 @@ async def create_text_channel( overwrites: dict[Role | Member, PermissionOverwrite] = MISSING, default_thread_slowmode_delay: int | None = MISSING, default_auto_archive_duration: int = MISSING, + spoiler: bool = MISSING, ) -> TextChannel: """|coro| @@ -1512,6 +1552,10 @@ async def create_text_channel( A value of `0` disables slowmode. The maximum value possible is `21600`. nsfw: :class:`bool` Whether the channel is marked as NSFW. + + .. warning:: + Passing both this and ``spoiler`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. @@ -1524,6 +1568,13 @@ async def create_text_channel( The default auto archive duration in minutes for threads created in this channel. .. versionadded:: 2.7 + spoiler: :class:`bool` + Whether the channel is marked as a spoiler channel. + + .. versionadded:: 2.9 + + .. warning:: + Passing both this and ``nsfw`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. Returns ------- @@ -1579,6 +1630,14 @@ async def create_text_channel( if default_auto_archive_duration is not MISSING: options["default_auto_archive_duration"] = default_auto_archive_duration + if spoiler is not MISSING: + options["flags"] = ChannelFlags(is_spoiler_channel=spoiler) + + if nsfw and spoiler: + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + data = await self._create_channel( name, overwrites=overwrites, @@ -1593,6 +1652,42 @@ async def create_text_channel( self._channels[channel.id] = channel return channel + @overload + async def create_voice_channel( + self, + name: str, + *, + reason: str | None = ..., + category: CategoryChannel | None = ..., + position: int = ..., + bitrate: int = ..., + user_limit: int = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + slowmode_delay: int = ..., + nsfw: bool = ..., + spoiler: Literal[False] = ..., + ) -> VoiceChannel: ... + + @overload + async def create_voice_channel( + self, + name: str, + *, + reason: str | None = None, + category: CategoryChannel | None = None, + position: int = ..., + bitrate: int = ..., + user_limit: int = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + slowmode_delay: int = ..., + nsfw: Literal[False] = ..., + spoiler: bool = ..., + ) -> VoiceChannel: ... + async def create_voice_channel( self, name: str, @@ -1607,6 +1702,7 @@ async def create_voice_channel( overwrites: dict[Role | Member, PermissionOverwrite] = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, + spoiler: bool = MISSING, ) -> VoiceChannel: """|coro| @@ -1652,6 +1748,17 @@ async def create_voice_channel( .. versionadded:: 2.7 + .. note:: + Passing both this and ``spoiler`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + + spoiler: :class:`bool` + Whether the channel is marked as a spoiler channel. + + .. versionadded:: 2.9 + + .. note:: + Passing both this and ``nsfw`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + Returns ------- :class:`VoiceChannel` @@ -1688,6 +1795,14 @@ async def create_voice_channel( if nsfw is not MISSING: options["nsfw"] = nsfw + if spoiler is not MISSING: + options["flags"] = ChannelFlags(is_spoiler_channel=spoiler) + + if nsfw and spoiler: + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + data = await self._create_channel( name, overwrites=overwrites, @@ -1702,6 +1817,44 @@ async def create_voice_channel( self._channels[channel.id] = channel return channel + @overload + async def create_stage_channel( + self, + name: str, + *, + topic: str, + position: int = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + category: CategoryChannel | None = ..., + reason: str | None = ..., + bitrate: int = ..., + user_limit: int = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., + nsfw: bool = ..., + spoiler: Literal[False] = ..., + ) -> StageChannel: ... + + @overload + async def create_stage_channel( + self, + name: str, + *, + topic: str, + position: int = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + category: CategoryChannel | None = None, + reason: str | None = None, + bitrate: int = ..., + user_limit: int = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., + nsfw: Literal[False] = ..., + spoiler: bool = ..., + ) -> StageChannel: ... + async def create_stage_channel( self, name: str, @@ -1717,6 +1870,7 @@ async def create_stage_channel( video_quality_mode: VideoQualityMode = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, + spoiler: bool = MISSING, ) -> StageChannel: """|coro| @@ -1774,6 +1928,17 @@ async def create_stage_channel( .. versionadded:: 2.7 + .. warning:: + Passing both this and ``spoiler`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + + spoiler: :class:`bool` + Whether the channel is marked as a spoiler channel. + + .. versionadded:: 2.9 + + .. warning:: + Passing both this and ``nsfw`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + Returns ------- :class:`StageChannel` @@ -1813,6 +1978,14 @@ async def create_stage_channel( if nsfw is not MISSING: options["nsfw"] = nsfw + if spoiler is not MISSING: + options["flags"] = ChannelFlags(is_spoiler_channel=spoiler) + + if nsfw and spoiler: + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + data = await self._create_channel( name, overwrites=overwrites, @@ -1827,6 +2000,46 @@ async def create_stage_channel( self._channels[channel.id] = channel return channel + @overload + async def create_forum_channel( + self, + name: str, + *, + reason: str | None = ..., + category: CategoryChannel | None = ..., + position: int = ..., + topic: str = ..., + slowmode_delay: int = ..., + nsfw: bool = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + default_reaction_emoji: GuildEmoji | int | str = ..., + available_tags: list[ForumTag] = ..., + default_sort_order: SortOrder | None = ..., + default_thread_slowmode_delay: int | None = ..., + default_auto_archive_duration: int = ..., + spoiler: Literal[False] = ..., + ) -> ForumChannel: ... + + @overload + async def create_forum_channel( + self, + name: str, + *, + reason: str | None = ..., + category: CategoryChannel | None = ..., + position: int = ..., + topic: str = ..., + slowmode_delay: int = ..., + nsfw: Literal[False] = ..., + overwrites: dict[Role | Member, PermissionOverwrite] = ..., + default_reaction_emoji: GuildEmoji | int | str = ..., + available_tags: list[ForumTag] = ..., + default_sort_order: SortOrder | None = ..., + default_thread_slowmode_delay: int | None = ..., + default_auto_archive_duration: int = ..., + spoiler: bool = ..., + ) -> ForumChannel: ... + async def create_forum_channel( self, name: str, @@ -1843,6 +2056,7 @@ async def create_forum_channel( default_sort_order: SortOrder | None = MISSING, default_thread_slowmode_delay: int | None = MISSING, default_auto_archive_duration: int = MISSING, + spoiler: bool = MISSING, ) -> ForumChannel: """|coro| @@ -1882,6 +2096,10 @@ async def create_forum_channel( A value of ``0`` disables slowmode. The maximum value possible is ``21600``. nsfw: :class:`bool` Whether the channel is marked as NSFW. + + .. warning:: + Passing both this and ``spoiler`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. + reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. default_reaction_emoji: Optional[:class:`GuildEmoji` | :class:`int` | :class:`str`] @@ -1910,6 +2128,13 @@ async def create_forum_channel( The default auto archive duration in minutes for threads created in this channel. .. versionadded:: 2.7 + spoiler: :class:`bool` + Whether the channel is marked as a spoiler channel. + + .. versionadded:: 2.9 + + .. warning:: + Passing both this and ``nsfw`` as ``True`` will mark the channel as NSFW and ignore the spoiler flag. Returns ------- @@ -1973,6 +2198,14 @@ async def create_forum_channel( if default_auto_archive_duration is not MISSING: options["default_auto_archive_duration"] = default_auto_archive_duration + if spoiler is not MISSING: + options["flags"] = ChannelFlags(is_spoiler_channel=spoiler) + + if nsfw and spoiler: + warn( + "NSFW setting is mutually exclusive with spoiler setting. Channel will become an NSFW channel." + ) + if default_reaction_emoji is not MISSING: if isinstance( default_reaction_emoji, _EmojiTag diff --git a/discord/http.py b/discord/http.py index 40ebcd7c1f..87be923490 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1153,6 +1153,7 @@ def create_channel( "video_quality_mode", "auto_archive_duration", "default_reaction_emoji", + "flags", ) payload.update( {k: v for k, v in options.items() if k in valid_keys and v is not None}