diff --git a/discord/client.py b/discord/client.py index 48bae6884..cc793c0b7 100644 --- a/discord/client.py +++ b/discord/client.py @@ -1451,9 +1451,15 @@ class Client: ------- TypeError A view was not passed. + ValueError + The view is not persistent. A persistent view has no timeout + and all their components have an explicitly provided custom_id. """ if not isinstance(view, View): raise TypeError(f'expected an instance of View not {view.__class__!r}') + if not view.is_persistent(): + raise ValueError('View is not persistent. Items need to have a custom_id set and View must have no timeout') + self._connection.store_view(view, message_id) diff --git a/discord/ui/button.py b/discord/ui/button.py index 2e4e54fde..7ac2a8af0 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -98,6 +98,7 @@ class Button(Item[V]): 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() diff --git a/discord/ui/item.py b/discord/ui/item.py index 6744f12d5..3a7c76f36 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -56,6 +56,13 @@ class Item(Generic[V]): 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 @@ -77,6 +84,9 @@ class Item(Generic[V]): 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}>' diff --git a/discord/ui/select.py b/discord/ui/select.py index 22d869fb0..22143aa7e 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -101,6 +101,7 @@ class Select(Item[V]): row: Optional[int] = None, ) -> None: 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( diff --git a/discord/ui/view.py b/discord/ui/view.py index 4df899ed3..b6ddf4e6f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -339,6 +339,14 @@ class View: """:class:`bool`: Whether the view has finished interacting.""" return self._stopped.done() + 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.