mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-07-12 21:05:22 +00:00
Add support for dynamic items that parse custom_id for state
This commit is contained in:
parent
78ce5f2331
commit
a852f90358
@ -72,6 +72,7 @@ from .backoff import ExponentialBackoff
|
|||||||
from .webhook import Webhook
|
from .webhook import Webhook
|
||||||
from .appinfo import AppInfo
|
from .appinfo import AppInfo
|
||||||
from .ui.view import View
|
from .ui.view import View
|
||||||
|
from .ui.dynamic import DynamicItem
|
||||||
from .stage_instance import StageInstance
|
from .stage_instance import StageInstance
|
||||||
from .threads import Thread
|
from .threads import Thread
|
||||||
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
|
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
|
||||||
@ -111,6 +112,7 @@ if TYPE_CHECKING:
|
|||||||
from .scheduled_event import ScheduledEvent
|
from .scheduled_event import ScheduledEvent
|
||||||
from .threads import ThreadMember
|
from .threads import ThreadMember
|
||||||
from .types.guild import Guild as GuildPayload
|
from .types.guild import Guild as GuildPayload
|
||||||
|
from .ui.item import Item
|
||||||
from .voice_client import VoiceProtocol
|
from .voice_client import VoiceProtocol
|
||||||
from .audit_logs import AuditLogEntry
|
from .audit_logs import AuditLogEntry
|
||||||
|
|
||||||
@ -2678,6 +2680,30 @@ class Client:
|
|||||||
data = await state.http.start_private_message(user.id)
|
data = await state.http.start_private_message(user.id)
|
||||||
return state.add_dm_channel(data)
|
return state.add_dm_channel(data)
|
||||||
|
|
||||||
|
def add_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
|
||||||
|
r"""Registers a :class:`~discord.ui.DynamicItem` class for persistent listening.
|
||||||
|
|
||||||
|
This method accepts *class types* rather than instances.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
\*items: Type[:class:`~discord.ui.DynamicItem`]
|
||||||
|
The classes of dynamic items to add.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
TypeError
|
||||||
|
The class is not a subclass of :class:`~discord.ui.DynamicItem`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not issubclass(item, DynamicItem):
|
||||||
|
raise TypeError(f'expected subclass of DynamicItem not {item.__name__}')
|
||||||
|
|
||||||
|
self._connection.store_dynamic_items(*items)
|
||||||
|
|
||||||
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
|
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
|
||||||
"""Registers a :class:`~discord.ui.View` for persistent listening.
|
"""Registers a :class:`~discord.ui.View` for persistent listening.
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ from typing import (
|
|||||||
Dict,
|
Dict,
|
||||||
Optional,
|
Optional,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Type,
|
||||||
Union,
|
Union,
|
||||||
Callable,
|
Callable,
|
||||||
Any,
|
Any,
|
||||||
@ -84,6 +85,8 @@ if TYPE_CHECKING:
|
|||||||
from .http import HTTPClient
|
from .http import HTTPClient
|
||||||
from .voice_client import VoiceProtocol
|
from .voice_client import VoiceProtocol
|
||||||
from .gateway import DiscordWebSocket
|
from .gateway import DiscordWebSocket
|
||||||
|
from .ui.item import Item
|
||||||
|
from .ui.dynamic import DynamicItem
|
||||||
from .app_commands import CommandTree, Translator
|
from .app_commands import CommandTree, Translator
|
||||||
|
|
||||||
from .types.automod import AutoModerationRule, AutoModerationActionExecution
|
from .types.automod import AutoModerationRule, AutoModerationActionExecution
|
||||||
@ -395,6 +398,9 @@ class ConnectionState(Generic[ClientT]):
|
|||||||
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
|
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
|
||||||
return self._view_store.remove_message_tracking(message_id)
|
return self._view_store.remove_message_tracking(message_id)
|
||||||
|
|
||||||
|
def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
|
||||||
|
self._view_store.add_dynamic_items(*items)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def persistent_views(self) -> Sequence[View]:
|
def persistent_views(self) -> Sequence[View]:
|
||||||
return self._view_store.persistent_views
|
return self._view_store.persistent_views
|
||||||
|
@ -15,3 +15,4 @@ from .item import *
|
|||||||
from .button import *
|
from .button import *
|
||||||
from .select import *
|
from .select import *
|
||||||
from .text_input import *
|
from .text_input import *
|
||||||
|
from .dynamic import *
|
||||||
|
205
discord/ui/dynamic.py
Normal file
205
discord/ui/dynamic.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
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 ClassVar, Dict, Generic, Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Any, Union
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .item import Item
|
||||||
|
from .._types import ClientT
|
||||||
|
|
||||||
|
__all__ = ('DynamicItem',)
|
||||||
|
|
||||||
|
BaseT = TypeVar('BaseT', bound='Item[Any]', covariant=True)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import TypeVar, Self
|
||||||
|
from ..interactions import Interaction
|
||||||
|
from ..components import Component
|
||||||
|
from ..enums import ComponentType
|
||||||
|
from .view import View
|
||||||
|
|
||||||
|
V = TypeVar('V', bound='View', covariant=True, default=View)
|
||||||
|
else:
|
||||||
|
V = TypeVar('V', bound='View', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicItem(Generic[BaseT], Item['View']):
|
||||||
|
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
|
||||||
|
that ``custom_id``.
|
||||||
|
|
||||||
|
The ``custom_id`` parsing is done using the ``re`` module by passing a ``template``
|
||||||
|
parameter to the class parameter list.
|
||||||
|
|
||||||
|
This item is generated every time the component is dispatched. This means that
|
||||||
|
any variable that holds an instance of this class will eventually be out of date
|
||||||
|
and should not be used long term. Their only purpose is to act as a "template"
|
||||||
|
for the actual dispatched item.
|
||||||
|
|
||||||
|
When this item is generated, :attr:`view` is set to a regular :class:`View` instance
|
||||||
|
from the original message given from the interaction. This means that custom view
|
||||||
|
subclasses cannot be accessed from this item.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
item: :class:`Item`
|
||||||
|
The item to wrap with dynamic custom ID parsing.
|
||||||
|
template: Union[:class:`str`, ``re.Pattern``]
|
||||||
|
The template to use for parsing the ``custom_id``. This can be a string or a compiled
|
||||||
|
regular expression. This must be passed as a keyword argument to the class creation.
|
||||||
|
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).
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
item: :class:`Item`
|
||||||
|
The item that is wrapped with dynamic custom ID parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
|
'item',
|
||||||
|
'template',
|
||||||
|
)
|
||||||
|
|
||||||
|
__discord_ui_compiled_template__: ClassVar[re.Pattern[str]]
|
||||||
|
|
||||||
|
def __init_subclass__(cls, *, template: Union[str, re.Pattern[str]]) -> None:
|
||||||
|
super().__init_subclass__()
|
||||||
|
cls.__discord_ui_compiled_template__ = re.compile(template) if isinstance(template, str) else template
|
||||||
|
if not isinstance(cls.__discord_ui_compiled_template__, re.Pattern):
|
||||||
|
raise TypeError('template must be a str or a re.Pattern')
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
item: BaseT,
|
||||||
|
*,
|
||||||
|
row: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.item: BaseT = item
|
||||||
|
self.row = row
|
||||||
|
|
||||||
|
if not self.item.is_dispatchable():
|
||||||
|
raise TypeError('item must be dispatchable, e.g. not a URL button')
|
||||||
|
|
||||||
|
if not self.template.match(self.custom_id):
|
||||||
|
raise ValueError(f'item custom_id must match the template {self.template.pattern!r}')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def template(self) -> re.Pattern[str]:
|
||||||
|
"""``re.Pattern``: The compiled regular expression that is used to parse the ``custom_id``."""
|
||||||
|
return self.__class__.__discord_ui_compiled_template__
|
||||||
|
|
||||||
|
def to_component_dict(self) -> Dict[str, Any]:
|
||||||
|
return self.item.to_component_dict()
|
||||||
|
|
||||||
|
def _refresh_component(self, component: Component) -> None:
|
||||||
|
self.item._refresh_component(component)
|
||||||
|
|
||||||
|
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
|
||||||
|
self.item._refresh_state(interaction, data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_component(cls: Type[Self], component: Component) -> Self:
|
||||||
|
raise TypeError('Dynamic items cannot be created from components')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> ComponentType:
|
||||||
|
return self.item.type
|
||||||
|
|
||||||
|
def is_dispatchable(self) -> bool:
|
||||||
|
return self.item.is_dispatchable()
|
||||||
|
|
||||||
|
def is_persistent(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_id(self) -> str:
|
||||||
|
""":class:`str`: The ID of the dynamic item that gets received during an interaction."""
|
||||||
|
return self.item.custom_id # type: ignore # This attribute exists for dispatchable items
|
||||||
|
|
||||||
|
@custom_id.setter
|
||||||
|
def custom_id(self, value: str) -> None:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError('custom_id must be a str')
|
||||||
|
|
||||||
|
if not self.template.match(value):
|
||||||
|
raise ValueError(f'custom_id must match the template {self.template.pattern!r}')
|
||||||
|
|
||||||
|
self.item.custom_id = value # type: ignore # This attribute exists for dispatchable items
|
||||||
|
self._provided_custom_id = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row(self) -> Optional[int]:
|
||||||
|
return self.item._row
|
||||||
|
|
||||||
|
@row.setter
|
||||||
|
def row(self, value: Optional[int]) -> None:
|
||||||
|
self.item.row = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
return self.item.width
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_custom_id(cls: Type[Self], interaction: Interaction[ClientT], match: re.Match[str], /) -> Self:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A classmethod that is called when the ``custom_id`` of a component matches the
|
||||||
|
``template`` of the class. This is called when the component is dispatched.
|
||||||
|
|
||||||
|
It must return a new instance of the :class:`DynamicItem`.
|
||||||
|
|
||||||
|
Subclasses *must* implement this method.
|
||||||
|
|
||||||
|
Exceptions raised in this method are logged and ignored.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This method is called before the callback is dispatched, therefore
|
||||||
|
it means that it is subject to the same timing restrictions as the callback.
|
||||||
|
Ergo, you must reply to an interaction within 3 seconds of it being
|
||||||
|
dispatched.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
interaction: :class:`~discord.Interaction`
|
||||||
|
The interaction that the component belongs to.
|
||||||
|
match: ``re.Match``
|
||||||
|
The match object that was created from the ``template``
|
||||||
|
matching the ``custom_id``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
:class:`DynamicItem`
|
||||||
|
The new instance of the :class:`DynamicItem` with information
|
||||||
|
from the ``match`` object.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
@ -133,3 +133,36 @@ class Item(Generic[V]):
|
|||||||
The interaction that triggered this UI item.
|
The interaction that triggered this UI item.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A callback that is called when an interaction happens within this item
|
||||||
|
that checks whether the callback should be processed.
|
||||||
|
|
||||||
|
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:`discord.ui.View.on_error` is called.
|
||||||
|
|
||||||
|
For :class:`~discord.ui.DynamicItem` this does not call the ``on_error``
|
||||||
|
handler.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
interaction: :class:`~discord.Interaction`
|
||||||
|
The interaction that occurred.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
---------
|
||||||
|
:class:`bool`
|
||||||
|
Whether the callback should be called.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple
|
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
from .item import Item, ItemCallbackType
|
from .item import Item, ItemCallbackType
|
||||||
|
from .dynamic import DynamicItem
|
||||||
from ..components import (
|
from ..components import (
|
||||||
Component,
|
Component,
|
||||||
ActionRow as ActionRowComponent,
|
ActionRow as ActionRowComponent,
|
||||||
@ -50,6 +51,7 @@ __all__ = (
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
import re
|
||||||
|
|
||||||
from ..interactions import Interaction
|
from ..interactions import Interaction
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
@ -417,7 +419,7 @@ class View:
|
|||||||
try:
|
try:
|
||||||
item._refresh_state(interaction, interaction.data) # type: ignore
|
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||||
|
|
||||||
allow = await self.interaction_check(interaction)
|
allow = await item.interaction_check(interaction) and await self.interaction_check(interaction)
|
||||||
if not allow:
|
if not allow:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -534,6 +536,8 @@ class ViewStore:
|
|||||||
self._synced_message_views: Dict[int, View] = {}
|
self._synced_message_views: Dict[int, View] = {}
|
||||||
# custom_id: Modal
|
# custom_id: Modal
|
||||||
self._modals: Dict[str, Modal] = {}
|
self._modals: Dict[str, Modal] = {}
|
||||||
|
# component_type is the key
|
||||||
|
self._dynamic_items: Dict[re.Pattern[str], Type[DynamicItem[Item[Any]]]] = {}
|
||||||
self._state: ConnectionState = state
|
self._state: ConnectionState = state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -548,6 +552,11 @@ class ViewStore:
|
|||||||
# fmt: on
|
# fmt: on
|
||||||
return list(views.values())
|
return list(views.values())
|
||||||
|
|
||||||
|
def add_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
|
||||||
|
for item in items:
|
||||||
|
pattern = item.__discord_ui_compiled_template__
|
||||||
|
self._dynamic_items[pattern] = item
|
||||||
|
|
||||||
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
|
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
|
||||||
view._start_listening_from_store(self)
|
view._start_listening_from_store(self)
|
||||||
if view.__discord_ui_modal__:
|
if view.__discord_ui_modal__:
|
||||||
@ -556,7 +565,10 @@ class ViewStore:
|
|||||||
|
|
||||||
dispatch_info = self._views.setdefault(message_id, {})
|
dispatch_info = self._views.setdefault(message_id, {})
|
||||||
for item in view._children:
|
for item in view._children:
|
||||||
if item.is_dispatchable():
|
if isinstance(item, DynamicItem):
|
||||||
|
pattern = item.__discord_ui_compiled_template__
|
||||||
|
self._dynamic_items[pattern] = item.__class__
|
||||||
|
elif item.is_dispatchable():
|
||||||
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
|
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
|
||||||
|
|
||||||
view._cache_key = message_id
|
view._cache_key = message_id
|
||||||
@ -571,7 +583,10 @@ class ViewStore:
|
|||||||
dispatch_info = self._views.get(view._cache_key)
|
dispatch_info = self._views.get(view._cache_key)
|
||||||
if dispatch_info:
|
if dispatch_info:
|
||||||
for item in view._children:
|
for item in view._children:
|
||||||
if item.is_dispatchable():
|
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
|
dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
|
||||||
|
|
||||||
if len(dispatch_info) == 0:
|
if len(dispatch_info) == 0:
|
||||||
@ -579,7 +594,57 @@ class ViewStore:
|
|||||||
|
|
||||||
self._synced_message_views.pop(view._cache_key, None) # type: ignore
|
self._synced_message_views.pop(view._cache_key, None) # type: ignore
|
||||||
|
|
||||||
|
async def schedule_dynamic_item_call(
|
||||||
|
self,
|
||||||
|
component_type: int,
|
||||||
|
factory: Type[DynamicItem[Item[Any]]],
|
||||||
|
interaction: Interaction,
|
||||||
|
match: re.Match[str],
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
item = await factory.from_custom_id(interaction, match)
|
||||||
|
except Exception:
|
||||||
|
_log.exception('Ignoring exception in dynamic item creation for %r', factory)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Unfortunately cannot set Item.view here...
|
||||||
|
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
allow = await item.interaction_check(interaction)
|
||||||
|
except Exception:
|
||||||
|
allow = False
|
||||||
|
|
||||||
|
if not allow:
|
||||||
|
return
|
||||||
|
|
||||||
|
if interaction.message is None:
|
||||||
|
item._view = None
|
||||||
|
else:
|
||||||
|
item._view = view = View.from_message(interaction.message)
|
||||||
|
|
||||||
|
# Find the original item and replace it with the dynamic item
|
||||||
|
for index, child in enumerate(view._children):
|
||||||
|
if child.type.value == component_type and getattr(child, 'custom_id', None) == item.custom_id:
|
||||||
|
view._children[index] = item
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
await item.callback(interaction)
|
||||||
|
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:
|
||||||
|
for pattern, item in self._dynamic_items.items():
|
||||||
|
match = pattern.fullmatch(custom_id)
|
||||||
|
if match is not None:
|
||||||
|
asyncio.create_task(
|
||||||
|
self.schedule_dynamic_item_call(component_type, item, interaction, match),
|
||||||
|
name=f'discord-ui-dynamic-item-{item.__name__}-{custom_id}',
|
||||||
|
)
|
||||||
|
|
||||||
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) -> None:
|
||||||
|
self.dispatch_dynamic_items(component_type, custom_id, interaction)
|
||||||
interaction_id: Optional[int] = None
|
interaction_id: Optional[int] = None
|
||||||
message_id: Optional[int] = None
|
message_id: Optional[int] = None
|
||||||
# Realistically, in a component based interaction the Interaction.message will never be None
|
# Realistically, in a component based interaction the Interaction.message will never be None
|
||||||
|
@ -443,6 +443,15 @@ Item
|
|||||||
.. autoclass:: discord.ui.Item
|
.. autoclass:: discord.ui.Item
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
DynamicItem
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. attributetable:: discord.ui.DynamicItem
|
||||||
|
|
||||||
|
.. autoclass:: discord.ui.DynamicItem
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
Button
|
Button
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
import discord
|
import discord
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
# Define a simple View that persists between bot restarts
|
# Define a simple View that persists between bot restarts
|
||||||
@ -29,6 +30,38 @@ class PersistentView(discord.ui.View):
|
|||||||
await interaction.response.send_message('This is grey.', ephemeral=True)
|
await interaction.response.send_message('This is grey.', ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# More complicated cases might require parsing state out from the custom_id instead.
|
||||||
|
# For this use case, the library provides a `DynamicItem` to make this easier.
|
||||||
|
# The same constraints as above apply to this too.
|
||||||
|
# For this example, the `template` class parameter is used to give the library a regular
|
||||||
|
# expression to parse the custom_id with.
|
||||||
|
# These custom IDs will be in the form of e.g. `button:user:80088516616269824`.
|
||||||
|
class DynamicButton(discord.ui.DynamicItem[discord.ui.Button], template=r'button:user:(?P<id>[0-9]+)'):
|
||||||
|
def __init__(self, user_id: int) -> None:
|
||||||
|
super().__init__(
|
||||||
|
discord.ui.Button(
|
||||||
|
label='Do Thing',
|
||||||
|
style=discord.ButtonStyle.blurple,
|
||||||
|
custom_id=f'button:user:{user_id}',
|
||||||
|
emoji='\N{THUMBS UP SIGN}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.user_id: int = user_id
|
||||||
|
|
||||||
|
# This is called when the button is clicked and the custom_id matches the template.
|
||||||
|
@classmethod
|
||||||
|
async def from_custom_id(cls, interaction: discord.Interaction, match: re.Match[str], /):
|
||||||
|
user_id = int(match['id'])
|
||||||
|
return cls(user_id)
|
||||||
|
|
||||||
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||||
|
# Only allow the user who created the button to interact with it.
|
||||||
|
return interaction.user.id == self.user_id
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction) -> None:
|
||||||
|
await interaction.response.send_message('This is your very own button!', ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
class PersistentViewBot(commands.Bot):
|
class PersistentViewBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
@ -43,6 +76,8 @@ class PersistentViewBot(commands.Bot):
|
|||||||
# If you have the message_id you can also pass it as a keyword argument, but for this example
|
# If you have the message_id you can also pass it as a keyword argument, but for this example
|
||||||
# we don't have one.
|
# we don't have one.
|
||||||
self.add_view(PersistentView())
|
self.add_view(PersistentView())
|
||||||
|
# For dynamic items, we must register the classes instead of the views.
|
||||||
|
self.add_dynamic_items(DynamicButton)
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
@ -63,4 +98,13 @@ async def prepare(ctx: commands.Context):
|
|||||||
await ctx.send("What's your favourite colour?", view=PersistentView())
|
await ctx.send("What's your favourite colour?", view=PersistentView())
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def dynamic_button(ctx: commands.Context):
|
||||||
|
"""Starts a dynamic button."""
|
||||||
|
|
||||||
|
view = discord.ui.View(timeout=None)
|
||||||
|
view.add_item(DynamicButton(ctx.author.id))
|
||||||
|
await ctx.send('Here is your very own button!', view=view)
|
||||||
|
|
||||||
|
|
||||||
bot.run('token')
|
bot.run('token')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user