diff --git a/discord/components.py b/discord/components.py index ef7d67670..a9a6de24b 100644 --- a/discord/components.py +++ b/discord/components.py @@ -177,13 +177,18 @@ class ActionRow(Component): ------------ children: List[Union[:class:`Button`, :class:`SelectMenu`, :class:`TextInput`]] The children components that this holds, if any. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ - __slots__: Tuple[str, ...] = ('children',) + __slots__: Tuple[str, ...] = ('children', 'id') __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ActionRowPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.children: List[ActionRowChildComponentType] = [] for component_data in data.get('components', []): @@ -198,10 +203,13 @@ class ActionRow(Component): return ComponentType.action_row def to_dict(self) -> ActionRowPayload: - return { + payload: ActionRowPayload = { 'type': self.type.value, 'components': [child.to_dict() for child in self.children], } + if self.id is not None: + payload['id'] = self.id + return payload class Button(Component): @@ -235,6 +243,10 @@ class Button(Component): The SKU ID this button sends you to, if available. .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -245,11 +257,13 @@ class Button(Component): 'label', 'emoji', 'sku_id', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ButtonComponentPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.style: ButtonStyle = try_enum(ButtonStyle, data['style']) self.custom_id: Optional[str] = data.get('custom_id') self.url: Optional[str] = data.get('url') @@ -278,6 +292,9 @@ class Button(Component): 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id + if self.sku_id: payload['sku_id'] = str(self.sku_id) @@ -329,6 +346,10 @@ class SelectMenu(Component): Whether the select is disabled or not. channel_types: List[:class:`.ChannelType`] A list of channel types that are allowed to be chosen in this select menu. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -341,6 +362,7 @@ class SelectMenu(Component): 'disabled', 'channel_types', 'default_values', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -357,6 +379,7 @@ class SelectMenu(Component): self.default_values: List[SelectDefaultValue] = [ SelectDefaultValue.from_dict(d) for d in data.get('default_values', []) ] + self.id: Optional[int] = data.get('id') def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { @@ -366,6 +389,8 @@ class SelectMenu(Component): 'max_values': self.max_values, 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id if self.placeholder: payload['placeholder'] = self.placeholder if self.options: @@ -531,6 +556,10 @@ class TextInput(Component): The minimum length of the text input. max_length: Optional[:class:`int`] The maximum length of the text input. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -542,6 +571,7 @@ class TextInput(Component): 'required', 'min_length', 'max_length', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -555,6 +585,7 @@ class TextInput(Component): self.required: bool = data.get('required', True) self.min_length: Optional[int] = data.get('min_length') self.max_length: Optional[int] = data.get('max_length') + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.text_input]: @@ -570,6 +601,9 @@ class TextInput(Component): 'required': self.required, } + if self.id is not None: + payload['id'] = self.id + if self.placeholder: payload['placeholder'] = self.placeholder @@ -721,11 +755,14 @@ class SectionComponent(Component): The components on this section. accessory: :class:`Component` The section accessory. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'components', 'accessory', + 'id', ) __repr_info__ = __slots__ @@ -733,6 +770,7 @@ class SectionComponent(Component): def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: self.components: List[SectionComponentType] = [] self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore + self.id: Optional[int] = data.get('id') for component_data in data['components']: component = _component_factory(component_data, state) @@ -749,6 +787,10 @@ class SectionComponent(Component): 'components': [c.to_dict() for c in self.components], 'accessory': self.accessory.to_dict(), } + + if self.id is not None: + payload['id'] = self.id + return payload @@ -772,12 +814,15 @@ class ThumbnailComponent(Component): The description shown within this thumbnail. spoiler: :class:`bool` Whether this thumbnail is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'media', 'spoiler', 'description', + 'id', ) __repr_info__ = __slots__ @@ -790,19 +835,25 @@ class ThumbnailComponent(Component): self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state) self.description: Optional[str] = data.get('description') self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.thumbnail]: return ComponentType.thumbnail def to_dict(self) -> ThumbnailComponentPayload: - return { - 'media': self.media.to_dict(), # pyright: ignore[reportReturnType] + payload = { + 'media': self.media.to_dict(), 'description': self.description, 'spoiler': self.spoiler, 'type': self.type.value, } + if self.id is not None: + payload['id'] = self.id + + return payload # type: ignore + class TextDisplay(Component): """Represents a text display from the Discord Bot UI Kit. @@ -820,24 +871,30 @@ class TextDisplay(Component): ---------- content: :class:`str` The content that this display shows. + id: Optional[:class:`int`] + The ID of this component. """ - __slots__ = ('content',) + __slots__ = ('content', 'id') __repr_info__ = __slots__ def __init__(self, data: TextComponentPayload) -> None: self.content: str = data['content'] + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.text_display]: return ComponentType.text_display def to_dict(self) -> TextComponentPayload: - return { + payload: TextComponentPayload = { 'type': self.type.value, 'content': self.content, } + if self.id is not None: + payload['id'] = self.id + return payload class UnfurledMediaItem(AssetMixin): @@ -1006,24 +1063,30 @@ class MediaGalleryComponent(Component): ---------- items: List[:class:`MediaGalleryItem`] The items this gallery has. + id: Optional[:class:`int`] + The ID of this component. """ - __slots__ = ('items',) + __slots__ = ('items', 'id') __repr_info__ = __slots__ def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None: self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.media_gallery]: return ComponentType.media_gallery def to_dict(self) -> MediaGalleryComponentPayload: - return { + payload: MediaGalleryComponentPayload = { 'type': self.type.value, 'items': [item.to_dict() for item in self.items], } + if self.id is not None: + payload['id'] = self.id + return payload class FileComponent(Component): @@ -1044,11 +1107,14 @@ class FileComponent(Component): The unfurled attachment contents of the file. spoiler: :class:`bool` Whether this file is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'media', 'spoiler', + 'id', ) __repr_info__ = __slots__ @@ -1056,17 +1122,21 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state) self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.file]: return ComponentType.file def to_dict(self) -> FileComponentPayload: - return { + payload: FileComponentPayload = { 'type': self.type.value, 'file': self.media.to_dict(), # type: ignore 'spoiler': self.spoiler, } + if self.id is not None: + payload['id'] = self.id + return payload class SeparatorComponent(Component): @@ -1087,11 +1157,14 @@ class SeparatorComponent(Component): The spacing size of the separator. visible: :class:`bool` Whether this separator is visible and shows a divider. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'spacing', 'visible', + 'id', ) __repr_info__ = __slots__ @@ -1102,17 +1175,21 @@ class SeparatorComponent(Component): ) -> None: self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1)) self.visible: bool = data.get('divider', True) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.separator]: return ComponentType.separator def to_dict(self) -> SeparatorComponentPayload: - return { + payload: SeparatorComponentPayload = { 'type': self.type.value, 'divider': self.visible, 'spacing': self.spacing.value, } + if self.id is not None: + payload['id'] = self.id + return payload class Container(Component): @@ -1133,10 +1210,13 @@ class Container(Component): This container's children. spoiler: :class:`bool` Whether this container is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None: self.children: List[Component] = [] + self.id: Optional[int] = data.get('id') for child in data['components']: comp = _component_factory(child, state) @@ -1158,6 +1238,18 @@ class Container(Component): accent_color = accent_colour + def to_dict(self) -> ContainerComponentPayload: + payload: ContainerComponentPayload = { + 'type': self.type.value, # type: ignore + 'spoiler': self.spoiler, + 'components': [c.to_dict() for c in self.children], + } + if self.id is not None: + payload['id'] = self.id + if self._colour: + payload['accent_color'] = self._colour.value + return payload + def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]: if data['type'] == 1: diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 510d6175b..b13948899 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -234,10 +234,13 @@ class ActionRow(Item[V]): for item in self._children: components.append(item.to_component_dict()) - return { + base = { 'type': self.type.value, 'components': components, } + if self.id is not None: + base['id'] = self.id + return base def button( self, diff --git a/discord/ui/item.py b/discord/ui/item.py index bcee854a8..854affa39 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -127,8 +127,7 @@ class Item(Generic[V]): @property def id(self) -> Optional[int]: - """Optional[:class:`int`]: The ID of this component. - """ + """Optional[:class:`int`]: The ID of this component.""" return self._id @id.setter