Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -25,3 +25,6 @@ from .text_display import *
from .thumbnail import *
from .action_row import *
from .label import *
from .file_upload import *
from .radio import *
from .checkbox import *

View file

@ -24,12 +24,12 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import copy
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
List,
@ -42,7 +42,7 @@ from typing import (
overload,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
from .button import Button, button as _button
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
from ..components import ActionRow as ActionRowComponent
@ -65,7 +65,6 @@ if TYPE_CHECKING:
)
from ..emoji import Emoji
from ..components import SelectOption
from ..interactions import Interaction
from .container import Container
from .dynamic import DynamicItem
@ -77,18 +76,6 @@ V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('ActionRow',)
class _ActionRowCallback:
__slots__ = ('row', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], row: ActionRow, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.row: ActionRow = row
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.row, interaction, self.item)
class ActionRow(Item[V]):
r"""Represents a UI action row.
@ -143,8 +130,9 @@ class ActionRow(Item[V]):
) -> None:
super().__init__()
self._children: List[Item[V]] = self._init_children()
self._children.extend(children)
self._weight: int = sum(i.width for i in self._children)
for child in children:
self.add_item(child)
if self._weight > 5:
raise ValueError('maximum number of children exceeded')
@ -173,8 +161,8 @@ class ActionRow(Item[V]):
for func in self.__action_row_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ActionRowCallback(func, self, item) # type: ignore
item._parent = getattr(func, '__discord_ui_parent__', self)
item.callback = _ItemCallback(func, self, item) # type: ignore
item._parent = self
setattr(self, func.__name__, item)
children.append(item)
return children
@ -184,6 +172,23 @@ class ActionRow(Item[V]):
for child in self._children:
child._view = view
def copy(self) -> ActionRow[V]:
new = copy.copy(self)
children = []
for child in new._children:
newch = child.copy()
newch._parent = new
if isinstance(newch.callback, _ItemCallback):
newch.callback.parent = new
children.append(newch)
new._children = children
new._parent = self._parent
new._update_view(self.view)
return new
def __deepcopy__(self, memo) -> ActionRow[V]:
return self.copy()
def _has_children(self):
return True
@ -227,8 +232,7 @@ class ActionRow(Item[V]):
An item in the action row.
"""
for child in self.children:
yield child
yield from self.children
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this action row."""

View file

@ -30,7 +30,7 @@ import inspect
import os
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..components import Button as ButtonComponent
@ -304,6 +304,9 @@ class Button(Item[V]):
sku_id=self.sku_id,
id=self.id,
)
if isinstance(new.callback, _ItemCallback):
new.callback.item = new
new._update_view(self.view)
return new
def __deepcopy__(self, memo) -> Self:

View file

@ -0,0 +1,391 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, TypeVar, Dict
import os
from ..utils import MISSING
from ..components import CheckboxGroupComponent, CheckboxComponent, CheckboxGroupOption
from ..enums import ComponentType
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..interactions import Interaction
from ..types.interactions import (
ModalSubmitCheckboxGroupInteractionData as ModalSubmitCheckboxGroupInteractionDataPayload,
ModalSubmitCheckboxInteractionData as ModalSubmitCheckboxInteractionDataPayload,
)
from ..types.components import (
CheckboxGroupComponent as CheckboxGroupComponentPayload,
CheckboxComponent as CheckboxComponentPayload,
)
from .view import BaseView
from ..app_commands.namespace import ResolveKey
# fmt: off
__all__ = (
'CheckboxGroup',
'Checkbox',
)
# fmt: on
V = TypeVar('V', bound='BaseView', covariant=True)
class CheckboxGroup(Item[V]):
"""Represents a checkbox group component within a modal.
.. versionadded:: 2.7
Parameters
------------
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
custom_id: Optional[:class:`str`]
The custom ID of the component.
options: List[:class:`discord.CheckboxGroupOption`]
A list of options that can be selected in this checkbox group.
Can only contain up to 10 items.
max_values: Optional[:class:`int`]
The maximum number of options that can be selected in this component.
Must be between 1 and 10. Defaults to 1.
min_values: Optional[:class:`int`]
The minimum number of options that must be selected in this component.
Must be between 0 and 10. Defaults to 0.
required: :class:`bool`
Whether this component is required to be filled before submitting the modal.
Defaults to ``True``.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'id',
'custom_id',
'options',
'required',
)
def __init__(
self,
*,
custom_id: str = MISSING,
required: bool = True,
min_values: Optional[int] = None,
max_values: Optional[int] = None,
options: List[CheckboxGroupOption] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
self._underlying: CheckboxGroupComponent = CheckboxGroupComponent._raw_construct(
id=id,
custom_id=custom_id,
required=required,
options=options or [],
min_values=min_values,
max_values=max_values,
)
self.id = id
self._values: List[str] = []
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def values(self) -> List[str]:
"""List[:class:`str`]: A list of values that have been selected by the user."""
return self._values
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the component that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def type(self) -> Literal[ComponentType.checkbox_group]:
""":class:`.ComponentType`: The type of this component."""
return ComponentType.checkbox_group
@property
def options(self) -> List[CheckboxGroupOption]:
"""List[:class:`discord.CheckboxGroupOption`]: A list of options that can be selected in this menu."""
return self._underlying.options
@options.setter
def options(self, value: List[CheckboxGroupOption]) -> None:
if not isinstance(value, list) or not all(isinstance(obj, CheckboxGroupOption) for obj in value):
raise TypeError('options must be a list of CheckboxGroupOption')
self._underlying.options = value
@property
def min_values(self) -> int:
""":class:`int`: The minimum number of options that must be selected before submitting the modal."""
return self._underlying.min_values
@min_values.setter
def min_values(self, value: int) -> None:
self._underlying.min_values = int(value)
@property
def max_values(self) -> int:
""":class:`int`: The maximum number of options that can be selected before submitting the modal."""
return self._underlying.max_values
@max_values.setter
def max_values(self, value: int) -> None:
self._underlying.max_values = int(value)
def add_option(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
default: bool = False,
) -> None:
"""Adds an option to the checkbox group.
To append a pre-existing :class:`discord.CheckboxGroupOption` use the
:meth:`append_option` method instead.
Parameters
-----------
label: :class:`str`
The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str`
The value of the option. This is not displayed to users.
If not given, defaults to the label.
Can only be up to 100 characters.
description: Optional[:class:`str`]
An additional description of the option, if any.
Can only be up to 100 characters.
default: :class:`bool`
Whether this option is selected by default.
Raises
-------
ValueError
The number of options exceeds 10.
"""
option = CheckboxGroupOption(
label=label,
value=value,
description=description,
default=default,
)
self.append_option(option)
def append_option(self, option: CheckboxGroupOption) -> None:
"""Appends an option to the checkbox group.
Parameters
-----------
option: :class:`discord.CheckboxGroupOption`
The option to append to the checkbox group.
Raises
-------
ValueError
The number of options exceeds 10.
"""
if len(self._underlying.options) >= 10:
raise ValueError('maximum number of options already provided (10)')
self._underlying.options.append(option)
@property
def required(self) -> bool:
""":class:`bool`: Whether the component is required or not."""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = bool(value)
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> CheckboxGroupComponentPayload:
return self._underlying.to_dict()
def _refresh_component(self, component: CheckboxGroupComponent) -> None:
self._underlying = component
def _handle_submit(
self, interaction: Interaction, data: ModalSubmitCheckboxGroupInteractionDataPayload, resolved: Dict[ResolveKey, Any]
) -> None:
self._values = data.get('values', [])
@classmethod
def from_component(cls, component: CheckboxGroupComponent) -> Self:
self = cls(
id=component.id,
custom_id=component.custom_id,
options=component.options,
required=component.required,
min_values=component.min_values,
max_values=component.max_values,
)
return self
def is_dispatchable(self) -> bool:
return False
class Checkbox(Item[V]):
"""Represents a checkbox component within a modal.
.. versionadded:: 2.7
Parameters
------------
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
custom_id: Optional[:class:`str`]
The custom ID of the component.
default: :class:`bool`
Whether this checkbox is selected by default.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'id',
'custom_id',
'default',
)
def __init__(
self,
*,
custom_id: str = MISSING,
default: bool = False,
id: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
self._underlying: CheckboxComponent = CheckboxComponent._raw_construct(
id=id,
custom_id=custom_id,
default=default,
)
self.id = id
self._value: bool = default
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def value(self) -> bool:
""":class:`bool`: ``True`` if this checkbox was selected, otherwise ``False``."""
return self._value
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the component that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def type(self) -> Literal[ComponentType.checkbox]:
""":class:`.ComponentType`: The type of this component."""
return ComponentType.checkbox
@property
def default(self) -> bool:
""":class:`bool`: Whether this checkbox is selected by default."""
return self._underlying.default
@default.setter
def default(self, value: bool) -> None:
self._underlying.default = bool(value)
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> CheckboxComponentPayload:
return self._underlying.to_dict()
def _refresh_component(self, component: CheckboxComponent) -> None:
self._underlying = component
def _handle_submit(
self, interaction: Interaction, data: ModalSubmitCheckboxInteractionDataPayload, resolved: Dict[ResolveKey, Any]
) -> None:
self._value = data.get('value', False)
@classmethod
def from_component(cls, component: CheckboxComponent) -> Self:
self = cls(
id=component.id,
custom_id=component.custom_id,
default=component.default,
)
return self
def is_dispatchable(self) -> bool:
return False

View file

@ -29,7 +29,6 @@ from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Coroutine,
Dict,
Generator,
List,
@ -39,7 +38,7 @@ from typing import (
Union,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
from .view import _component_to_item, LayoutView
from ..enums import ComponentType
from ..utils import get as _utils_get
@ -49,7 +48,6 @@ if TYPE_CHECKING:
from typing_extensions import Self
from ..components import Container as ContainerComponent
from ..interactions import Interaction
from .dynamic import DynamicItem
S = TypeVar('S', bound='Container', covariant=True)
@ -58,18 +56,6 @@ V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Container',)
class _ContainerCallback:
__slots__ = ('container', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], container: Container, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.container: Container = container
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.container, interaction, self.item)
class Container(Item[V]):
r"""Represents a UI container.
@ -163,7 +149,7 @@ class Container(Item[V]):
# action rows can be created inside containers, and then callbacks can exist here
# so we create items based off them
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ContainerCallback(raw, self, item) # type: ignore
item.callback = _ItemCallback(raw, self, item) # type: ignore
setattr(self, raw.__name__, item)
# this should not fail because in order for a function to be here it should be from
# an action row and must have passed the check in __init_subclass__, but still
@ -196,6 +182,15 @@ class Container(Item[V]):
child._update_view(view)
return True
def copy(self) -> Container[V]:
new = copy.deepcopy(self)
for child in new._children:
newch = child.copy()
newch._parent = new
new._parent = self._parent
new._update_view(self.view)
return new
def _has_children(self):
return True

View file

@ -100,7 +100,15 @@ class File(Item[V]):
spoiler=bool(spoiler),
id=id,
)
self.id = id
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this file component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
def _is_v2(self):
return True

View file

@ -0,0 +1,199 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, TypeVar, Dict
import os
from ..utils import MISSING
from ..components import FileUploadComponent
from ..enums import ComponentType
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..message import Attachment
from ..interactions import Interaction
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitFileUploadInteractionDataPayload
from ..types.components import FileUploadComponent as FileUploadComponentPayload
from .view import BaseView
from ..app_commands.namespace import ResolveKey
# fmt: off
__all__ = (
'FileUpload',
)
# fmt: on
V = TypeVar('V', bound='BaseView', covariant=True)
class FileUpload(Item[V]):
"""Represents a file upload component within a modal.
.. versionadded:: 2.7
Parameters
------------
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
custom_id: Optional[:class:`str`]
The custom ID of the file upload component.
max_values: Optional[:class:`int`]
The maximum number of files that can be uploaded in this component.
Must be between 1 and 10. Defaults to 1.
min_values: Optional[:class:`int`]
The minimum number of files that must be uploaded in this component.
Must be between 0 and 10. Defaults to 0.
required: :class:`bool`
Whether this component is required to be filled before submitting the modal.
Defaults to ``True``.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'id',
'custom_id',
'max_values',
'min_values',
'required',
)
def __init__(
self,
*,
custom_id: str = MISSING,
required: bool = True,
min_values: Optional[int] = None,
max_values: Optional[int] = None,
id: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
self._underlying: FileUploadComponent = FileUploadComponent._raw_construct(
id=id,
custom_id=custom_id,
max_values=max_values,
min_values=min_values,
required=required,
)
self.id = id
self._values: List[Attachment] = []
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def values(self) -> List[Attachment]:
"""List[:class:`discord.Attachment`]: The list of attachments uploaded by the user.
You can call :meth:`~discord.Attachment.to_file` on each attachment
to get a :class:`~discord.File` for sending.
"""
return self._values
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the component that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def min_values(self) -> int:
""":class:`int`: The minimum number of files that must be user upload before submitting the modal."""
return self._underlying.min_values
@min_values.setter
def min_values(self, value: int) -> None:
self._underlying.min_values = int(value)
@property
def max_values(self) -> int:
""":class:`int`: The maximum number of files that the user must upload before submitting the modal."""
return self._underlying.max_values
@max_values.setter
def max_values(self, value: int) -> None:
self._underlying.max_values = int(value)
@property
def required(self) -> bool:
""":class:`bool`: Whether the component is required or not."""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = bool(value)
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> FileUploadComponentPayload:
return self._underlying.to_dict()
def _refresh_component(self, component: FileUploadComponent) -> None:
self._underlying = component
def _handle_submit(
self, interaction: Interaction, data: ModalSubmitFileUploadInteractionDataPayload, resolved: Dict[ResolveKey, Any]
) -> None:
self._values = [v for k, v in resolved.items() if k.id in data.get('values', [])]
@classmethod
def from_component(cls, component: FileUploadComponent) -> Self:
self = cls(
id=component.id,
custom_id=component.custom_id,
max_values=component.max_values,
min_values=component.min_values,
required=component.required,
)
return self
@property
def type(self) -> Literal[ComponentType.file_upload]:
return self._underlying.type
def is_dispatchable(self) -> bool:
return False

View file

@ -55,6 +55,21 @@ ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
ContainedItemCallbackType = Callable[[C, Interaction[Any], I], Coroutine[Any, Any, Any]]
class _ItemCallback:
__slots__ = ('parent', 'callback', 'item')
def __init__(self, callback: ContainedItemCallbackType[Any, Any], parent: Any, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.parent: Any = parent
self.item: Item[Any] = item
def __repr__(self) -> str:
return f'<ItemCallback callback={self.callback!r} parent={self.parent!r} item={self.item!r}>'
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.parent, interaction, self.item)
class Item(Generic[V]):
"""Represents the base UI item that all UI components inherit from.
@ -72,6 +87,9 @@ class Item(Generic[V]):
- :class:`discord.ui.TextDisplay`
- :class:`discord.ui.Thumbnail`
- :class:`discord.ui.Label`
- :class:`discord.ui.RadioGroup`
- :class:`discord.ui.CheckboxGroup`
- :class:`discord.ui.Checkbox`
.. versionadded:: 2.0
"""

View file

@ -139,3 +139,8 @@ class Label(Item[V]):
def is_dispatchable(self) -> bool:
return False
@property
def _total_count(self) -> int:
# Count the component and ourselves
return 2

View file

@ -75,11 +75,11 @@ class Modal(BaseView):
from discord import ui
class Questionnaire(ui.Modal, title='Questionnaire Response'):
name = ui.TextInput(label='Name')
answer = ui.TextInput(label='Answer', style=discord.TextStyle.paragraph)
name = ui.Label(text='Name', component=ui.TextInput())
answer = ui.Label(text='Answer', component=ui.TextInput(style=discord.TextStyle.paragraph))
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.send_message(f'Thanks for your response, {self.name}!', ephemeral=True)
await interaction.response.send_message(f'Thanks for your response, {self.name.component.value}!', ephemeral=True)
Parameters
-----------
@ -223,7 +223,7 @@ class Modal(BaseView):
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
return item._rendered_row or item.row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []

View file

@ -0,0 +1,246 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, TypeVar, Dict
import os
from ..utils import MISSING
from ..components import RadioGroupComponent, RadioGroupOption
from ..enums import ComponentType
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..interactions import Interaction
from ..types.interactions import (
ModalSubmitRadioGroupInteractionData as ModalSubmitRadioGroupInteractionDataPayload,
)
from ..types.components import RadioGroupComponent as RadioGroupComponentPayload
from .view import BaseView
from ..app_commands.namespace import ResolveKey
# fmt: off
__all__ = (
'RadioGroup',
)
# fmt: on
V = TypeVar('V', bound='BaseView', covariant=True)
class RadioGroup(Item[V]):
"""Represents a radio group component within a modal.
.. versionadded:: 2.7
Parameters
------------
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
custom_id: Optional[:class:`str`]
The custom ID of the component.
options: List[:class:`discord.RadioGroupOption`]
A list of options that can be selected in this radio group.
Can contain between 2 and 10 items.
required: :class:`bool`
Whether this component is required to be filled before submitting the modal.
Defaults to ``True``.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'id',
'custom_id',
'options',
'required',
)
def __init__(
self,
*,
custom_id: str = MISSING,
required: bool = True,
options: List[RadioGroupOption] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
self._underlying: RadioGroupComponent = RadioGroupComponent._raw_construct(
id=id,
custom_id=custom_id,
required=required,
options=options or [],
)
self.id = id
self._value: Optional[str] = None
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def value(self) -> Optional[str]:
"""Optional[:class:`str`]: The value have been selected by the user, if any."""
return self._value
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the component that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def type(self) -> Literal[ComponentType.radio_group]:
""":class:`.ComponentType`: The type of this component."""
return ComponentType.radio_group
@property
def options(self) -> List[RadioGroupOption]:
"""List[:class:`discord.RadioGroupOption`]: A list of options that can be selected in this radio group."""
return self._underlying.options
@options.setter
def options(self, value: List[RadioGroupOption]) -> None:
if not isinstance(value, list) or not all(isinstance(obj, RadioGroupOption) for obj in value):
raise TypeError('options must be a list of RadioGroupOption')
self._underlying.options = value
def add_option(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
default: bool = False,
) -> None:
"""Adds an option to the group.
To append a pre-existing :class:`discord.RadioGroupOption` use the
:meth:`append_option` method instead.
Parameters
-----------
label: :class:`str`
The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str`
The value of the option. This is not displayed to users.
If not given, defaults to the label.
Can only be up to 100 characters.
description: Optional[:class:`str`]
An additional description of the option, if any.
Can only be up to 100 characters.
default: :class:`bool`
Whether this option is selected by default.
Raises
-------
ValueError
The number of options exceeds 10.
"""
option = RadioGroupOption(
label=label,
value=value,
description=description,
default=default,
)
self.append_option(option)
def append_option(self, option: RadioGroupOption) -> None:
"""Appends an option to the group.
Parameters
-----------
option: :class:`discord.RadioGroupOption`
The option to append to the group.
Raises
-------
ValueError
The number of options exceeds 10.
"""
if len(self._underlying.options) >= 10:
raise ValueError('maximum number of options already provided (10)')
self._underlying.options.append(option)
@property
def required(self) -> bool:
""":class:`bool`: Whether the component is required or not."""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = bool(value)
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> RadioGroupComponentPayload:
return self._underlying.to_dict()
def _refresh_component(self, component: RadioGroupComponent) -> None:
self._underlying = component
def _handle_submit(
self, interaction: Interaction, data: ModalSubmitRadioGroupInteractionDataPayload, resolved: Dict[ResolveKey, Any]
) -> None:
self._value = data.get('value')
@classmethod
def from_component(cls, component: RadioGroupComponent) -> Self:
self = cls(
id=component.id,
custom_id=component.custom_id,
options=component.options,
required=component.required,
)
return self
def is_dispatchable(self) -> bool:
return False

View file

@ -138,8 +138,7 @@ class Section(Item[V]):
An item in this section.
"""
for child in self.children:
yield child
yield from self.children
yield self.accessory
def _update_view(self, view) -> None:

View file

@ -39,10 +39,11 @@ from typing import (
Sequence,
)
from contextvars import ContextVar
import copy
import inspect
import os
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
from ..enums import ChannelType, ComponentType, SelectDefaultValueType
from ..partial_emoji import PartialEmoji
from ..emoji import Emoji
@ -70,7 +71,7 @@ __all__ = (
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeGuard
from typing_extensions import TypeAlias, TypeGuard, Self
from .view import BaseView
from .action_row import ActionRow
@ -269,6 +270,14 @@ class BaseSelect(Item[V]):
self.row = row
self._values: List[PossibleValue] = []
def copy(self) -> Self:
new = copy.copy(self)
if isinstance(new.callback, _ItemCallback):
new.callback.item = new
new._parent = self._parent
new._update_view(self.view)
return new
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this select."""
@ -497,10 +506,8 @@ class Select(BaseSelect[V]):
@options.setter
def options(self, value: List[SelectOption]) -> None:
if not isinstance(value, list):
if not isinstance(value, list) or not all(isinstance(obj, SelectOption) for obj in value):
raise TypeError('options must be a list of SelectOption')
if not all(isinstance(obj, SelectOption) for obj in value):
raise TypeError('all list items must subclass SelectOption')
self._underlying.options = value
@ -567,7 +574,7 @@ class Select(BaseSelect[V]):
"""
if len(self._underlying.options) >= 25:
raise ValueError('maximum number of options already provided')
raise ValueError('maximum number of options already provided (25)')
self._underlying.options.append(option)

View file

@ -83,6 +83,15 @@ class Separator(Item[V]):
def _is_v2(self):
return True
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this separator."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def visible(self) -> bool:
""":class:`bool`: Whether this separator is visible.

View file

@ -146,11 +146,19 @@ class TextInput(Item[V]):
id=id,
)
self.row = row
self.id = id
def __str__(self) -> str:
return self.value
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this text input."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the text input that gets received during an interaction."""

View file

@ -28,7 +28,6 @@ from typing import (
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
Iterator,
@ -50,7 +49,7 @@ import sys
import time
import os
from .item import Item, ItemCallbackType
from .item import Item, ItemCallbackType, _ItemCallback
from .select import Select
from .dynamic import DynamicItem
from ..components import (
@ -83,6 +82,7 @@ if TYPE_CHECKING:
import re
from ..interactions import Interaction
from .._types import ClientT
from ..message import Message
from ..types.components import ComponentBase as ComponentBasePayload
from ..types.interactions import (
@ -207,16 +207,22 @@ class _ViewWeights:
self.weights = [0, 0, 0, 0, 0]
class _ViewCallback:
__slots__ = ('view', 'callback', 'item')
class _ViewCacheSnapshot:
__slots__ = ('items', 'dynamic_items')
def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.view: BaseView = view
self.item: Item[BaseView] = item
def __init__(self) -> None:
self.items: Set[Tuple[int, str]] = set()
self.dynamic_items: Set[re.Pattern[str]] = set()
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.view, interaction, self.item)
@classmethod
def diff(cls, older: _ViewCacheSnapshot, newer: _ViewCacheSnapshot) -> Self:
self = cls()
self.items = older.items - newer.items
self.dynamic_items = older.dynamic_items - newer.dynamic_items
return self
def __repr__(self) -> str:
return f'<_ViewCacheSnapshot items={self.items!r} dynamic_items={self.dynamic_items!r}>'
class BaseView:
@ -232,7 +238,15 @@ class BaseView:
self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
self.__snapshot: Optional[_ViewCacheSnapshot] = None
try:
loop = asyncio.get_running_loop()
except RuntimeError:
self.__stopped: Optional[asyncio.Future[bool]] = None
else:
self.__stopped: Optional[asyncio.Future[bool]] = loop.create_future()
self._total_children: int = len(tuple(self.walk_children()))
def _is_layout(self) -> bool:
@ -252,13 +266,13 @@ class BaseView:
item._update_view(self)
parent = getattr(item, '__discord_ui_parent__', None)
if parent and parent._view is None:
parent._view = self
parent._update_view(self)
children.append(item)
parents[raw] = item
else:
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(raw, self, item) # type: ignore
item._view = self
item.callback = _ItemCallback(raw, self, item) # type: ignore
item._update_view(self)
if isinstance(item, Select):
item.options = [option.copy() for option in item.options]
setattr(self, raw.__name__, item)
@ -331,6 +345,31 @@ class BaseView:
def _add_count(self, value: int) -> None:
self._total_children = max(0, self._total_children + value)
@property
def _snapshot(self) -> Optional[_ViewCacheSnapshot]:
return self.__snapshot
def _get_snapshot_diff(self) -> Optional[_ViewCacheSnapshot]:
if self.__snapshot is None:
self.__snapshot = self._get_snapshot()
return None
newer = self._get_snapshot()
diff = _ViewCacheSnapshot.diff(older=self.__snapshot, newer=newer)
# Update our snapshot to the newer version after diffing it
self.__snapshot = newer
return diff
def _get_snapshot(self) -> _ViewCacheSnapshot:
snapshot = _ViewCacheSnapshot()
for item in self.walk_children():
if isinstance(item, DynamicItem):
snapshot.dynamic_items.add(item.__discord_ui_compiled_template__)
elif item.is_dispatchable():
custom_id = item.custom_id # type: ignore
snapshot.items.add((item.type.value, custom_id))
return snapshot
@property
def children(self) -> List[Item[Self]]:
"""List[:class:`Item`]: The list of children attached to this view."""
@ -452,6 +491,7 @@ class BaseView:
pass
else:
self._add_count(-item._total_count)
item._update_view(None)
return self
@ -461,6 +501,9 @@ class BaseView:
This function returns the class instance to allow for fluent-style
chaining.
"""
for child in self._children:
child._update_view(None)
self._children.clear()
self._total_children = 0
return self
@ -487,7 +530,7 @@ class BaseView:
"""
return _utils_get(self.walk_children(), id=id)
async def interaction_check(self, interaction: Interaction, /) -> bool:
async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool:
"""|coro|
A callback that is called when an interaction happens within the view
@ -522,7 +565,7 @@ class BaseView:
"""
pass
async def on_error(self, interaction: Interaction, error: Exception, item: Item[Any], /) -> None:
async def on_error(self, interaction: Interaction[ClientT], error: Exception, item: Item[Any], /) -> None:
"""|coro|
A callback that is called when an item's callback or :meth:`interaction_check`
@ -541,7 +584,7 @@ class BaseView:
"""
_log.error('Ignoring exception in view %r for item %r', self, item, exc_info=error)
async def _scheduled_task(self, item: Item, interaction: Interaction):
async def _scheduled_task(self, item: Item[Any], interaction: Interaction[ClientT]):
try:
item._refresh_state(interaction, interaction.data) # type: ignore
@ -566,7 +609,7 @@ class BaseView:
self.__timeout_task = asyncio.create_task(self.__timeout_task_impl())
def _dispatch_timeout(self):
if self.__stopped.done():
if self.__stopped is None or self.__stopped.done():
return
if self.__cancel_callback:
@ -576,9 +619,9 @@ class BaseView:
self.__stopped.set_result(True)
asyncio.create_task(self.on_timeout(), name=f'discord-ui-view-timeout-{self.id}')
def _dispatch_item(self, item: Item, interaction: Interaction) -> Optional[asyncio.Task[None]]:
if self.__stopped.done():
return
def _dispatch_item(self, item: Item[Any], interaction: Interaction[ClientT]) -> Optional[asyncio.Task[None]]:
if self.__stopped is None or self.__stopped.done():
return None
return asyncio.create_task(self._scheduled_task(item, interaction), name=f'discord-ui-view-dispatch-{self.id}')
@ -609,7 +652,7 @@ class BaseView:
This operation cannot be undone.
"""
if not self.__stopped.done():
if self.__stopped is not None and not self.__stopped.done():
self.__stopped.set_result(False)
self.__timeout_expiry = None
@ -623,6 +666,9 @@ class BaseView:
def is_finished(self) -> bool:
""":class:`bool`: Whether the view has finished interacting."""
if self.__stopped is None:
return False
return self.__stopped.done()
def is_dispatching(self) -> bool:
@ -651,6 +697,9 @@ class BaseView:
If ``True``, then the view timed out. If ``False`` then
the view finished normally.
"""
if self.__stopped is None:
self.__stopped = asyncio.get_running_loop().create_future()
return await self.__stopped
def walk_children(self) -> Generator[Item[Any], None, None]:
@ -757,6 +806,8 @@ class View(BaseView):
pass
else:
self.__weights.remove_item(item)
item._update_view(None)
return self
def clear_items(self) -> Self:
@ -892,8 +943,9 @@ class ViewStore:
self._modals[view.custom_id] = view # type: ignore
return
dispatch_info = self._views.setdefault(message_id, {})
dispatch_info = self._views.get(message_id, {})
is_fully_dynamic = True
snapshot = view._get_snapshot_diff()
for item in view.walk_children():
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
@ -902,26 +954,34 @@ class ViewStore:
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
is_fully_dynamic = False
if snapshot is not None:
for key in snapshot.items:
dispatch_info.pop(key, None)
for key in snapshot.dynamic_items:
self._dynamic_items.pop(key, None)
view._cache_key = message_id
if dispatch_info:
self._views[message_id] = dispatch_info
if message_id is not None and not is_fully_dynamic:
self._synced_message_views[message_id] = view
def remove_view(self, view: View) -> None:
def remove_view(self, view: BaseView) -> None:
if view.__discord_ui_modal__:
self._modals.pop(view.custom_id, None) # type: ignore
return
dispatch_info = self._views.get(view._cache_key)
if dispatch_info:
for item in view._children:
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None)
elif item.is_dispatchable():
dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
snapshot = view._snapshot
if dispatch_info and snapshot:
for key in snapshot.items:
dispatch_info.pop(key, None)
for key in snapshot.dynamic_items:
self._dynamic_items.pop(key, None)
if len(dispatch_info) == 0:
self._views.pop(view._cache_key, None)
if dispatch_info is not None and len(dispatch_info) == 0:
self._views.pop(view._cache_key, None)
self._synced_message_views.pop(view._cache_key, None) # type: ignore
@ -929,7 +989,7 @@ class ViewStore:
self,
component_type: int,
factory: Type[DynamicItem[Item[Any]]],
interaction: Interaction,
interaction: Interaction[ClientT],
custom_id: str,
match: re.Match[str],
) -> None:
@ -980,7 +1040,7 @@ class ViewStore:
except Exception:
_log.exception('Ignoring exception in dynamic item callback for %r', item)
def dispatch_dynamic_items(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
def dispatch_dynamic_items(self, component_type: int, custom_id: str, interaction: Interaction[ClientT]) -> None:
for pattern, item in self._dynamic_items.items():
match = pattern.fullmatch(custom_id)
if match is not None:
@ -991,17 +1051,14 @@ class ViewStore:
)
)
def dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
def dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction[ClientT]) -> None:
self.dispatch_dynamic_items(component_type, custom_id, interaction)
interaction_id: Optional[int] = None
message_id: Optional[int] = None
# Realistically, in a component based interaction the Interaction.message will never be None
# However, this guard is just in case Discord screws up somehow
msg = interaction.message
if msg is not None:
message_id = msg.id
if msg.interaction_metadata:
interaction_id = msg.interaction_metadata.id
key = (component_type, custom_id)
@ -1010,21 +1067,6 @@ class ViewStore:
if message_id is not None:
item = self._views.get(message_id, {}).get(key)
if item is None and interaction_id is not None:
try:
items = self._views.pop(interaction_id)
except KeyError:
item = None
else:
item = items.get(key)
# If we actually got the items, then these keys should probably be moved
# to the proper message_id instead of the interaction_id as they are now.
# An interaction_id is only used as a temporary stop gap for
# InteractionResponse.send_message so multiple view instances do not
# override each other.
# NOTE: Fix this mess if /callback endpoint ever gets proper return types
self._views.setdefault(message_id, {}).update(items)
if item is None:
# Fallback to None message_id searches in case a persistent view
# was added without an associated message_id
@ -1034,15 +1076,18 @@ class ViewStore:
if item is None:
return
# Note, at this point the View is *not* None
task = item.view._dispatch_item(item, interaction) # type: ignore
if item.view is None:
_log.warning('View interaction referencing unknown view for item %s. Discarding', item)
return
task = item.view._dispatch_item(item, interaction)
if task is not None:
self.add_task(task)
def dispatch_modal(
self,
custom_id: str,
interaction: Interaction,
interaction: Interaction[ClientT],
components: List[ModalSubmitComponentInteractionDataPayload],
resolved: ResolvedDataPayload,
) -> None:
@ -1053,11 +1098,6 @@ class ViewStore:
self.add_task(modal._dispatch_submit(interaction, components, resolved))
def remove_interaction_mapping(self, interaction_id: int) -> None:
# This is called before re-adding the view
self._views.pop(interaction_id, None)
self._synced_message_views.pop(interaction_id, None)
def is_message_tracked(self, message_id: int) -> bool:
return message_id in self._synced_message_views