From 714b2f6e8f402c3adc476e10bdc63ea321bb275e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:40:48 +0100 Subject: [PATCH 01/15] base types and flags --- discord/enums.py | 7 +++++ discord/flags.py | 30 ++++++++++++------- discord/types/components.py | 57 ++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e1086651e9..0435f7920b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -712,6 +712,13 @@ class ComponentType(Enum): role_select = 6 mentionable_select = 7 channel_select = 8 + section = 9 + text_display = 10 + thumbnail = 11 + media_gallery = 12 + file = 13 + separator = 14 + container = 17 def __int__(self): return self.value diff --git a/discord/flags.py b/discord/flags.py index 7073a56e35..bd6370af05 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -333,22 +333,22 @@ class MessageFlags(BaseFlags): @flag_value def crossposted(self): """:class:`bool`: Returns ``True`` if the message is the original crossposted message.""" - return 1 + return 1 << 0 @flag_value def is_crossposted(self): """:class:`bool`: Returns ``True`` if the message was crossposted from another channel.""" - return 2 + return 1 << 1 @flag_value def suppress_embeds(self): """:class:`bool`: Returns ``True`` if the message's embeds have been suppressed.""" - return 4 + return 1 << 2 @flag_value def source_message_deleted(self): """:class:`bool`: Returns ``True`` if the source message for this crosspost has been deleted.""" - return 8 + return 1 << 3 @flag_value def urgent(self): @@ -356,7 +356,7 @@ def urgent(self): An urgent message is one sent by Discord Trust and Safety. """ - return 16 + return 1 << 4 @flag_value def has_thread(self): @@ -364,7 +364,7 @@ def has_thread(self): .. versionadded:: 2.0 """ - return 32 + return 1 << 5 @flag_value def ephemeral(self): @@ -372,7 +372,7 @@ def ephemeral(self): .. versionadded:: 2.0 """ - return 64 + return 1 << 6 @flag_value def loading(self): @@ -382,7 +382,7 @@ def loading(self): .. versionadded:: 2.0 """ - return 128 + return 1 << 7 @flag_value def failed_to_mention_some_roles_in_thread(self): @@ -390,7 +390,7 @@ def failed_to_mention_some_roles_in_thread(self): .. versionadded:: 2.0 """ - return 256 + return 1 << 8 @flag_value def suppress_notifications(self): @@ -401,7 +401,7 @@ def suppress_notifications(self): .. versionadded:: 2.4 """ - return 4096 + return 1 << 12 @flag_value def is_voice_message(self): @@ -409,7 +409,15 @@ def is_voice_message(self): .. versionadded:: 2.5 """ - return 8192 + return 1 << 13 + + @flag_value + def is_components_v2(self): + """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content` and `embeds`. + + .. versionadded:: 2.7 + """ + return 1 << 15 @fill_with_flags() diff --git a/discord/types/components.py b/discord/types/components.py index 7b05f8bf08..3c59c322e4 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -33,9 +33,10 @@ from .emoji import PartialEmoji from .snowflake import Snowflake -ComponentType = Literal[1, 2, 3, 4] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] +SeparatorSpacingSize = Literal[1, 2] class ActionRow(TypedDict): @@ -85,4 +86,58 @@ class SelectMenu(TypedDict): custom_id: str +class TextDisplay(TypedDict): + type: Literal[10] + content: str + + +class UnfurledMediaItem: + url: str + + +class MediaGalleryItem(TypedDict): + media: UnfurledMediaItem + description: NotRequired[str] + spoiler: NotRequired[bool] + + +class SectionComponent(TypedDict): + type: Literal[9] + components: list[TextDisplayComponent, ButtonComponent] + + +class ThumbnailComponent(TypedDict): + type: Literal[11] + media: UnfurledMediaItem + description: NotRequired[str] + spoiler: NotRequired[bool] + + +class MediaGalleryComponent(TypedDict): + type: Literal[12] + items: list[MediaGalleryItem] + + +class FileComponent(TypedDict): + type: Literal[13] + file: UnfurledMediaItem + spoiler: NotRequired[bool] + + +class SeparatorComponent(TypedDict): + type: Literal[14] + divider: NotRequired[bool] + spacing: NotRequired[SeparatorSpacingSize] + + +ContainerComponents = Union[ActionRow, TextDisplayComponent, MediaGalleryComponent, FileComponent, SeparatorComponent, SectionComponent] + + +class ContainerComponent(TypedDict): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: list[ContainerComponents] + + Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] From 468f996e29671d582fc6e68e84536e283676de41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 04:42:42 +0000 Subject: [PATCH 02/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/types/components.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/discord/types/components.py b/discord/types/components.py index 3c59c322e4..3bca251724 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -130,7 +130,14 @@ class SeparatorComponent(TypedDict): spacing: NotRequired[SeparatorSpacingSize] -ContainerComponents = Union[ActionRow, TextDisplayComponent, MediaGalleryComponent, FileComponent, SeparatorComponent, SectionComponent] +ContainerComponents = Union[ + ActionRow, + TextDisplayComponent, + MediaGalleryComponent, + FileComponent, + SeparatorComponent, + SectionComponent, +] class ContainerComponent(TypedDict): From 905b9ff5e2d8fbc736ba6a6954fe58343a21cc0f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:56:19 +0100 Subject: [PATCH 03/15] textdisplayComponent --- discord/types/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/components.py b/discord/types/components.py index 3bca251724..4360798bdc 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -86,7 +86,7 @@ class SelectMenu(TypedDict): custom_id: str -class TextDisplay(TypedDict): +class TextDisplayComponent(TypedDict): type: Literal[10] content: str From 49080e7d4727f4dd7d700a1d8d1e34674778c07e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:10:05 +0100 Subject: [PATCH 04/15] more --- discord/enums.py | 7 +++++ discord/types/components.py | 59 ++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 0435f7920b..16d4aa9e07 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1070,6 +1070,13 @@ class SubscriptionStatus(Enum): inactive = 2 +class SeparatorSpacingSize(Enum): + """A separator component's spacing size.""" + + small = 1 + large = 2 + + T = TypeVar("T") diff --git a/discord/types/components.py b/discord/types/components.py index 4360798bdc..5a275a6f34 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -39,12 +39,17 @@ SeparatorSpacingSize = Literal[1, 2] -class ActionRow(TypedDict): +class BaseComponent(TypedDict): + type: ComponentType + id: NotRequired[int] + + +class ActionRow(BaseComponent): type: Literal[1] components: list[Component] -class ButtonComponent(TypedDict): +class ButtonComponent(BaseComponent): custom_id: NotRequired[str] url: NotRequired[str] disabled: NotRequired[bool] @@ -55,7 +60,7 @@ class ButtonComponent(TypedDict): sku_id: Snowflake -class InputText(TypedDict): +class InputText(BaseComponent): min_length: NotRequired[int] max_length: NotRequired[int] required: NotRequired[bool] @@ -75,7 +80,7 @@ class SelectOption(TypedDict): default: bool -class SelectMenu(TypedDict): +class SelectMenu(BaseComponent): placeholder: NotRequired[str] min_values: NotRequired[int] max_values: NotRequired[int] @@ -86,50 +91,60 @@ class SelectMenu(TypedDict): custom_id: str -class TextDisplayComponent(TypedDict): +class TextDisplayComponent(BaseComponent): type: Literal[10] content: str -class UnfurledMediaItem: +class SectionComponent(BaseComponent): + type: Literal[9] + components: list[TextDisplayComponent, ButtonComponent] + + +class UnfurledMediaItem(TypedDict): url: str -class MediaGalleryItem(TypedDict): +class ThumbnailComponent(BaseComponent): + type: Literal[11] media: UnfurledMediaItem description: NotRequired[str] spoiler: NotRequired[bool] -class SectionComponent(TypedDict): - type: Literal[9] - components: list[TextDisplayComponent, ButtonComponent] - - -class ThumbnailComponent(TypedDict): - type: Literal[11] +class MediaGalleryItem(TypedDict): media: UnfurledMediaItem description: NotRequired[str] spoiler: NotRequired[bool] -class MediaGalleryComponent(TypedDict): +class MediaGalleryComponent(BaseComponent): type: Literal[12] items: list[MediaGalleryItem] -class FileComponent(TypedDict): +class FileComponent(BaseComponent): type: Literal[13] file: UnfurledMediaItem spoiler: NotRequired[bool] -class SeparatorComponent(TypedDict): +class SeparatorComponent(BaseComponent): type: Literal[14] divider: NotRequired[bool] spacing: NotRequired[SeparatorSpacingSize] +class ContainerComponent(BaseComponent): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: list[ContainerComponents] + + +Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] + + ContainerComponents = Union[ ActionRow, TextDisplayComponent, @@ -138,13 +153,3 @@ class SeparatorComponent(TypedDict): SeparatorComponent, SectionComponent, ] - - -class ContainerComponent(TypedDict): - type: Literal[17] - accent_color: NotRequired[int] - spoiler: NotRequired[bool] - components: list[ContainerComponents] - - -Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] From e3e7aba78321ef8f3a0d56503207020dcc9ca5c5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:57:39 +0100 Subject: [PATCH 05/15] Section, TextDisplay --- discord/components.py | 120 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/discord/components.py b/discord/components.py index c80eb5a57c..448a65f562 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,7 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, try_enum +from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, SeparatorSpacingSize, try_enum from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -36,9 +36,19 @@ from .types.components import ActionRow as ActionRowPayload from .types.components import ButtonComponent as ButtonComponentPayload from .types.components import Component as ComponentPayload + from .types.components import BaseComponent as BaseComponentPayload from .types.components import InputText as InputTextComponentPayload from .types.components import SelectMenu as SelectMenuPayload from .types.components import SelectOption as SelectOptionPayload + from .types.components import TextDisplayComponent as TextDisplayComponentPayload + from .types.components import SectionComponent as SectionComponentPayload + from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload + from .types.components import ThumbnailComponent as ThumbnailComponentPayload + from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .types.components import FileComponent as FileComponentPayload + from .types.components import SeparatorComponent as SeparatorComponentPayload + from .types.components import ContainerComponent as ContainerComponentPayload __all__ = ( "Component", @@ -47,11 +57,12 @@ "SelectMenu", "SelectOption", "InputText", + "Section", + "TextDisplay", ) C = TypeVar("C", bound="Component") - class Component: """Represents a Discord Bot UI Kit Component. @@ -69,12 +80,15 @@ class Component: ---------- type: :class:`ComponentType` The type of component. + id: :class:`str` + The component's ID. """ - __slots__: tuple[str, ...] = ("type",) + __slots__: tuple[str, ...] = ("type", "id") __repr_info__: ClassVar[tuple[str, ...]] type: ComponentType + id: str def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) @@ -494,16 +508,100 @@ def to_dict(self) -> SelectOptionPayload: return payload +class Section(Component): + """Represents a Section from Components V2. + + This is a component that contains other components such as :class:`TextDisplay` and :class:`Thumbnail`. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + components: List[:class:`Component`] + The components contained in this section. + accessory: Optional[:class:`Component`] + The accessory attached to this Section. + """ + + __slots__: tuple[str, ...] = ("components", "accessory") + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: SectionComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.components: list[Component] = [_component_factory(d) for d in data.get("components", [])] + self.accessory: Component | None = None + if _accessory := data.get("accessory"): + self.accessory = _component_factory(_accessory) + + def to_dict(self) -> SectionComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components] + } + if self.accessory: + payload["accessory"] = self.accessory.to_dict() + return payload + + +class TextDisplay(Component): + """Represents a Text Display from Components V2. + + This is a component that displays text. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + content: :class:`str` + The component's text content. + """ + + __slots__: tuple[str, ...] = ("content",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: TextDisplayComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.content: str = data.get("content") + + def to_dict(self) -> TextDisplayComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "content": self.content + } + + +COMPONENT_MAPPINGS = { + 1: ActionRow, + 2: Button, + 3: SelectMenu, + 4: InputText, + 5: SelectMenu, + 6: SelectMenu, + 7: SelectMenu, + 8: SelectMenu, + 9: Section, + 10: TextDisplay, + 11: None, + 12: None, + 13: None, + 14: None, + 17: None, +} + def _component_factory(data: ComponentPayload) -> Component: component_type = data["type"] - if component_type == 1: - return ActionRow(data) - elif component_type == 2: - return Button(data) # type: ignore - elif component_type == 4: - return InputText(data) # type: ignore - elif component_type in (3, 5, 6, 7, 8): - return SelectMenu(data) # type: ignore + if cls := COMPONENT_MAPPINGS.get(component_type): + return cls(data) else: as_enum = try_enum(ComponentType, component_type) return Component._raw_construct(type=as_enum) From e961db542e8d2309a9318293f19d503fe1fcab0c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:58:03 +0000 Subject: [PATCH 06/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/discord/components.py b/discord/components.py index 448a65f562..d8e49a789d 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,28 +27,35 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, SeparatorSpacingSize, try_enum +from .enums import ( + ButtonStyle, + ChannelType, + ComponentType, + InputTextStyle, + SeparatorSpacingSize, + try_enum, +) from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji from .types.components import ActionRow as ActionRowPayload + from .types.components import BaseComponent as BaseComponentPayload from .types.components import ButtonComponent as ButtonComponentPayload from .types.components import Component as ComponentPayload - from .types.components import BaseComponent as BaseComponentPayload + from .types.components import ContainerComponent as ContainerComponentPayload + from .types.components import FileComponent as FileComponentPayload from .types.components import InputText as InputTextComponentPayload + from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import SectionComponent as SectionComponentPayload from .types.components import SelectMenu as SelectMenuPayload from .types.components import SelectOption as SelectOptionPayload + from .types.components import SeparatorComponent as SeparatorComponentPayload from .types.components import TextDisplayComponent as TextDisplayComponentPayload - from .types.components import SectionComponent as SectionComponentPayload - from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload from .types.components import ThumbnailComponent as ThumbnailComponentPayload - from .types.components import MediaGalleryItem as MediaGalleryItemPayload - from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload - from .types.components import FileComponent as FileComponentPayload - from .types.components import SeparatorComponent as SeparatorComponentPayload - from .types.components import ContainerComponent as ContainerComponentPayload + from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload __all__ = ( "Component", @@ -63,6 +70,7 @@ C = TypeVar("C", bound="Component") + class Component: """Represents a Discord Bot UI Kit Component. @@ -532,7 +540,9 @@ class Section(Component): def __init__(self, data: SectionComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.components: list[Component] = [_component_factory(d) for d in data.get("components", [])] + self.components: list[Component] = [ + _component_factory(d) for d in data.get("components", []) + ] self.accessory: Component | None = None if _accessory := data.get("accessory"): self.accessory = _component_factory(_accessory) @@ -541,7 +551,7 @@ def to_dict(self) -> SectionComponentPayload: payload = { "type": int(self.type), "id": self.id, - "components": [c.to_dict() for c in self.components] + "components": [c.to_dict() for c in self.components], } if self.accessory: payload["accessory"] = self.accessory.to_dict() @@ -573,11 +583,7 @@ def __init__(self, data: TextDisplayComponentPayload): self.content: str = data.get("content") def to_dict(self) -> TextDisplayComponentPayload: - return { - "type": int(self.type), - "id": self.id, - "content": self.content - } + return {"type": int(self.type), "id": self.id, "content": self.content} COMPONENT_MAPPINGS = { @@ -598,6 +604,7 @@ def to_dict(self) -> TextDisplayComponentPayload: 17: None, } + def _component_factory(data: ComponentPayload) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): From 947890d6fc72c0c22dee8eb99ebf45d5769404ed Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:40:37 +0100 Subject: [PATCH 07/15] remaining classes --- discord/components.py | 224 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 219 insertions(+), 5 deletions(-) diff --git a/discord/components.py b/discord/components.py index d8e49a789d..a0aa713243 100644 --- a/discord/components.py +++ b/discord/components.py @@ -35,6 +35,7 @@ SeparatorSpacingSize, try_enum, ) +from .colour import Colour from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -586,6 +587,219 @@ def to_dict(self) -> TextDisplayComponentPayload: return {"type": int(self.type), "id": self.id, "content": self.content} +class UnfurledMediaItem: + + def __init__(self, data: UnfurledMediaItemPayload): + self.url = data.get("url") + # need to test this more + + def to_dict(self): + return {"url": self.url} + + +class Thumbnail(Component): + """Represents a Thumbnail from Components V2. + + This is a component that displays media such as images and videos. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The component's media URL. + description: Optional[:class:`str`] + The thumbnail's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the thumbnail is a spoiler. + """ + + __slots__: tuple[str, ...] = ("media", "description", "spoiler", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: ThumbnailComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.description: str | None = data.get("description") + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self) -> ThumbnailComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "media": self.media.to_dict() + } + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGalleryItem: + + def __init__(self, data: MediaGalleryItemPayload): + self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.description: str | None = data.get("description") + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self): + payload = { + "media": self.media.to_dict() + } + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGallery(Component): + """Represents a Media Gallery from Components V2. + + This is a component that displays up to 10 different :class:`MediaGalleryItem`s. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + + __slots__: tuple[str, ...] = ("items", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: MediaGalleryComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.items: list[MediaGalleryItem] = [MediaGalleryItem(d) for d in data.get("items", [])] + + def to_dict(self) -> MediaGalleryComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "items": [i.to_dict() for i in self.items] + } + + +class FileComponent(Component): + """Represents a File from Components V2. + + This is a component that displays some file (elaborate?). + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + file: :class:`UnfurledMediaItem` + The file's media URL. + spoiler: Optional[:class:`bool`] + Whether the file is a spoiler. + """ + + __slots__: tuple[str, ...] = ("file", "spoiler", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: FileComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self) -> FileComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "file": self.file.to_dict() + } + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class Separator(Component): + """Represents a Separator from Components V2. + + This is a component that separates components. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + divider: :class:`bool` + Whether the separator is a divider (provide example?) + spacing: Optional[:class:`SeparatorSpacingSize`] + The separator's spacing size. + """ + + __slots__: tuple[str, ...] = ("divider", "spacing",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: SeparatorComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.divider: bool = data.get("divider") + self.spacing: SeparatorSpacingSize = try_enum(SeparatorSpacingSize, data.get("spacing", 1)) + + def to_dict(self) -> SeparatorComponentPayload: + return {"type": int(self.type), "id": self.id, "divider": self.divider, "spacing": int(self.spacing)} + + +class Container(Component): + """Represents a Container from Components V2. + + This is a component that contains up to 10 different :class:`Component`s. + It may only contain :class:`ActionRow`, :class:`TextDisplay`, :class:`Section`, :class:`MediaGallery`, :class:`Separator`, and :class:`FileComponent`. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + + __slots__: tuple[str, ...] = ("accent_color", "spoiler", "components", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: ContainerComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour(c) # at this point, not adding alternative spelling + self.spoiler: bool | None = data.get("spoiler") + self.components: list[Component] = [ + _component_factory(d) for d in data.get("components", []) + ] + + def to_dict(self) -> ContainerComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components], + } + if self.accent_color: + payload["accent_color"] = self.accent_color.value + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + COMPONENT_MAPPINGS = { 1: ActionRow, 2: Button, @@ -597,11 +811,11 @@ def to_dict(self) -> TextDisplayComponentPayload: 8: SelectMenu, 9: Section, 10: TextDisplay, - 11: None, - 12: None, - 13: None, - 14: None, - 17: None, + 11: Thumbnail, + 12: MediaGallery, + 13: FileComponent, + 14: Separator, + 17: Container, } From 6e7dde99b7ee10dae26d73762189f48181d3ac0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:41:30 +0000 Subject: [PATCH 08/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 77 +++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/discord/components.py b/discord/components.py index a0aa713243..2d6fe407c6 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from .colour import Colour from .enums import ( ButtonStyle, ChannelType, @@ -35,7 +36,6 @@ SeparatorSpacingSize, try_enum, ) -from .colour import Colour from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -616,23 +616,25 @@ class Thumbnail(Component): Whether the thumbnail is a spoiler. """ - __slots__: tuple[str, ...] = ("media", "description", "spoiler", ) + __slots__: tuple[str, ...] = ( + "media", + "description", + "spoiler", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: ThumbnailComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.media: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem(umi) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> ThumbnailComponentPayload: - payload = { - "type": int(self.type), - "id": self.id, - "media": self.media.to_dict() - } + payload = {"type": int(self.type), "id": self.id, "media": self.media.to_dict()} if self.description: payload["description"] = self.description if self.spoiler is not None: @@ -641,16 +643,16 @@ def to_dict(self) -> ThumbnailComponentPayload: class MediaGalleryItem: - + def __init__(self, data: MediaGalleryItemPayload): - self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.media: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem(umi) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") def to_dict(self): - payload = { - "media": self.media.to_dict() - } + payload = {"media": self.media.to_dict()} if self.description: payload["description"] = self.description if self.spoiler is not None: @@ -673,20 +675,22 @@ class MediaGallery(Component): The media this gallery contains. """ - __slots__: tuple[str, ...] = ("items", ) + __slots__: tuple[str, ...] = ("items",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: MediaGalleryComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.items: list[MediaGalleryItem] = [MediaGalleryItem(d) for d in data.get("items", [])] + self.items: list[MediaGalleryItem] = [ + MediaGalleryItem(d) for d in data.get("items", []) + ] def to_dict(self) -> MediaGalleryComponentPayload: return { "type": int(self.type), "id": self.id, - "items": [i.to_dict() for i in self.items] + "items": [i.to_dict() for i in self.items], } @@ -707,22 +711,23 @@ class FileComponent(Component): Whether the file is a spoiler. """ - __slots__: tuple[str, ...] = ("file", "spoiler", ) + __slots__: tuple[str, ...] = ( + "file", + "spoiler", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: FileComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem( + umi + ) self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> FileComponentPayload: - payload = { - "type": int(self.type), - "id": self.id, - "file": self.file.to_dict() - } + payload = {"type": int(self.type), "id": self.id, "file": self.file.to_dict()} if self.spoiler is not None: payload["spoiler"] = self.spoiler return payload @@ -745,17 +750,27 @@ class Separator(Component): The separator's spacing size. """ - __slots__: tuple[str, ...] = ("divider", "spacing",) + __slots__: tuple[str, ...] = ( + "divider", + "spacing", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.divider: bool = data.get("divider") - self.spacing: SeparatorSpacingSize = try_enum(SeparatorSpacingSize, data.get("spacing", 1)) + self.spacing: SeparatorSpacingSize = try_enum( + SeparatorSpacingSize, data.get("spacing", 1) + ) def to_dict(self) -> SeparatorComponentPayload: - return {"type": int(self.type), "id": self.id, "divider": self.divider, "spacing": int(self.spacing)} + return { + "type": int(self.type), + "id": self.id, + "divider": self.divider, + "spacing": int(self.spacing), + } class Container(Component): @@ -774,14 +789,20 @@ class Container(Component): The media this gallery contains. """ - __slots__: tuple[str, ...] = ("accent_color", "spoiler", "components", ) + __slots__: tuple[str, ...] = ( + "accent_color", + "spoiler", + "components", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: ContainerComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour(c) # at this point, not adding alternative spelling + self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour( + c + ) # at this point, not adding alternative spelling self.spoiler: bool | None = data.get("spoiler") self.components: list[Component] = [ _component_factory(d) for d in data.get("components", []) From d18a9c48091672110c9bf2aac0a3f0cf6cf7d224 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:52:29 +0100 Subject: [PATCH 09/15] basic view support start --- discord/types/components.py | 1 + discord/ui/section.py | 102 ++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 discord/ui/section.py diff --git a/discord/types/components.py b/discord/types/components.py index 5a275a6f34..d532a2b590 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -99,6 +99,7 @@ class TextDisplayComponent(BaseComponent): class SectionComponent(BaseComponent): type: Literal[9] components: list[TextDisplayComponent, ButtonComponent] + accessory: NotRequired[Component] class UnfurledMediaItem(TypedDict): diff --git a/discord/ui/section.py b/discord/ui/section.py new file mode 100644 index 0000000000..272f74d085 --- /dev/null +++ b/discord/ui/section.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from ..components import Section as SectionComponent +from ..enums import ComponentType +from .item import Item + +__all__ = ("InputText",) + +if TYPE_CHECKING: + from ..types.components import SectionComponent as SectionComponentPayload + + +class Section: + """Represents a UI section. + + .. versionadded:: 2.7 + + Parameters + ---------- + *items: :class:`Item` + The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + accessory: Optional[:class:`Item`] + This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + """ + + def __init__( + self, + *items: Item, + accessory: Item = None + ): + super().__init__() + + self.items = items + self.accessory = accessory + components = [] + + self._underlying = SectionComponent._raw_construct( + type=ComponentType.section, + components=components, + accessory=accessory, + ) + + def add_item(self, item: Item) -> None: + """Adds an item to the section. + + Parameters + ---------- + item: :class:`Item` + The item to add to the section. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self.items) >= 3: + raise ValueError("maximum number of children exceeded") + + if not isinstance(item, Item): + raise TypeError(f"expected Item not {item.__class__!r}") + + self.items.append(item) + + def add_text(self, content: str) -> None: + """Adds a :class:`TextDisplay` to the section. + + Parameters + ---------- + content: :class:`str` + The content of the TextDisplay + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of items has been exceeded (3). + """ + + if len(self.items) >= 3: + raise ValueError("maximum number of children exceeded") + + text = ... + + self.items.append(text) + + def add_button(self, label: str, ) -> None: + """finish""" + pass + + @property + def type(self) -> ComponentType: + return self._underlying.type + + def to_component_dict(self) -> SectionComponentPayload: + return self._underlying.to_dict() From 5ab45fd41d9d32dd8a0fc3717302455263f83e16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:52:51 +0000 Subject: [PATCH 10/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 272f74d085..ff54bb1e30 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING from ..components import Section as SectionComponent @@ -26,11 +25,7 @@ class Section: This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. """ - def __init__( - self, - *items: Item, - accessory: Item = None - ): + def __init__(self, *items: Item, accessory: Item = None): super().__init__() self.items = items @@ -85,14 +80,16 @@ def add_text(self, content: str) -> None: if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") - + text = ... self.items.append(text) - - def add_button(self, label: str, ) -> None: + + def add_button( + self, + label: str, + ) -> None: """finish""" - pass @property def type(self) -> ComponentType: From fb8d13d37db86bbd7749a013ec92889fa863fbfe Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:23:15 +0100 Subject: [PATCH 11/15] flag clarification --- discord/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/flags.py b/discord/flags.py index bd6370af05..1406bdcea0 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -413,7 +413,7 @@ def is_voice_message(self): @flag_value def is_components_v2(self): - """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content` and `embeds`. + """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content`, `embed`, and `embeds`. .. versionadded:: 2.7 """ From 65dc63d78289010059b233c05b3a3594a3242864 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 05:53:33 +0100 Subject: [PATCH 12/15] complete models --- discord/components.py | 11 ++++++++++- discord/enums.py | 1 + discord/types/components.py | 11 ++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 2d6fe407c6..89504a1ff6 100644 --- a/discord/components.py +++ b/discord/components.py @@ -38,6 +38,7 @@ ) from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots +from .flags import AttachmentFlags if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji @@ -591,7 +592,15 @@ class UnfurledMediaItem: def __init__(self, data: UnfurledMediaItemPayload): self.url = data.get("url") - # need to test this more + self.proxy_url: str = data.get("proxy_url") + self.height: int | None = data.get("height") + self.width: int | None = data.get("width") + self.content_type: str | None = data.get("content_type") + self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) + self.placeholder: str = data.get("placeholder") + self.placeholder_version: int = data.get("placeholder_version") + self.loading_state: int = data.get("loading_state") + self.src_is_animated: bool = data.get("src_is_animated") def to_dict(self): return {"url": self.url} diff --git a/discord/enums.py b/discord/enums.py index 16d4aa9e07..c52f3ee2ab 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -718,6 +718,7 @@ class ComponentType(Enum): media_gallery = 12 file = 13 separator = 14 + content_inventory_entry = 16 container = 17 def __int__(self): diff --git a/discord/types/components.py b/discord/types/components.py index d532a2b590..a689c09d0d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -33,7 +33,7 @@ from .emoji import PartialEmoji from .snowflake import Snowflake -ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] SeparatorSpacingSize = Literal[1, 2] @@ -104,6 +104,15 @@ class SectionComponent(BaseComponent): class UnfurledMediaItem(TypedDict): url: str + proxy_url: str + height: NotRequired[int | None] + width: NotRequired[int | None] + content_type: NotRequired[str] + src_is_animated: NotRequired[bool] + placeholder: str + placeholder_version: int + loading_state: int + flags: NotRequired[int] class ThumbnailComponent(BaseComponent): From f86f7078398caebe90963fa12e245139cba88b0f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 04:54:15 +0000 Subject: [PATCH 13/15] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 89504a1ff6..5a6cbfb01f 100644 --- a/discord/components.py +++ b/discord/components.py @@ -36,9 +36,9 @@ SeparatorSpacingSize, try_enum, ) +from .flags import AttachmentFlags from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots -from .flags import AttachmentFlags if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji From 3e03e842e6766ffa2933bb8d02f4d8c96607753f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:21:52 +0100 Subject: [PATCH 14/15] fix --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index ff54bb1e30..49b3de3f45 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -6,7 +6,7 @@ from ..enums import ComponentType from .item import Item -__all__ = ("InputText",) +__all__ = ("Section",) if TYPE_CHECKING: from ..types.components import SectionComponent as SectionComponentPayload From 0f8c20a1e379fc3c498310717f632e0d4ab67aec Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:24:22 +0100 Subject: [PATCH 15/15] fix2 --- discord/types/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/types/components.py b/discord/types/components.py index a689c09d0d..3bc75ef01d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -149,13 +149,13 @@ class ContainerComponent(BaseComponent): type: Literal[17] accent_color: NotRequired[int] spoiler: NotRequired[bool] - components: list[ContainerComponents] + components: list[AllowedContainerComponents] Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] -ContainerComponents = Union[ +AllowedContainerComponents = Union[ ActionRow, TextDisplayComponent, MediaGalleryComponent,