diff --git a/changelog/1282.feature.rst b/changelog/1282.feature.rst new file mode 100644 index 0000000000..03b4c05d1c --- /dev/null +++ b/changelog/1282.feature.rst @@ -0,0 +1 @@ +Add :attr:`Embed.flags`, ``Embed.image.flags``, ``Embed.thumbnail.flags``, :class:`EmbedFlags`, :class:`EmbedMediaFlags` and update the :class:`AttachmentFlags`. diff --git a/disnake/embeds.py b/disnake/embeds.py index add4395151..d9082da1f3 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -22,6 +22,7 @@ from . import utils from .colour import Colour from .file import File +from .flags import EmbedFlags, EmbedMediaFlags from .utils import MISSING, classproperty, warn_deprecated __all__ = ("Embed",) @@ -90,6 +91,7 @@ class _EmbedMediaProxy(Sized, Protocol): proxy_url: Optional[str] height: Optional[int] width: Optional[int] + flags: Optional[EmbedMediaFlags] class _EmbedVideoProxy(Sized, Protocol): url: Optional[str] @@ -183,6 +185,7 @@ class Embed: "_fields", "description", "_files", + "_flags", ) _default_colour: ClassVar[Optional[Colour]] = None @@ -221,6 +224,7 @@ def __init__( self._image: Optional[EmbedImagePayload] = None self._footer: Optional[EmbedFooterPayload] = None self._fields: Optional[List[EmbedFieldPayload]] = None + self._flags: int = 0 self._files: Dict[_FileKey, File] = {} @@ -268,12 +272,20 @@ def from_dict(cls, data: EmbedData) -> Self: self.timestamp = utils.parse_time(data.get("timestamp")) self._thumbnail = data.get("thumbnail") + if self._thumbnail and (thumbnail_flags := self._thumbnail.get("flags")): + self._thumbnail["flags"] = EmbedMediaFlags._from_value(thumbnail_flags) # type: ignore + self._video = data.get("video") self._provider = data.get("provider") self._author = data.get("author") + self._image = data.get("image") + if self._image and (image_flags := self._image.get("flags")): + self._image["flags"] = EmbedMediaFlags._from_value(image_flags) # type: ignore + self._footer = data.get("footer") self._fields = data.get("fields") + self._flags = data.get("flags", 0) return self @@ -370,6 +382,14 @@ def timestamp(self, value: Optional[datetime.datetime]) -> None: msg = f"Expected datetime.datetime or None received {type(value).__name__} instead" raise TypeError(msg) + @property + def flags(self) -> EmbedFlags: + """:class:`EmbedFlags`: Returns the embed's flags. + + .. versionadded:: |vnext| + """ + return EmbedFlags._from_value(self._flags) + @property def footer(self) -> _EmbedFooterProxy: """Returns an ``EmbedProxy`` denoting the footer contents. @@ -454,6 +474,11 @@ def image(self) -> _EmbedMediaProxy: - ``proxy_url`` - ``width`` - ``height`` + - ``flags`` + + .. versionchanged:: |vnext| + + Added the ``flags`` attribute. If an attribute is not set, it will be ``None``. """ @@ -507,6 +532,11 @@ def thumbnail(self) -> _EmbedMediaProxy: - ``proxy_url`` - ``width`` - ``height`` + - ``flags`` + + .. versionchanged:: |vnext| + + Added the ``flags`` attribute. If an attribute is not set, it will be ``None``. """ diff --git a/disnake/flags.py b/disnake/flags.py index 8ce3547908..7a6da26d09 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -45,6 +45,8 @@ "SKUFlags", "ApplicationInstallTypes", "InteractionContextTypes", + "EmbedFlags", + "EmbedMediaFlags", ) BF = TypeVar("BF", bound="BaseFlags") @@ -2623,13 +2625,62 @@ class AttachmentFlags(BaseFlags): if TYPE_CHECKING: @_generated - def __init__(self, *, is_remix: bool = ...) -> None: ... + def __init__( + self, + *, + contains_explicit_media: bool = ..., + is_animated: bool = ..., + is_clip: bool = ..., + is_remix: bool = ..., + is_spoiler: bool = ..., + is_thumbnail: bool = ..., + ) -> None: ... + + @flag_value + def is_clip(self): + """:class:`bool`: Returns ``True`` if the attachment is a clip. + + .. versionadded:: |vnext| + """ + return 1 << 0 + + @flag_value + def is_thumbnail(self): + """:class:`bool`: Returns ``True`` if the attachment is the thumbnail of a thread in a media channel. + + .. versionadded:: |vnext| + """ + return 1 << 1 @flag_value def is_remix(self) -> int: """:class:`bool`: Returns ``True`` if the attachment has been edited using the Remix feature.""" return 1 << 2 + @flag_value + def is_spoiler(self): + """:class:`bool`: Returns ``True`` if the attachment was marked as a spoiler. + + .. versionadded:: |vnext| + """ + return 1 << 3 + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. + + .. versionadded:: |vnext| + """ + return 1 << 4 + + @flag_value + def is_animated(self): + """:class:`bool`: Returns ``True`` if the attachment is an animated image. + + .. versionadded:: |vnext| + """ + return 1 << 5 + class SKUFlags(BaseFlags): """Wraps up Discord SKU flags. @@ -2917,3 +2968,164 @@ def bot_dm(self) -> int: def private_channel(self) -> int: """:class:`bool`: Returns ``True`` if the command is usable in DMs and group DMs with other users.""" return 1 << 2 + + +class EmbedFlags(BaseFlags): + """Wraps up Discord Embed flags. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two EmbedFlags instances are equal. + .. describe:: x != y + + Checks if two EmbedFlags instances are not equal. + .. describe:: x <= y + + Checks if an EmbedFlags instance is a subset of another EmbedFlags instance. + .. describe:: x >= y + + Checks if an EmbedFlags instance is a superset of another EmbedFlags instance. + .. describe:: x < y + + Checks if an EmbedFlags instance is a strict subset of another EmbedFlags instance. + .. describe:: x > y + + Checks if an EmbedFlags instance is a strict superset of another EmbedFlags instance. + .. describe:: x | y, x |= y + + Returns a new EmbedFlags instance with all enabled flags from both x and y. + (Using ``|=`` will update in place). + .. describe:: x & y, x &= y + + Returns a new EmbedFlags instance with only flags enabled on both x and y. + (Using ``&=`` will update in place). + .. describe:: x ^ y, x ^= y + + Returns a new EmbedFlags instance with only flags enabled on one of x or y, but not both. + (Using ``^=`` will update in place). + .. describe:: ~x + + Returns a new EmbedFlags instance with all flags from x inverted. + .. describe:: hash(x) + + Returns the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + Additionally supported are a few operations on class attributes. + + .. describe:: EmbedFlags.y | EmbedFlags.z, EmbedFlags(y=True) | EmbedFlags.z + + Returns an EmbedFlags instance with all provided flags enabled. + + .. describe:: ~EmbedFlags.y + + Returns an EmbedFlags instance with all flags except ``y`` inverted from their default value. + + .. versionadded:: |vnext| + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + if TYPE_CHECKING: + + @_generated + def __init__( + self, *, contains_explicit_media: bool = ..., is_content_inventory_entry: bool = ... + ) -> None: ... + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the embed was flagged as sensitive content.""" + return 1 << 4 + + @flag_value + def is_content_inventory_entry(self): + """:class:`bool`: Returns ``True`` if the embed is a reply to an activity card.""" + return 1 << 5 + + +class EmbedMediaFlags(BaseFlags): + """Wraps up Discord Embed media flags. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two EmbedMediaFlags instances are equal. + .. describe:: x != y + + Checks if two EmbedMediaFlags instances are not equal. + .. describe:: x <= y + + Checks if an EmbedMediaFlags instance is a subset of another EmbedMediaFlags instance. + .. describe:: x >= y + + Checks if an EmbedMediaFlags instance is a superset of another EmbedMediaFlags instance. + .. describe:: x < y + + Checks if an EmbedMediaFlags instance is a strict subset of another EmbedMediaFlags instance. + .. describe:: x > y + + Checks if an EmbedMediaFlags instance is a strict superset of another EmbedMediaFlags instance. + .. describe:: x | y, x |= y + + Returns a new EmbedMediaFlags instance with all enabled flags from both x and y. + (Using ``|=`` will update in place). + .. describe:: x & y, x &= y + + Returns a new EmbedMediaFlags instance with only flags enabled on both x and y. + (Using ``&=`` will update in place). + .. describe:: x ^ y, x ^= y + + Returns a new EmbedMediaFlags instance with only flags enabled on one of x or y, but not both. + (Using ``^=`` will update in place). + .. describe:: ~x + + Returns a new EmbedMediaFlags instance with all flags from x inverted. + .. describe:: hash(x) + + Returns the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + Additionally supported are a few operations on class attributes. + + .. describe:: EmbedMediaFlags.y | EmbedMediaFlags.z, EmbedMediaFlags(y=True) | EmbedMediaFlags.z + + Returns an EmbedMediaFlags instance with all provided flags enabled. + + .. describe:: ~EmbedMediaFlags.y + + Returns an EmbedMediaFlags instance with all flags except ``y`` inverted from their default value. + + .. versionadded:: |vnext| + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + if TYPE_CHECKING: + + @_generated + def __init__(self, *, is_animated: bool = ...) -> None: ... + + @flag_value + def is_animated(self): + """:class:`bool`: Returns ``True`` if the embed media is animated.""" + return 1 << 5 diff --git a/disnake/message.py b/disnake/message.py index b5319ad864..41db2ec699 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -326,6 +326,16 @@ class Attachment(Hashable): (see :attr:`MessageFlags.is_voice_message`). .. versionadded:: 2.9 + + clip_participants: List[:class:`User`] + If this attachment is a clip returns a list of users who were in the stream. + + .. versionadded:: |vnext| + + clip_created_at: Optional[:class:`datetime.datetime`] + If this attachment is a clip returns the creation timestamp. + + .. versionadded:: |vnext| """ __slots__ = ( @@ -344,6 +354,8 @@ class Attachment(Hashable): "duration", "waveform", "_flags", + "clip_participants", + "clip_created_at", ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState) -> None: @@ -364,13 +376,23 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState) -> None: b64decode(waveform_data) if (waveform_data := data.get("waveform")) else None ) self._flags: int = data.get("flags", 0) + self.clip_participants: List[User] = [ + User(state=state, data=d) for d in data.get("clip_participants", []) + ] + self.clip_created_at: Optional[datetime.datetime] = utils.parse_time( + data.get("clip_created_at") + ) def is_spoiler(self) -> bool: """Whether this attachment contains a spoiler. :return type: :class:`bool` + + .. versionchanged: |vnext| + + Now considers the attachment flags as well as the filename. """ - return self.filename.startswith("SPOILER_") + return self.filename.startswith("SPOILER_") or self.flags.is_spoiler def __repr__(self) -> str: return f"" diff --git a/disnake/types/embed.py b/disnake/types/embed.py index a066b084d3..765e17ac0b 100644 --- a/disnake/types/embed.py +++ b/disnake/types/embed.py @@ -22,6 +22,7 @@ class EmbedThumbnail(TypedDict): proxy_url: NotRequired[str] height: NotRequired[int] width: NotRequired[int] + flags: NotRequired[int] class EmbedVideo(TypedDict, total=False): @@ -36,6 +37,7 @@ class EmbedImage(TypedDict): proxy_url: NotRequired[str] height: NotRequired[int] width: NotRequired[int] + flags: NotRequired[int] class EmbedProvider(TypedDict, total=False): @@ -67,3 +69,4 @@ class Embed(TypedDict, total=False): provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] + flags: int diff --git a/disnake/types/message.py b/disnake/types/message.py index 6e0347bfeb..6d8eaddbdc 100644 --- a/disnake/types/message.py +++ b/disnake/types/message.py @@ -6,6 +6,7 @@ from typing_extensions import NotRequired +from .appinfo import AppInfo from .channel import ChannelType from .components import MessageTopLevelComponent from .embed import Embed @@ -47,6 +48,9 @@ class Attachment(TypedDict): duration_secs: NotRequired[float] waveform: NotRequired[str] flags: NotRequired[int] + clip_participants: NotRequired[List[User]] + clip_created_at: NotRequired[str] + application: NotRequired[Optional[AppInfo]] MessageActivityType = Literal[1, 2, 3, 5] diff --git a/docs/api/messages.rst b/docs/api/messages.rst index 64fde721c3..7361c67d3a 100644 --- a/docs/api/messages.rst +++ b/docs/api/messages.rst @@ -169,6 +169,22 @@ AttachmentFlags .. autoclass:: AttachmentFlags() :members: +EmbedFlags +~~~~~~~~~~ + +.. attributetable:: EmbedFlags + +.. autoclass:: EmbedFlags() + :members: + +EmbedMediaFlags +~~~~~~~~~~~~~~~ + +.. attributetable:: EmbedMediaFlags + +.. autoclass:: EmbedMediaFlags() + :members: + AllowedMentions ~~~~~~~~~~~~~~~