From 75134562fd1be352994721b1e91577cd2b81f798 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:13:21 +0100 Subject: [PATCH] feat: First components v2 commit --- discord/attachment.py | 178 +++++++++++++++++++----------------- discord/components.py | 6 +- discord/types/attachment.py | 4 +- discord/types/components.py | 4 +- discord/ui/section.py | 7 +- 5 files changed, 104 insertions(+), 95 deletions(-) diff --git a/discord/attachment.py b/discord/attachment.py index 2be4eac1a..45dab6c74 100644 --- a/discord/attachment.py +++ b/discord/attachment.py @@ -29,12 +29,19 @@ from typing import TYPE_CHECKING, Any, Optional, Union from .mixins import Hashable from .file import File -from .state import ConnectionState from .flags import AttachmentFlags +from .enums import MediaLoadingState, try_enum from . import utils if TYPE_CHECKING: - from .types.attachment import Attachment as AttachmentPayload + from .types.attachment import ( + AttachmentBase as AttachmentBasePayload, + Attachment as AttachmentPayload, + UnfurledAttachment as UnfurledAttachmentPayload, + ) + + from .http import HTTPClient + from .state import ConnectionState MISSING = utils.MISSING @@ -45,7 +52,40 @@ __all__ = ( class AttachmentBase: - url: str + + __slots__ = ( + 'url', + 'proxy_url', + 'description', + 'filename', + 'spoiler', + 'height', + 'width', + 'content_type', + '_flags', + '_http', + '_state', + ) + + def __init__(self, data: AttachmentBasePayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self._http: HTTPClient = state.http + self.url: str = data['url'] + self.proxy_url: str = data['proxy_url'] + self.description: Optional[str] = data.get('description') + self.spoiler: bool = data.get('spoiler', False) + self.height: Optional[int] = data.get('height') + self.width: Optional[int] = data.get('width') + self.content_type: Optional[str] = data.get('content_type') + self._flags: int = data.get('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: The attachment's flag value.""" + return AttachmentFlags._from_value(self._flags) + + def __str__(self) -> str: + return self.url or '' async def save( self, @@ -200,6 +240,22 @@ class AttachmentBase: spoiler=spoiler, ) + def to_dict(self): + base = { + 'url': self.url, + 'proxy_url': self.proxy_url, + 'spoiler': self.spoiler, + } + + if self.width: + base['width'] = self.width + if self.height: + base['height'] = self.height + if self.description: + base['description'] = self.description + + return base + class Attachment(Hashable, AttachmentBase): """Represents an attachment from Discord. @@ -268,56 +324,34 @@ class Attachment(Hashable, AttachmentBase): The normalised version of the attachment's filename. .. versionadded:: 2.5 + spoiler: :class:`bool` + Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned + data. + + .. versionadded:: 2.6 """ __slots__ = ( 'id', 'size', - 'height', - 'width', - 'filename', - 'url', - 'proxy_url', - '_http', - 'content_type', - 'description', 'ephemeral', 'duration', 'waveform', - '_flags', 'title', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.id: int = int(data['id']) - self.size: int = data['size'] - self.height: Optional[int] = data.get('height') - self.width: Optional[int] = data.get('width') self.filename: str = data['filename'] - self.url: str = data['url'] - self.proxy_url: str = data['proxy_url'] - self._http = state.http - self.content_type: Optional[str] = data.get('content_type') - self.description: Optional[str] = data.get('description') + self.size: int = data['size'] self.ephemeral: bool = data.get('ephemeral', False) self.duration: Optional[float] = data.get('duration_secs') self.title: Optional[str] = data.get('title') - - waveform = data.get('waveform') - self.waveform: Optional[bytes] = ( - utils._base64_to_bytes(waveform) if waveform is not None else None - ) - - self._flags: int = data.get('flags', 0) - - @property - def flags(self) -> AttachmentFlags: - """:class:`AttachmentFlags`: The attachment's flags.""" - return AttachmentFlags._from_value(self._flags) + super().__init__(data, state) def is_spoiler(self) -> bool: """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.filename.startswith('SPOILER_') + return self.spoiler or self.filename.startswith('SPOILER_') def is_voice_message(self) -> bool: """:class:`bool`: Whether this attachment is a voice message.""" @@ -326,33 +360,18 @@ class Attachment(Hashable, AttachmentBase): def __repr__(self) -> str: return f'' - def __str__(self) -> str: - return self.url or '' - def to_dict(self) -> AttachmentPayload: - result: AttachmentPayload = { - 'filename': self.filename, - 'id': self.id, - 'proxy_url': self.proxy_url, - 'size': self.size, - 'url': self.url, - 'spoiler': self.is_spoiler(), - } - if self.height: - result['height'] = self.height - if self.width: - result['width'] = self.width - if self.content_type: - result['content_type'] = self.content_type - if self.description is not None: - result['description'] = self.description + result: AttachmentPayload = super().to_dict() # pyright: ignore[reportAssignmentType] + result['id'] = self.id + result['filename'] = self.filename + result['size'] = self.size return result class UnfurledAttachment(AttachmentBase): """Represents an unfurled attachment item from a :class:`Component`. - .. versionadded:: tbd + .. versionadded:: 2.6 .. container:: operations @@ -370,48 +389,35 @@ class UnfurledAttachment(AttachmentBase): Attributes ---------- + height: Optional[:class:`int`] + The attachment's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The attachment's width, in pixels. Only applicable to images and videos. url: :class:`str` - The unfurled attachment URL. - proxy_url: Optional[:class:`str`] - The proxy URL. This is cached version of the :attr:`~UnfurledAttachment.url` in the + The attachment URL. If the message this attachment was attached + to is deleted, then this will 404. + proxy_url: :class:`str` + The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the case of images. When the message is deleted, this URL might be valid for a few minutes or not valid at all. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. - height: Optional[:class:`int`] - The unfurled attachment's height, in pixels. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. - width: Optional[:class:`int`] - The unfurled attachment's width, in pixels. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. content_type: Optional[:class:`str`] The attachment's `media type `_ - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + spoiler: :class:`bool` + Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned + data. loading_state: :class:`MediaLoadingState` - The load state of this attachment on Discord side. - description + The cache state of this unfurled attachment. """ __slots__ = ( - 'url', - 'proxy_url', - 'height', - 'width', - 'content_type', 'loading_state', - '_resolved', - '_state', ) - def __init__(self, ) + def __init__(self, data: UnfurledAttachmentPayload, state: ConnectionState) -> None: + self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data['loading_state']) + super().__init__(data, state) + + def __repr__(self) -> str: + return f'' diff --git a/discord/components.py b/discord/components.py index a0fd1148d..1a40d3d0b 100644 --- a/discord/components.py +++ b/discord/components.py @@ -690,7 +690,7 @@ class SectionComponent(Component): The user constructible and usable type to create a section is :class:`discord.ui.Section` not this one. - .. versionadded:: tbd + .. versionadded:: 2.6 Attributes ---------- @@ -732,7 +732,7 @@ class TextDisplay(Component): This inherits from :class:`Component`. - .. versionadded:: tbd + .. versionadded:: 2.6 Parameters ---------- @@ -770,7 +770,7 @@ class ThumbnailComponent(Component): The user constructuble and usable type to create a thumbnail component is :class:`discord.ui.Thumbnail` not this one. - .. versionadded:: tbd + .. versionadded:: 2.6 Attributes ---------- diff --git a/discord/types/attachment.py b/discord/types/attachment.py index 38d8ad667..20fcd8e1b 100644 --- a/discord/types/attachment.py +++ b/discord/types/attachment.py @@ -49,10 +49,8 @@ class Attachment(AttachmentBase): ephemeral: NotRequired[bool] duration_secs: NotRequired[float] waveform: NotRequired[str] + title: NotRequired[str] class UnfurledAttachment(AttachmentBase): loading_state: LoadingState - src_is_animated: NotRequired[bool] - placeholder: str - placeholder_version: int diff --git a/discord/types/components.py b/discord/types/components.py index 4521f2514..c169a5286 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -143,12 +143,12 @@ class ThumbnailComponent(ComponentBase, UnfurledAttachment): class MediaGalleryComponent(ComponentBase): type: Literal[12] - items: List[MediaItem] + items: List[UnfurledAttachment] class FileComponent(ComponentBase): type: Literal[13] - file: MediaItem + file: UnfurledAttachment spoiler: NotRequired[bool] diff --git a/discord/ui/section.py b/discord/ui/section.py index 0f6f76006..fc8a9e142 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -26,6 +26,9 @@ from __future__ import annotations from typing import List, Optional from .item import Item +from ..components import SectionComponent + +__all__ = ('Section',) class Section(Item): @@ -47,4 +50,6 @@ class Section(Item): def __init__(self, *, accessory: Optional[Item]) -> None: self.accessory: Optional[Item] = accessory self._children: List[Item] = [] - self._underlying = SectionComponent + self._underlying = SectionComponent._raw_construct( + accessory=accessory, + )