From 6ab747f9e5a67ecc28aaaac519a3de8cb2b967d8 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:37:20 +0100 Subject: [PATCH] Add support for sending views in stateless webhooks --- discord/ui/view.py | 5 +++++ discord/webhook/async_.py | 24 ++++++++++++++---------- discord/webhook/sync.py | 26 +++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 2341a720fef6..ad5ea058536f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -214,6 +214,11 @@ async def __timeout_task_impl(self) -> None: # Wait N seconds to see if timeout data has been refreshed await asyncio.sleep(self.__timeout_expiry - now) + def is_dispatchable(self) -> bool: + # this is used by webhooks to check whether a view requires a state attached + # or not, this simply is, whether a view has a component other than a url button + return any(item.is_dispatchable() for item in self.children) + def to_components(self) -> List[Dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2faa9f0e0d6d..1966832e0f0a 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -310,8 +310,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> Response[Optional[MessagePayload]]: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -1715,10 +1716,9 @@ async def send( .. versionadded:: 1.4 view: :class:`discord.ui.View` - The view to send with the message. You can only send a view - if this webhook is not partial and has state attached. A - webhook has state attached if the webhook is managed by the - library. + The view to send with the message. If the webhook is partial or + is not managed by the library, then you can only send URL buttons. + Otherwise, you can send views with any type of components. .. versionadded:: 2.0 thread: :class:`~discord.abc.Snowflake` @@ -1770,7 +1770,8 @@ async def send( The length of ``embeds`` was invalid, there was no token associated with this webhook or ``ephemeral`` was passed with the improper webhook type or there was no state - attached with this webhook when giving it a view. + attached with this webhook when giving it a view that had + components other than URL buttons. Returns --------- @@ -1800,13 +1801,15 @@ async def send( wait = True if view is not MISSING: - if isinstance(self._state, _WebhookState): - raise ValueError('Webhook views require an associated state with the webhook') - if not hasattr(view, '__discord_ui_view__'): raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') - if ephemeral is True and view.timeout is None: + if isinstance(self._state, _WebhookState) and view.is_dispatchable(): + raise ValueError( + 'Webhook views with any component other than URL buttons require an associated state with the webhook' + ) + + if ephemeral is True and view.timeout is None and view.is_dispatchable(): view.timeout = 15 * 60.0 if thread_name is not MISSING and thread is not MISSING: @@ -1850,6 +1853,7 @@ async def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) msg = None diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index cf23e977b33a..171931b12ea2 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -66,6 +66,7 @@ from ..message import Attachment from ..abc import Snowflake from ..state import ConnectionState + from ..ui import View from ..types.webhook import ( Webhook as WebhookPayload, ) @@ -290,8 +291,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> MessagePayload: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -919,6 +921,7 @@ def send( silent: bool = False, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, + view: View = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. @@ -991,6 +994,13 @@ def send( When sending a Poll via webhook, you cannot manually end it. .. versionadded:: 2.4 + view: :class:`~discord.ui.View` + The view to send with the message. This can only have URL buttons, which donnot + require a state to be attached to it. + + If you want to send a view with any component attached to it, check :meth:`Webhook.send`. + + .. versionadded:: 2.5 Raises -------- @@ -1004,8 +1014,9 @@ def send( You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` or ``thread`` and ``thread_name``. ValueError - The length of ``embeds`` was invalid or - there was no token associated with this webhook. + The length of ``embeds`` was invalid, there was no token + associated with this webhook or you tried to send a view + with components other than URL buttons. Returns --------- @@ -1027,6 +1038,13 @@ def send( else: flags = MISSING + if view is not MISSING: + if not hasattr(view, '__discord_ui_view__'): + raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') + + if view.is_dispatchable(): + raise ValueError('SyncWebhook views can only contain URL buttons') + if thread_name is not MISSING and thread is not MISSING: raise TypeError('Cannot mix thread_name and thread keyword arguments.') @@ -1050,6 +1068,7 @@ def send( flags=flags, applied_tags=applied_tag_ids, poll=poll, + view=view, ) as params: adapter: WebhookAdapter = _get_webhook_adapter() thread_id: Optional[int] = None @@ -1065,6 +1084,7 @@ def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) msg = None