23 changed files with 135 additions and 1379 deletions
@ -1,15 +0,0 @@ |
|||
""" |
|||
discord.ui |
|||
~~~~~~~~~~~ |
|||
|
|||
Bot UI Kit helper for the Discord API |
|||
|
|||
:copyright: (c) 2015-present Rapptz |
|||
:license: MIT, see LICENSE for more details. |
|||
|
|||
""" |
|||
|
|||
from .view import * |
|||
from .item import * |
|||
from .button import * |
|||
from .select import * |
@ -1,290 +0,0 @@ |
|||
""" |
|||
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 Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union |
|||
import inspect |
|||
import os |
|||
|
|||
|
|||
from .item import Item, ItemCallbackType |
|||
from ..enums import ButtonStyle, ComponentType |
|||
from ..partial_emoji import PartialEmoji, _EmojiTag |
|||
from ..components import Button as ButtonComponent |
|||
|
|||
__all__ = ( |
|||
'Button', |
|||
'button', |
|||
) |
|||
|
|||
if TYPE_CHECKING: |
|||
from .view import View |
|||
from ..emoji import Emoji |
|||
|
|||
B = TypeVar('B', bound='Button') |
|||
V = TypeVar('V', bound='View', covariant=True) |
|||
|
|||
|
|||
class Button(Item[V]): |
|||
"""Represents a UI button. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Parameters |
|||
------------ |
|||
style: :class:`discord.ButtonStyle` |
|||
The style of the button. |
|||
custom_id: Optional[:class:`str`] |
|||
The ID of the button that gets received during an interaction. |
|||
If this button is for a URL, it does not have a custom ID. |
|||
url: Optional[:class:`str`] |
|||
The URL this button sends you to. |
|||
disabled: :class:`bool` |
|||
Whether the button is disabled or not. |
|||
label: Optional[:class:`str`] |
|||
The label of the button, if any. |
|||
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] |
|||
The emoji of the button, if available. |
|||
row: Optional[:class:`int`] |
|||
The relative row this button belongs to. A Discord component can only have 5 |
|||
rows. By default, items are arranged automatically into those 5 rows. If you'd |
|||
like to control the relative positioning of the row then passing an index is advised. |
|||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic |
|||
ordering. The row number must be between 0 and 4 (i.e. zero indexed). |
|||
""" |
|||
|
|||
__item_repr_attributes__: Tuple[str, ...] = ( |
|||
'style', |
|||
'url', |
|||
'disabled', |
|||
'label', |
|||
'emoji', |
|||
'row', |
|||
) |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
style: ButtonStyle = ButtonStyle.secondary, |
|||
label: Optional[str] = None, |
|||
disabled: bool = False, |
|||
custom_id: Optional[str] = None, |
|||
url: Optional[str] = None, |
|||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, |
|||
row: Optional[int] = None, |
|||
): |
|||
super().__init__() |
|||
if custom_id is not None and url is not None: |
|||
raise TypeError('cannot mix both url and custom_id with Button') |
|||
|
|||
self._provided_custom_id = custom_id is not None |
|||
if url is None and custom_id is None: |
|||
custom_id = os.urandom(16).hex() |
|||
|
|||
if url is not None: |
|||
style = ButtonStyle.link |
|||
|
|||
if emoji is not None: |
|||
if isinstance(emoji, str): |
|||
emoji = PartialEmoji.from_str(emoji) |
|||
elif isinstance(emoji, _EmojiTag): |
|||
emoji = emoji._to_partial() |
|||
else: |
|||
raise TypeError(f'expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}') |
|||
|
|||
self._underlying = ButtonComponent._raw_construct( |
|||
type=ComponentType.button, |
|||
custom_id=custom_id, |
|||
url=url, |
|||
disabled=disabled, |
|||
label=label, |
|||
style=style, |
|||
emoji=emoji, |
|||
) |
|||
self.row = row |
|||
|
|||
@property |
|||
def style(self) -> ButtonStyle: |
|||
""":class:`discord.ButtonStyle`: The style of the button.""" |
|||
return self._underlying.style |
|||
|
|||
@style.setter |
|||
def style(self, value: ButtonStyle): |
|||
self._underlying.style = value |
|||
|
|||
@property |
|||
def custom_id(self) -> Optional[str]: |
|||
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction. |
|||
|
|||
If this button is for a URL, it does not have a custom ID. |
|||
""" |
|||
return self._underlying.custom_id |
|||
|
|||
@custom_id.setter |
|||
def custom_id(self, value: Optional[str]): |
|||
if value is not None and not isinstance(value, str): |
|||
raise TypeError('custom_id must be None or str') |
|||
|
|||
self._underlying.custom_id = value |
|||
|
|||
@property |
|||
def url(self) -> Optional[str]: |
|||
"""Optional[:class:`str`]: The URL this button sends you to.""" |
|||
return self._underlying.url |
|||
|
|||
@url.setter |
|||
def url(self, value: Optional[str]): |
|||
if value is not None and not isinstance(value, str): |
|||
raise TypeError('url must be None or str') |
|||
self._underlying.url = value |
|||
|
|||
@property |
|||
def disabled(self) -> bool: |
|||
""":class:`bool`: Whether the button is disabled or not.""" |
|||
return self._underlying.disabled |
|||
|
|||
@disabled.setter |
|||
def disabled(self, value: bool): |
|||
self._underlying.disabled = bool(value) |
|||
|
|||
@property |
|||
def label(self) -> Optional[str]: |
|||
"""Optional[:class:`str`]: The label of the button, if available.""" |
|||
return self._underlying.label |
|||
|
|||
@label.setter |
|||
def label(self, value: Optional[str]): |
|||
self._underlying.label = str(value) if value is not None else value |
|||
|
|||
@property |
|||
def emoji(self) -> Optional[PartialEmoji]: |
|||
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available.""" |
|||
return self._underlying.emoji |
|||
|
|||
@emoji.setter |
|||
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore |
|||
if value is not None: |
|||
if isinstance(value, str): |
|||
self._underlying.emoji = PartialEmoji.from_str(value) |
|||
elif isinstance(value, _EmojiTag): |
|||
self._underlying.emoji = value._to_partial() |
|||
else: |
|||
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead') |
|||
else: |
|||
self._underlying.emoji = None |
|||
|
|||
@classmethod |
|||
def from_component(cls: Type[B], button: ButtonComponent) -> B: |
|||
return cls( |
|||
style=button.style, |
|||
label=button.label, |
|||
disabled=button.disabled, |
|||
custom_id=button.custom_id, |
|||
url=button.url, |
|||
emoji=button.emoji, |
|||
row=None, |
|||
) |
|||
|
|||
@property |
|||
def type(self) -> ComponentType: |
|||
return self._underlying.type |
|||
|
|||
def to_component_dict(self): |
|||
return self._underlying.to_dict() |
|||
|
|||
def is_dispatchable(self) -> bool: |
|||
return self.custom_id is not None |
|||
|
|||
def is_persistent(self) -> bool: |
|||
if self.style is ButtonStyle.link: |
|||
return self.url is not None |
|||
return super().is_persistent() |
|||
|
|||
def refresh_component(self, button: ButtonComponent) -> None: |
|||
self._underlying = button |
|||
|
|||
|
|||
def button( |
|||
*, |
|||
label: Optional[str] = None, |
|||
custom_id: Optional[str] = None, |
|||
disabled: bool = False, |
|||
style: ButtonStyle = ButtonStyle.secondary, |
|||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, |
|||
row: Optional[int] = None, |
|||
) -> Callable[[ItemCallbackType], ItemCallbackType]: |
|||
"""A decorator that attaches a button to a component. |
|||
|
|||
The function being decorated should have three parameters, ``self`` representing |
|||
the :class:`discord.ui.View`, the :class:`discord.ui.Button` being pressed and |
|||
the :class:`discord.Interaction` you receive. |
|||
|
|||
.. note:: |
|||
|
|||
Buttons with a URL cannot be created with this function. |
|||
Consider creating a :class:`Button` manually instead. |
|||
This is because buttons with a URL do not have a callback |
|||
associated with them since Discord does not do any processing |
|||
with it. |
|||
|
|||
Parameters |
|||
------------ |
|||
label: Optional[:class:`str`] |
|||
The label of the button, if any. |
|||
custom_id: Optional[:class:`str`] |
|||
The ID of the button that gets received during an interaction. |
|||
It is recommended not to set this parameter to prevent conflicts. |
|||
style: :class:`.ButtonStyle` |
|||
The style of the button. Defaults to :attr:`.ButtonStyle.grey`. |
|||
disabled: :class:`bool` |
|||
Whether the button is disabled or not. Defaults to ``False``. |
|||
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] |
|||
The emoji of the button. This can be in string form or a :class:`.PartialEmoji` |
|||
or a full :class:`.Emoji`. |
|||
row: Optional[:class:`int`] |
|||
The relative row this button belongs to. A Discord component can only have 5 |
|||
rows. By default, items are arranged automatically into those 5 rows. If you'd |
|||
like to control the relative positioning of the row then passing an index is advised. |
|||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic |
|||
ordering. The row number must be between 0 and 4 (i.e. zero indexed). |
|||
""" |
|||
|
|||
def decorator(func: ItemCallbackType) -> ItemCallbackType: |
|||
if not inspect.iscoroutinefunction(func): |
|||
raise TypeError('button function must be a coroutine function') |
|||
|
|||
func.__discord_ui_model_type__ = Button |
|||
func.__discord_ui_model_kwargs__ = { |
|||
'style': style, |
|||
'custom_id': custom_id, |
|||
'url': None, |
|||
'disabled': disabled, |
|||
'label': label, |
|||
'emoji': emoji, |
|||
'row': row, |
|||
} |
|||
return func |
|||
|
|||
return decorator |
@ -1,131 +0,0 @@ |
|||
""" |
|||
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 Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar |
|||
|
|||
from ..interactions import Interaction |
|||
|
|||
__all__ = ( |
|||
'Item', |
|||
) |
|||
|
|||
if TYPE_CHECKING: |
|||
from ..enums import ComponentType |
|||
from .view import View |
|||
from ..components import Component |
|||
|
|||
I = TypeVar('I', bound='Item') |
|||
V = TypeVar('V', bound='View', covariant=True) |
|||
ItemCallbackType = Callable[[Any, I, Interaction], Coroutine[Any, Any, Any]] |
|||
|
|||
|
|||
class Item(Generic[V]): |
|||
"""Represents the base UI item that all UI components inherit from. |
|||
|
|||
The current UI items supported are: |
|||
|
|||
- :class:`discord.ui.Button` |
|||
- :class:`discord.ui.Select` |
|||
|
|||
.. versionadded:: 2.0 |
|||
""" |
|||
|
|||
__item_repr_attributes__: Tuple[str, ...] = ('row',) |
|||
|
|||
def __init__(self): |
|||
self._view: Optional[V] = None |
|||
self._row: Optional[int] = None |
|||
self._rendered_row: Optional[int] = None |
|||
# This works mostly well but there is a gotcha with |
|||
# the interaction with from_component, since that technically provides |
|||
# a custom_id most dispatchable items would get this set to True even though |
|||
# it might not be provided by the library user. However, this edge case doesn't |
|||
# actually affect the intended purpose of this check because from_component is |
|||
# only called upon edit and we're mainly interested during initial creation time. |
|||
self._provided_custom_id: bool = False |
|||
|
|||
def to_component_dict(self) -> Dict[str, Any]: |
|||
raise NotImplementedError |
|||
|
|||
def refresh_component(self, component: Component) -> None: |
|||
return None |
|||
|
|||
def refresh_state(self, interaction: Interaction) -> None: |
|||
return None |
|||
|
|||
@classmethod |
|||
def from_component(cls: Type[I], component: Component) -> I: |
|||
return cls() |
|||
|
|||
@property |
|||
def type(self) -> ComponentType: |
|||
raise NotImplementedError |
|||
|
|||
def is_dispatchable(self) -> bool: |
|||
return False |
|||
|
|||
def is_persistent(self) -> bool: |
|||
return self._provided_custom_id |
|||
|
|||
def __repr__(self) -> str: |
|||
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__) |
|||
return f'<{self.__class__.__name__} {attrs}>' |
|||
|
|||
@property |
|||
def row(self) -> Optional[int]: |
|||
return self._row |
|||
|
|||
@row.setter |
|||
def row(self, value: Optional[int]): |
|||
if value is None: |
|||
self._row = None |
|||
elif 5 > value >= 0: |
|||
self._row = value |
|||
else: |
|||
raise ValueError('row cannot be negative or greater than or equal to 5') |
|||
|
|||
@property |
|||
def width(self) -> int: |
|||
return 1 |
|||
|
|||
@property |
|||
def view(self) -> Optional[V]: |
|||
"""Optional[:class:`View`]: The underlying view for this item.""" |
|||
return self._view |
|||
|
|||
async def callback(self, interaction: Interaction): |
|||
"""|coro| |
|||
|
|||
The callback associated with this UI item. |
|||
|
|||
This can be overriden by subclasses. |
|||
|
|||
Parameters |
|||
----------- |
|||
interaction: :class:`.Interaction` |
|||
The interaction that triggered this UI item. |
|||
""" |
|||
pass |
@ -1,357 +0,0 @@ |
|||
""" |
|||
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 List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type, Callable, Union |
|||
import inspect |
|||
import os |
|||
|
|||
from .item import Item, ItemCallbackType |
|||
from ..enums import ComponentType |
|||
from ..partial_emoji import PartialEmoji |
|||
from ..emoji import Emoji |
|||
from ..interactions import Interaction |
|||
from ..utils import MISSING |
|||
from ..components import ( |
|||
SelectOption, |
|||
SelectMenu, |
|||
) |
|||
|
|||
__all__ = ( |
|||
'Select', |
|||
'select', |
|||
) |
|||
|
|||
if TYPE_CHECKING: |
|||
from .view import View |
|||
from ..types.components import SelectMenu as SelectMenuPayload |
|||
from ..types.interactions import ( |
|||
ComponentInteractionData, |
|||
) |
|||
|
|||
S = TypeVar('S', bound='Select') |
|||
V = TypeVar('V', bound='View', covariant=True) |
|||
|
|||
|
|||
class Select(Item[V]): |
|||
"""Represents a UI select menu. |
|||
|
|||
This is usually represented as a drop down menu. |
|||
|
|||
In order to get the selected items that the user has chosen, use :attr:`Select.values`. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Parameters |
|||
------------ |
|||
custom_id: :class:`str` |
|||
The ID of the select menu that gets received during an interaction. |
|||
If not given then one is generated for you. |
|||
placeholder: Optional[:class:`str`] |
|||
The placeholder text that is shown if nothing is selected, if any. |
|||
min_values: :class:`int` |
|||
The minimum number of items that must be chosen for this select menu. |
|||
Defaults to 1 and must be between 1 and 25. |
|||
max_values: :class:`int` |
|||
The maximum number of items that must be chosen for this select menu. |
|||
Defaults to 1 and must be between 1 and 25. |
|||
options: List[:class:`discord.SelectOption`] |
|||
A list of options that can be selected in this menu. |
|||
disabled: :class:`bool` |
|||
Whether the select is disabled or not. |
|||
row: Optional[:class:`int`] |
|||
The relative row this select menu belongs to. A Discord component can only have 5 |
|||
rows. By default, items are arranged automatically into those 5 rows. If you'd |
|||
like to control the relative positioning of the row then passing an index is advised. |
|||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic |
|||
ordering. The row number must be between 0 and 4 (i.e. zero indexed). |
|||
""" |
|||
|
|||
__item_repr_attributes__: Tuple[str, ...] = ( |
|||
'placeholder', |
|||
'min_values', |
|||
'max_values', |
|||
'options', |
|||
'disabled', |
|||
) |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
custom_id: str = MISSING, |
|||
placeholder: Optional[str] = None, |
|||
min_values: int = 1, |
|||
max_values: int = 1, |
|||
options: List[SelectOption] = MISSING, |
|||
disabled: bool = False, |
|||
row: Optional[int] = None, |
|||
) -> None: |
|||
super().__init__() |
|||
self._selected_values: List[str] = [] |
|||
self._provided_custom_id = custom_id is not MISSING |
|||
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id |
|||
options = [] if options is MISSING else options |
|||
self._underlying = SelectMenu._raw_construct( |
|||
custom_id=custom_id, |
|||
type=ComponentType.select, |
|||
placeholder=placeholder, |
|||
min_values=min_values, |
|||
max_values=max_values, |
|||
options=options, |
|||
disabled=disabled, |
|||
) |
|||
self.row = row |
|||
|
|||
@property |
|||
def custom_id(self) -> str: |
|||
""":class:`str`: The ID of the select menu that gets received during an interaction.""" |
|||
return self._underlying.custom_id |
|||
|
|||
@custom_id.setter |
|||
def custom_id(self, value: str): |
|||
if not isinstance(value, str): |
|||
raise TypeError('custom_id must be None or str') |
|||
|
|||
self._underlying.custom_id = value |
|||
|
|||
@property |
|||
def placeholder(self) -> Optional[str]: |
|||
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any.""" |
|||
return self._underlying.placeholder |
|||
|
|||
@placeholder.setter |
|||
def placeholder(self, value: Optional[str]): |
|||
if value is not None and not isinstance(value, str): |
|||
raise TypeError('placeholder must be None or str') |
|||
|
|||
self._underlying.placeholder = value |
|||
|
|||
@property |
|||
def min_values(self) -> int: |
|||
""":class:`int`: The minimum number of items that must be chosen for this select menu.""" |
|||
return self._underlying.min_values |
|||
|
|||
@min_values.setter |
|||
def min_values(self, value: int): |
|||
self._underlying.min_values = int(value) |
|||
|
|||
@property |
|||
def max_values(self) -> int: |
|||
""":class:`int`: The maximum number of items that must be chosen for this select menu.""" |
|||
return self._underlying.max_values |
|||
|
|||
@max_values.setter |
|||
def max_values(self, value: int): |
|||
self._underlying.max_values = int(value) |
|||
|
|||
@property |
|||
def options(self) -> List[SelectOption]: |
|||
"""List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu.""" |
|||
return self._underlying.options |
|||
|
|||
@options.setter |
|||
def options(self, value: List[SelectOption]): |
|||
if not isinstance(value, list): |
|||
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 |
|||
|
|||
def add_option( |
|||
self, |
|||
*, |
|||
label: str, |
|||
value: str = MISSING, |
|||
description: Optional[str] = None, |
|||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, |
|||
default: bool = False, |
|||
): |
|||
"""Adds an option to the select menu. |
|||
|
|||
To append a pre-existing :class:`discord.SelectOption` 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. |
|||
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] |
|||
The emoji of the option, if available. This can either be a string representing |
|||
the custom or unicode emoji or an instance of :class:`.PartialEmoji` or :class:`.Emoji`. |
|||
default: :class:`bool` |
|||
Whether this option is selected by default. |
|||
|
|||
Raises |
|||
------- |
|||
ValueError |
|||
The number of options exceeds 25. |
|||
""" |
|||
|
|||
option = SelectOption( |
|||
label=label, |
|||
value=value, |
|||
description=description, |
|||
emoji=emoji, |
|||
default=default, |
|||
) |
|||
|
|||
|
|||
self.append_option(option) |
|||
|
|||
def append_option(self, option: SelectOption): |
|||
"""Appends an option to the select menu. |
|||
|
|||
Parameters |
|||
----------- |
|||
option: :class:`discord.SelectOption` |
|||
The option to append to the select menu. |
|||
|
|||
Raises |
|||
------- |
|||
ValueError |
|||
The number of options exceeds 25. |
|||
""" |
|||
|
|||
if len(self._underlying.options) > 25: |
|||
raise ValueError('maximum number of options already provided') |
|||
|
|||
self._underlying.options.append(option) |
|||
|
|||
@property |
|||
def disabled(self) -> bool: |
|||
""":class:`bool`: Whether the select is disabled or not.""" |
|||
return self._underlying.disabled |
|||
|
|||
@disabled.setter |
|||
def disabled(self, value: bool): |
|||
self._underlying.disabled = bool(value) |
|||
|
|||
@property |
|||
def values(self) -> List[str]: |
|||
"""List[:class:`str`]: A list of values that have been selected by the user.""" |
|||
return self._selected_values |
|||
|
|||
@property |
|||
def width(self) -> int: |
|||
return 5 |
|||
|
|||
def to_component_dict(self) -> SelectMenuPayload: |
|||
return self._underlying.to_dict() |
|||
|
|||
def refresh_component(self, component: SelectMenu) -> None: |
|||
self._underlying = component |
|||
|
|||
def refresh_state(self, interaction: Interaction) -> None: |
|||
data: ComponentInteractionData = interaction.data # type: ignore |
|||
self._selected_values = data.get('values', []) |
|||
|
|||
@classmethod |
|||
def from_component(cls: Type[S], component: SelectMenu) -> S: |
|||
return cls( |
|||
custom_id=component.custom_id, |
|||
placeholder=component.placeholder, |
|||
min_values=component.min_values, |
|||
max_values=component.max_values, |
|||
options=component.options, |
|||
disabled=component.disabled, |
|||
row=None, |
|||
) |
|||
|
|||
@property |
|||
def type(self) -> ComponentType: |
|||
return self._underlying.type |
|||
|
|||
def is_dispatchable(self) -> bool: |
|||
return True |
|||
|
|||
|
|||
def select( |
|||
*, |
|||
placeholder: Optional[str] = None, |
|||
custom_id: str = MISSING, |
|||
min_values: int = 1, |
|||
max_values: int = 1, |
|||
options: List[SelectOption] = MISSING, |
|||
disabled: bool = False, |
|||
row: Optional[int] = None, |
|||
) -> Callable[[ItemCallbackType], ItemCallbackType]: |
|||
"""A decorator that attaches a select menu to a component. |
|||
|
|||
The function being decorated should have three parameters, ``self`` representing |
|||
the :class:`discord.ui.View`, the :class:`discord.ui.Select` being pressed and |
|||
the :class:`discord.Interaction` you receive. |
|||
|
|||
In order to get the selected items that the user has chosen within the callback |
|||
use :attr:`Select.values`. |
|||
|
|||
Parameters |
|||
------------ |
|||
placeholder: Optional[:class:`str`] |
|||
The placeholder text that is shown if nothing is selected, if any. |
|||
custom_id: :class:`str` |
|||
The ID of the select menu that gets received during an interaction. |
|||
It is recommended not to set this parameter to prevent conflicts. |
|||
row: Optional[:class:`int`] |
|||
The relative row this select menu belongs to. A Discord component can only have 5 |
|||
rows. By default, items are arranged automatically into those 5 rows. If you'd |
|||
like to control the relative positioning of the row then passing an index is advised. |
|||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic |
|||
ordering. The row number must be between 0 and 4 (i.e. zero indexed). |
|||
min_values: :class:`int` |
|||
The minimum number of items that must be chosen for this select menu. |
|||
Defaults to 1 and must be between 1 and 25. |
|||
max_values: :class:`int` |
|||
The maximum number of items that must be chosen for this select menu. |
|||
Defaults to 1 and must be between 1 and 25. |
|||
options: List[:class:`discord.SelectOption`] |
|||
A list of options that can be selected in this menu. |
|||
disabled: :class:`bool` |
|||
Whether the select is disabled or not. Defaults to ``False``. |
|||
""" |
|||
|
|||
def decorator(func: ItemCallbackType) -> ItemCallbackType: |
|||
if not inspect.iscoroutinefunction(func): |
|||
raise TypeError('select function must be a coroutine function') |
|||
|
|||
func.__discord_ui_model_type__ = Select |
|||
func.__discord_ui_model_kwargs__ = { |
|||
'placeholder': placeholder, |
|||
'custom_id': custom_id, |
|||
'row': row, |
|||
'min_values': min_values, |
|||
'max_values': max_values, |
|||
'options': options, |
|||
'disabled': disabled, |
|||
} |
|||
return func |
|||
|
|||
return decorator |
@ -1,529 +0,0 @@ |
|||
""" |
|||
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 Any, Callable, ClassVar, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple |
|||
from functools import partial |
|||
from itertools import groupby |
|||
|
|||
import traceback |
|||
import asyncio |
|||
import sys |
|||
import time |
|||
import os |
|||
from .item import Item, ItemCallbackType |
|||
from ..components import ( |
|||
Component, |
|||
ActionRow as ActionRowComponent, |
|||
_component_factory, |
|||
Button as ButtonComponent, |
|||
SelectMenu as SelectComponent, |
|||
) |
|||
|
|||
__all__ = ( |
|||
'View', |
|||
) |
|||
|
|||
|
|||
if TYPE_CHECKING: |
|||
from ..interactions import Interaction |
|||
from ..message import Message |
|||
from ..types.components import Component as ComponentPayload |
|||
from ..state import ConnectionState |
|||
|
|||
|
|||
def _walk_all_components(components: List[Component]) -> Iterator[Component]: |
|||
for item in components: |
|||
if isinstance(item, ActionRowComponent): |
|||
yield from item.children |
|||
else: |
|||
yield item |
|||
|
|||
|
|||
def _component_to_item(component: Component) -> Item: |
|||
if isinstance(component, ButtonComponent): |
|||
from .button import Button |
|||
|
|||
return Button.from_component(component) |
|||
if isinstance(component, SelectComponent): |
|||
from .select import Select |
|||
|
|||
return Select.from_component(component) |
|||
return Item.from_component(component) |
|||
|
|||
|
|||
class _ViewWeights: |
|||
__slots__ = ( |
|||
'weights', |
|||
) |
|||
|
|||
def __init__(self, children: List[Item]): |
|||
self.weights: List[int] = [0, 0, 0, 0, 0] |
|||
|
|||
key = lambda i: sys.maxsize if i.row is None else i.row |
|||
children = sorted(children, key=key) |
|||
for row, group in groupby(children, key=key): |
|||
for item in group: |
|||
self.add_item(item) |
|||
|
|||
def find_open_space(self, item: Item) -> int: |
|||
for index, weight in enumerate(self.weights): |
|||
if weight + item.width <= 5: |
|||
return index |
|||
|
|||
raise ValueError('could not find open space for item') |
|||
|
|||
def add_item(self, item: Item) -> None: |
|||
if item.row is not None: |
|||
total = self.weights[item.row] + item.width |
|||
if total > 5: |
|||
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') |
|||
self.weights[item.row] = total |
|||
item._rendered_row = item.row |
|||
else: |
|||
index = self.find_open_space(item) |
|||
self.weights[index] += item.width |
|||
item._rendered_row = index |
|||
|
|||
def remove_item(self, item: Item) -> None: |
|||
if item._rendered_row is not None: |
|||
self.weights[item._rendered_row] -= item.width |
|||
item._rendered_row = None |
|||
|
|||
def clear(self) -> None: |
|||
self.weights = [0, 0, 0, 0, 0] |
|||
|
|||
|
|||
class View: |
|||
"""Represents a UI view. |
|||
|
|||
This object must be inherited to create a UI within Discord. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Parameters |
|||
----------- |
|||
timeout: Optional[:class:`float`] |
|||
Timeout in seconds from last interaction with the UI before no longer accepting input. |
|||
If ``None`` then there is no timeout. |
|||
|
|||
Attributes |
|||
------------ |
|||
timeout: Optional[:class:`float`] |
|||
Timeout from last interaction with the UI before no longer accepting input. |
|||
If ``None`` then there is no timeout. |
|||
children: List[:class:`Item`] |
|||
The list of children attached to this view. |
|||
""" |
|||
|
|||
__discord_ui_view__: ClassVar[bool] = True |
|||
__view_children_items__: ClassVar[List[ItemCallbackType]] = [] |
|||
|
|||
def __init_subclass__(cls) -> None: |
|||
children: List[ItemCallbackType] = [] |
|||
for base in reversed(cls.__mro__): |
|||
for member in base.__dict__.values(): |
|||
if hasattr(member, '__discord_ui_model_type__'): |
|||
children.append(member) |
|||
|
|||
if len(children) > 25: |
|||
raise TypeError('View cannot have more than 25 children') |
|||
|
|||
cls.__view_children_items__ = children |
|||
|
|||
def __init__(self, *, timeout: Optional[float] = 180.0): |
|||
self.timeout = timeout |
|||
self.children: List[Item] = [] |
|||
for func in self.__view_children_items__: |
|||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) |
|||
item.callback = partial(func, self, item) |
|||
item._view = self |
|||
setattr(self, func.__name__, item) |
|||
self.children.append(item) |
|||
|
|||
self.__weights = _ViewWeights(self.children) |
|||
loop = asyncio.get_running_loop() |
|||
self.id: str = os.urandom(16).hex() |
|||
self.__cancel_callback: Optional[Callable[[View], None]] = None |
|||
self.__timeout_expiry: Optional[float] = None |
|||
self.__timeout_task: Optional[asyncio.Task[None]] = None |
|||
self.__stopped: asyncio.Future[bool] = loop.create_future() |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>' |
|||
|
|||
async def __timeout_task_impl(self) -> None: |
|||
while True: |
|||
# Guard just in case someone changes the value of the timeout at runtime |
|||
if self.timeout is None: |
|||
return |
|||
|
|||
if self.__timeout_expiry is None: |
|||
return self._dispatch_timeout() |
|||
|
|||
# Check if we've elapsed our currently set timeout |
|||
now = time.monotonic() |
|||
if now >= self.__timeout_expiry: |
|||
return self._dispatch_timeout() |
|||
|
|||
# Wait N seconds to see if timeout data has been refreshed |
|||
await asyncio.sleep(self.__timeout_expiry - now) |
|||
|
|||
def to_components(self) -> List[Dict[str, Any]]: |
|||
def key(item: Item) -> int: |
|||
return item._rendered_row or 0 |
|||
|
|||
children = sorted(self.children, key=key) |
|||
components: List[Dict[str, Any]] = [] |
|||
for _, group in groupby(children, key=key): |
|||
children = [item.to_component_dict() for item in group] |
|||
if not children: |
|||
continue |
|||
|
|||
components.append( |
|||
{ |
|||
'type': 1, |
|||
'components': children, |
|||
} |
|||
) |
|||
|
|||
return components |
|||
|
|||
@classmethod |
|||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: |
|||
"""Converts a message's components into a :class:`View`. |
|||
|
|||
The :attr:`.Message.components` of a message are read-only |
|||
and separate types from those in the ``discord.ui`` namespace. |
|||
In order to modify and edit message components they must be |
|||
converted into a :class:`View` first. |
|||
|
|||
Parameters |
|||
----------- |
|||
message: :class:`discord.Message` |
|||
The message with components to convert into a view. |
|||
timeout: Optional[:class:`float`] |
|||
The timeout of the converted view. |
|||
|
|||
Returns |
|||
-------- |
|||
:class:`View` |
|||
The converted view. This always returns a :class:`View` and not |
|||
one of its subclasses. |
|||
""" |
|||
view = View(timeout=timeout) |
|||
for component in _walk_all_components(message.components): |
|||
view.add_item(_component_to_item(component)) |
|||
return view |
|||
|
|||
@property |
|||
def _expires_at(self) -> Optional[float]: |
|||
if self.timeout: |
|||
return time.monotonic() + self.timeout |
|||
return None |
|||
|
|||
def add_item(self, item: Item) -> None: |
|||
"""Adds an item to the view. |
|||
|
|||
Parameters |
|||
----------- |
|||
item: :class:`Item` |
|||
The item to add to the view. |
|||
|
|||
Raises |
|||
-------- |
|||
TypeError |
|||
An :class:`Item` was not passed. |
|||
ValueError |
|||
Maximum number of children has been exceeded (25) |
|||
or the row the item is trying to be added to is full. |
|||
""" |
|||
|
|||
if len(self.children) > 25: |
|||
raise ValueError('maximum number of children exceeded') |
|||
|
|||
if not isinstance(item, Item): |
|||
raise TypeError(f'expected Item not {item.__class__!r}') |
|||
|
|||
self.__weights.add_item(item) |
|||
|
|||
item._view = self |
|||
self.children.append(item) |
|||
|
|||
def remove_item(self, item: Item) -> None: |
|||
"""Removes an item from the view. |
|||
|
|||
Parameters |
|||
----------- |
|||
item: :class:`Item` |
|||
The item to remove from the view. |
|||
""" |
|||
|
|||
try: |
|||
self.children.remove(item) |
|||
except ValueError: |
|||
pass |
|||
else: |
|||
self.__weights.remove_item(item) |
|||
|
|||
def clear_items(self) -> None: |
|||
"""Removes all items from the view.""" |
|||
self.children.clear() |
|||
self.__weights.clear() |
|||
|
|||
async def interaction_check(self, interaction: Interaction) -> bool: |
|||
"""|coro| |
|||
|
|||
A callback that is called when an interaction happens within the view |
|||
that checks whether the view should process item callbacks for the interaction. |
|||
|
|||
This is useful to override if, for example, you want to ensure that the |
|||
interaction author is a given user. |
|||
|
|||
The default implementation of this returns ``True``. |
|||
|
|||
.. note:: |
|||
|
|||
If an exception occurs within the body then the check |
|||
is considered a failure and :meth:`on_error` is called. |
|||
|
|||
Parameters |
|||
----------- |
|||
interaction: :class:`~discord.Interaction` |
|||
The interaction that occurred. |
|||
|
|||
Returns |
|||
--------- |
|||
:class:`bool` |
|||
Whether the view children's callbacks should be called. |
|||
""" |
|||
return True |
|||
|
|||
async def on_timeout(self) -> None: |
|||
"""|coro| |
|||
|
|||
A callback that is called when a view's timeout elapses without being explicitly stopped. |
|||
""" |
|||
pass |
|||
|
|||
async def on_error(self, error: Exception, item: Item, interaction: Interaction) -> None: |
|||
"""|coro| |
|||
|
|||
A callback that is called when an item's callback or :meth:`interaction_check` |
|||
fails with an error. |
|||
|
|||
The default implementation prints the traceback to stderr. |
|||
|
|||
Parameters |
|||
----------- |
|||
error: :class:`Exception` |
|||
The exception that was raised. |
|||
item: :class:`Item` |
|||
The item that failed the dispatch. |
|||
interaction: :class:`~discord.Interaction` |
|||
The interaction that led to the failure. |
|||
""" |
|||
print(f'Ignoring exception in view {self} for item {item}:', file=sys.stderr) |
|||
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr) |
|||
|
|||
async def _scheduled_task(self, item: Item, interaction: Interaction): |
|||
try: |
|||
if self.timeout: |
|||
self.__timeout_expiry = time.monotonic() + self.timeout |
|||
|
|||
allow = await self.interaction_check(interaction) |
|||
if not allow: |
|||
return |
|||
|
|||
await item.callback(interaction) |
|||
if not interaction.response._responded: |
|||
await interaction.response.defer() |
|||
except Exception as e: |
|||
return await self.on_error(e, item, interaction) |
|||
|
|||
def _start_listening_from_store(self, store: ViewStore) -> None: |
|||
self.__cancel_callback = partial(store.remove_view) |
|||
if self.timeout: |
|||
loop = asyncio.get_running_loop() |
|||
if self.__timeout_task is not None: |
|||
self.__timeout_task.cancel() |
|||
|
|||
self.__timeout_expiry = time.monotonic() + self.timeout |
|||
self.__timeout_task = loop.create_task(self.__timeout_task_impl()) |
|||
|
|||
def _dispatch_timeout(self): |
|||
if self.__stopped.done(): |
|||
return |
|||
|
|||
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): |
|||
if self.__stopped.done(): |
|||
return |
|||
|
|||
asyncio.create_task(self._scheduled_task(item, interaction), name=f'discord-ui-view-dispatch-{self.id}') |
|||
|
|||
def refresh(self, components: List[Component]): |
|||
# This is pretty hacky at the moment |
|||
# fmt: off |
|||
old_state: Dict[Tuple[int, str], Item] = { |
|||
(item.type.value, item.custom_id): item # type: ignore |
|||
for item in self.children |
|||
if item.is_dispatchable() |
|||
} |
|||
# fmt: on |
|||
children: List[Item] = [] |
|||
for component in _walk_all_components(components): |
|||
try: |
|||
older = old_state[(component.type.value, component.custom_id)] # type: ignore |
|||
except (KeyError, AttributeError): |
|||
children.append(_component_to_item(component)) |
|||
else: |
|||
older.refresh_component(component) |
|||
children.append(older) |
|||
|
|||
self.children = children |
|||
|
|||
def stop(self) -> None: |
|||
"""Stops listening to interaction events from this view. |
|||
|
|||
This operation cannot be undone. |
|||
""" |
|||
if not self.__stopped.done(): |
|||
self.__stopped.set_result(False) |
|||
|
|||
self.__timeout_expiry = None |
|||
if self.__timeout_task is not None: |
|||
self.__timeout_task.cancel() |
|||
self.__timeout_task = None |
|||
|
|||
if self.__cancel_callback: |
|||
self.__cancel_callback(self) |
|||
self.__cancel_callback = None |
|||
|
|||
def is_finished(self) -> bool: |
|||
""":class:`bool`: Whether the view has finished interacting.""" |
|||
return self.__stopped.done() |
|||
|
|||
def is_dispatching(self) -> bool: |
|||
""":class:`bool`: Whether the view has been added for dispatching purposes.""" |
|||
return self.__cancel_callback is not None |
|||
|
|||
def is_persistent(self) -> bool: |
|||
""":class:`bool`: Whether the view is set up as persistent. |
|||
|
|||
A persistent view has all their components with a set ``custom_id`` and |
|||
a :attr:`timeout` set to ``None``. |
|||
""" |
|||
return self.timeout is None and all(item.is_persistent() for item in self.children) |
|||
|
|||
async def wait(self) -> bool: |
|||
"""Waits until the view has finished interacting. |
|||
|
|||
A view is considered finished when :meth:`stop` is called |
|||
or it times out. |
|||
|
|||
Returns |
|||
-------- |
|||
:class:`bool` |
|||
If ``True``, then the view timed out. If ``False`` then |
|||
the view finished normally. |
|||
""" |
|||
return await self.__stopped |
|||
|
|||
|
|||
class ViewStore: |
|||
def __init__(self, state: ConnectionState): |
|||
# (component_type, message_id, custom_id): (View, Item) |
|||
self._views: Dict[Tuple[int, Optional[int], str], Tuple[View, Item]] = {} |
|||
# message_id: View |
|||
self._synced_message_views: Dict[int, View] = {} |
|||
self._state: ConnectionState = state |
|||
|
|||
@property |
|||
def persistent_views(self) -> Sequence[View]: |
|||
# fmt: off |
|||
views = { |
|||
view.id: view |
|||
for (_, (view, _)) in self._views.items() |
|||
if view.is_persistent() |
|||
} |
|||
# fmt: on |
|||
return list(views.values()) |
|||
|
|||
def __verify_integrity(self): |
|||
to_remove: List[Tuple[int, Optional[int], str]] = [] |
|||
for (k, (view, _)) in self._views.items(): |
|||
if view.is_finished(): |
|||
to_remove.append(k) |
|||
|
|||
for k in to_remove: |
|||
del self._views[k] |
|||
|
|||
def add_view(self, view: View, message_id: Optional[int] = None): |
|||
self.__verify_integrity() |
|||
|
|||
view._start_listening_from_store(self) |
|||
for item in view.children: |
|||
if item.is_dispatchable(): |
|||
self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore |
|||
|
|||
if message_id is not None: |
|||
self._synced_message_views[message_id] = view |
|||
|
|||
def remove_view(self, view: View): |
|||
for item in view.children: |
|||
if item.is_dispatchable(): |
|||
self._views.pop((item.type.value, item.custom_id), None) # type: ignore |
|||
|
|||
for key, value in self._synced_message_views.items(): |
|||
if value.id == view.id: |
|||
del self._synced_message_views[key] |
|||
break |
|||
|
|||
def dispatch(self, component_type: int, custom_id: str, interaction: Interaction): |
|||
self.__verify_integrity() |
|||
message_id: Optional[int] = interaction.message and interaction.message.id |
|||
key = (component_type, message_id, custom_id) |
|||
# Fallback to None message_id searches in case a persistent view |
|||
# was added without an associated message_id |
|||
value = self._views.get(key) or self._views.get((component_type, None, custom_id)) |
|||
if value is None: |
|||
return |
|||
|
|||
view, item = value |
|||
item.refresh_state(interaction) |
|||
view._dispatch_item(item, interaction) |
|||
|
|||
def is_message_tracked(self, message_id: int): |
|||
return message_id in self._synced_message_views |
|||
|
|||
def remove_message_tracking(self, message_id: int) -> Optional[View]: |
|||
return self._synced_message_views.pop(message_id, None) |
|||
|
|||
def update_from_message(self, message_id: int, components: List[ComponentPayload]): |
|||
# pre-req: is_message_tracked == true |
|||
view = self._synced_message_views[message_id] |
|||
view.refresh([_component_factory(d) for d in components]) |
Loading…
Reference in new issue