From e423ff401d71b6d32f30694c25dc331be2e8e2f7 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:48:20 +0100 Subject: [PATCH 1/7] feat: implement new flags --- disnake/embeds.py | 32 +++++++ disnake/flags.py | 191 +++++++++++++++++++++++++++++++++++++++ disnake/message.py | 28 ++++++ disnake/types/embed.py | 3 + disnake/types/message.py | 4 + docs/api/messages.rst | 16 ++++ 6 files changed, 274 insertions(+) diff --git a/disnake/embeds.py b/disnake/embeds.py index f243ae8e89..2605c8843e 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",) @@ -89,6 +90,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] @@ -182,6 +184,7 @@ class Embed: "_fields", "description", "_files", + "_flags", ) _default_colour: ClassVar[Optional[Colour]] = None @@ -220,6 +223,7 @@ def __init__( self._image: Optional[EmbedImagePayload] = None self._footer: Optional[EmbedFooterPayload] = None self._fields: Optional[List[EmbedFieldPayload]] = None + self._flags: Optional[int] = None self._files: Dict[_FileKey, File] = {} @@ -267,12 +271,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") return self @@ -371,6 +383,16 @@ def timestamp(self, value: Optional[datetime.datetime]) -> None: f"Expected datetime.datetime or None received {type(value).__name__} instead" ) + @property + def flags(self) -> Optional[EmbedFlags]: + """Optional[:class:`EmbedFlags`]: Returns the embed's flags. + + .. versionadded:: 2.11 + """ + if self._flags is None: + return + return EmbedFlags._from_value(self._flags) + @property def footer(self) -> _EmbedFooterProxy: """Returns an ``EmbedProxy`` denoting the footer contents. @@ -455,6 +477,11 @@ def image(self) -> _EmbedMediaProxy: - ``proxy_url`` - ``width`` - ``height`` + - ``flags`` + + .. versionchanged:: 2.11 + + Added the ``flags`` attribute. If an attribute is not set, it will be ``None``. """ @@ -508,6 +535,11 @@ def thumbnail(self) -> _EmbedMediaProxy: - ``proxy_url`` - ``width`` - ``height`` + - ``flags`` + + .. versionchanged:: 2.11 + + 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 c8434bc14a..a5aea58673 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -45,6 +45,8 @@ "SKUFlags", "ApplicationInstallTypes", "InteractionContextTypes", + "EmbedFlags", + "EmbedMediaFlags", ) BF = TypeVar("BF", bound="BaseFlags") @@ -2600,11 +2602,51 @@ class AttachmentFlags(BaseFlags): @_generated def __init__(self, *, is_remix: bool = ...) -> None: ... + @flag_value + def is_clip(self): + """:class:`bool`: Returns ``True`` if the attachment is a clip. + + .. versionadded:: 2.11 + """ + 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:: 2.11 + """ + return 1 << 1 + @flag_value def is_remix(self): """: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:: 2.11 + """ + return 1 << 3 + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. + + .. versionadded:: 2.11 + """ + return 1 << 4 + + @flag_value + def is_animated(self): + """:class:`bool`: Returns ``True`` if the attachment is an animated image. + + .. versionadded:: 2.11 + """ + return 1 << 5 + class SKUFlags(BaseFlags): """Wraps up Discord SKU flags. @@ -2892,3 +2934,152 @@ def bot_dm(self): def private_channel(self): """: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:: 2.11 + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @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:: 2.11 + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @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 9dcbdfe16b..7fa7752e5d 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -23,6 +23,7 @@ ) from . import utils +from .appinfo import AppInfo from .channel import PartialMessageable from .components import ActionRow, MessageComponent, _component_factory from .embeds import Embed @@ -307,6 +308,21 @@ 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:: 2.11 + + clip_created_at: Optional[:class:`datetime.datetime`] + If this attachment is a clip returns the creation timestamp. + + .. versionadded:: 2.11 + + application: Optional[:class:`AppInfo`] + If this attachment is a clip returns the application in the stream, if recognized. + + .. versionadded:: 2.11 """ __slots__ = ( @@ -325,6 +341,9 @@ class Attachment(Hashable): "duration", "waveform", "_flags", + "clip_participants", + "clip_created_at", + "application", ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState) -> None: @@ -345,6 +364,15 @@ 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") + ) + self.application: Optional[AppInfo] = ( + AppInfo(state=state, data=d) if (d := data.get("application")) else None + ) def is_spoiler(self) -> bool: """Whether this attachment contains a spoiler. 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 c3f8e4d1e9..c05a42c40d 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 Component 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 ~~~~~~~~~~~~~~~ From cad882ca8f75b44884e704cd705eeab4a5fdb5c7 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:51:39 +0100 Subject: [PATCH 2/7] add changelog entry --- changelog/1282.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1282.feature.rst 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`. From 5ec213516462da4aa97e6f3323af7ddd44b88b5a Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:18:55 +0100 Subject: [PATCH 3/7] run codemod --- disnake/flags.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/disnake/flags.py b/disnake/flags.py index a5aea58673..eeeecdc2e4 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -2600,7 +2600,16 @@ 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): @@ -3002,6 +3011,13 @@ class EmbedFlags(BaseFlags): 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.""" @@ -3079,6 +3095,11 @@ class EmbedMediaFlags(BaseFlags): 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.""" From b56f99ceedcede3ff5efc421992aaaf1760f8d01 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:23:49 +0200 Subject: [PATCH 4/7] make 0 the default value for the _flags member --- disnake/embeds.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/disnake/embeds.py b/disnake/embeds.py index 2605c8843e..18ff1567b3 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -223,7 +223,7 @@ def __init__( self._image: Optional[EmbedImagePayload] = None self._footer: Optional[EmbedFooterPayload] = None self._fields: Optional[List[EmbedFieldPayload]] = None - self._flags: Optional[int] = None + self._flags: int = 0 self._files: Dict[_FileKey, File] = {} @@ -284,7 +284,7 @@ def from_dict(cls, data: EmbedData) -> Self: self._footer = data.get("footer") self._fields = data.get("fields") - self._flags = data.get("flags") + self._flags = data.get("flags", 0) return self @@ -389,8 +389,6 @@ def flags(self) -> Optional[EmbedFlags]: .. versionadded:: 2.11 """ - if self._flags is None: - return return EmbedFlags._from_value(self._flags) @property From 46fc41b7c0efb44b3c97bb3834cbd9c6200a944c Mon Sep 17 00:00:00 2001 From: arielle Date: Mon, 8 Sep 2025 09:34:39 -0400 Subject: [PATCH 5/7] Apply suggestion from @Enegg Co-authored-by: Eneg <42005170+Enegg@users.noreply.github.com> Signed-off-by: arielle --- disnake/embeds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/embeds.py b/disnake/embeds.py index 18ff1567b3..ce7ea4d5df 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -384,8 +384,8 @@ def timestamp(self, value: Optional[datetime.datetime]) -> None: ) @property - def flags(self) -> Optional[EmbedFlags]: - """Optional[:class:`EmbedFlags`]: Returns the embed's flags. + def flags(self) -> EmbedFlags: + """:class:`EmbedFlags`: Returns the embed's flags. .. versionadded:: 2.11 """ From 9f617290a2723b4c282c875595456c4660229e3d Mon Sep 17 00:00:00 2001 From: arielle Date: Mon, 29 Sep 2025 01:02:27 -0400 Subject: [PATCH 6/7] fix: update all versionaddeds to use vnext --- disnake/embeds.py | 6 +++--- disnake/flags.py | 14 +++++++------- disnake/message.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/disnake/embeds.py b/disnake/embeds.py index a671fb4148..d9082da1f3 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -386,7 +386,7 @@ def timestamp(self, value: Optional[datetime.datetime]) -> None: def flags(self) -> EmbedFlags: """:class:`EmbedFlags`: Returns the embed's flags. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return EmbedFlags._from_value(self._flags) @@ -476,7 +476,7 @@ def image(self) -> _EmbedMediaProxy: - ``height`` - ``flags`` - .. versionchanged:: 2.11 + .. versionchanged:: |vnext| Added the ``flags`` attribute. @@ -534,7 +534,7 @@ def thumbnail(self) -> _EmbedMediaProxy: - ``height`` - ``flags`` - .. versionchanged:: 2.11 + .. versionchanged:: |vnext| Added the ``flags`` attribute. diff --git a/disnake/flags.py b/disnake/flags.py index e5241fcc15..7a6da26d09 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -2640,7 +2640,7 @@ def __init__( def is_clip(self): """:class:`bool`: Returns ``True`` if the attachment is a clip. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return 1 << 0 @@ -2648,7 +2648,7 @@ def is_clip(self): def is_thumbnail(self): """:class:`bool`: Returns ``True`` if the attachment is the thumbnail of a thread in a media channel. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return 1 << 1 @@ -2661,7 +2661,7 @@ def is_remix(self) -> int: def is_spoiler(self): """:class:`bool`: Returns ``True`` if the attachment was marked as a spoiler. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return 1 << 3 @@ -2669,7 +2669,7 @@ def is_spoiler(self): def contains_explicit_media(self): """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return 1 << 4 @@ -2677,7 +2677,7 @@ def contains_explicit_media(self): def is_animated(self): """:class:`bool`: Returns ``True`` if the attachment is an animated image. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ return 1 << 5 @@ -3027,7 +3027,7 @@ class EmbedFlags(BaseFlags): Returns an EmbedFlags instance with all flags except ``y`` inverted from their default value. - .. versionadded:: 2.11 + .. versionadded:: |vnext| Attributes ---------- @@ -3111,7 +3111,7 @@ class EmbedMediaFlags(BaseFlags): Returns an EmbedMediaFlags instance with all flags except ``y`` inverted from their default value. - .. versionadded:: 2.11 + .. versionadded:: |vnext| Attributes ---------- diff --git a/disnake/message.py b/disnake/message.py index 913a69c234..05e7b84874 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -331,17 +331,17 @@ class Attachment(Hashable): clip_participants: List[:class:`User`] If this attachment is a clip returns a list of users who were in the stream. - .. versionadded:: 2.11 + .. versionadded:: |vnext| clip_created_at: Optional[:class:`datetime.datetime`] If this attachment is a clip returns the creation timestamp. - .. versionadded:: 2.11 + .. versionadded:: |vnext| application: Optional[:class:`AppInfo`] If this attachment is a clip returns the application in the stream, if recognized. - .. versionadded:: 2.11 + .. versionadded:: |vnext| """ __slots__ = ( @@ -397,8 +397,12 @@ 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"" From b00e4d48731da6c176b678bb2e68cf788178619a Mon Sep 17 00:00:00 2001 From: arielle Date: Mon, 29 Sep 2025 01:02:51 -0400 Subject: [PATCH 7/7] remove clip_application --- disnake/message.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/disnake/message.py b/disnake/message.py index 05e7b84874..41db2ec699 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -24,7 +24,6 @@ ) from . import utils -from .appinfo import AppInfo from .channel import PartialMessageable from .components import MessageTopLevelComponent, _message_component_factory from .embeds import Embed @@ -336,11 +335,6 @@ class Attachment(Hashable): clip_created_at: Optional[:class:`datetime.datetime`] If this attachment is a clip returns the creation timestamp. - .. versionadded:: |vnext| - - application: Optional[:class:`AppInfo`] - If this attachment is a clip returns the application in the stream, if recognized. - .. versionadded:: |vnext| """ @@ -362,7 +356,6 @@ class Attachment(Hashable): "_flags", "clip_participants", "clip_created_at", - "application", ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState) -> None: @@ -389,9 +382,6 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState) -> None: self.clip_created_at: Optional[datetime.datetime] = utils.parse_time( data.get("clip_created_at") ) - self.application: Optional[AppInfo] = ( - AppInfo(state=state, data=d) if (d := data.get("application")) else None - ) def is_spoiler(self) -> bool: """Whether this attachment contains a spoiler.