From 0b577fa2098c0ce4a18ea1aca498935773cfeac4 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 3 Jul 2021 11:00:48 -0400 Subject: [PATCH] Add support for fetching the original interaction response message --- discord/interactions.py | 286 +++++++++++++++++++++++++++++++++++++++- docs/api.rst | 8 ++ 2 files changed, 288 insertions(+), 6 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 3e9bbce68..b81dfa08e 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -25,19 +25,19 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from discord.types.interactions import InteractionResponse from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union +import asyncio from . import utils from .enums import try_enum, InteractionType, InteractionResponseType -from .errors import InteractionResponded +from .errors import InteractionResponded, HTTPException, ClientException from .user import User from .member import Member from .message import Message, Attachment from .object import Object from .permissions import Permissions -from .webhook.async_ import async_context, Webhook +from .webhook.async_ import async_context, Webhook, handle_message_parameters __all__ = ( 'Interaction', @@ -51,6 +51,8 @@ if TYPE_CHECKING: ) from .guild import Guild from .state import ConnectionState + from .file import File + from .mentions import AllowedMentions from aiohttp import ClientSession from .embeds import Embed from .ui.view import View @@ -105,6 +107,7 @@ class Interaction: '_permissions', '_state', '_session', + '_original_message', '_cs_response', '_cs_followup', ) @@ -112,6 +115,7 @@ class Interaction: def __init__(self, *, data: InteractionPayload, state: ConnectionState): self._state: ConnectionState = state self._session: ClientSession = state.http._HTTPClient__session + self._original_message: Optional[InteractionMessage] = None self._from_data(data) def _from_data(self, data: InteractionPayload): @@ -122,7 +126,7 @@ class Interaction: self.version: int = data['version'] self.channel_id: Optional[int] = utils._get_as_snowflake(data, 'channel_id') self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') - self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') + self.application_id: int = int(data['application_id']) channel = self.channel or Object(id=self.channel_id) # type: ignore self.message: Optional[Message] @@ -175,7 +179,11 @@ class Interaction: @utils.cached_slot_property('_cs_response') def response(self) -> InteractionResponse: - """:class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction.""" + """:class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction. + + A response can only be done once. If secondary messages need to be sent, consider using :attr:`followup` + instead. + """ return InteractionResponse(self) @utils.cached_slot_property('_cs_followup') @@ -188,6 +196,146 @@ class Interaction: } return Webhook.from_state(data=payload, state=self._state) + async def original_message(self) -> InteractionMessage: + """|coro| + + Fetches the original interaction response message associated with the interaction. + + If the interaction response was :meth:`InteractionResponse.send_message` then this would + return the message that was sent using that response. Otherwise, this would return + the message that triggered the interaction. + + Repeated calls to this will return a cached value. + + Raises + ------- + HTTPException + Fetching the original response message failed. + ClientException + The channel for the message could not be resolved. + + Returns + -------- + InteractionMessage + The original interaction response message. + """ + + if self._original_message is not None: + return self._original_message + + # TODO: fix later to not raise? + channel = self.channel + if channel is None: + raise ClientException('Channel for message could not be resolved') + + adapter = async_context.get() + data = await adapter.get_original_interaction_response( + application_id=self.application_id, + token=self.token, + session=self._session, + ) + state = _InteractionMessageState(self, self._state) + message = InteractionMessage(state=state, channel=channel, data=data) # type: ignore + self._original_message = message + return message + + async def edit_original_message( + self, + *, + content: Optional[str] = MISSING, + embeds: List[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + file: File = MISSING, + files: List[File] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + ): + """|coro| + + Edits the original interaction response message. + + This is a lower level interface to :meth:`InteractionMessage.edit` in case + you do not want to fetch the message and save an HTTP request. + + Parameters + ------------ + content: Optional[:class:`str`] + The content to edit the message with or ``None`` to clear it. + embeds: List[:class:`Embed`] + A list of embeds to edit the message with. + embed: Optional[:class:`Embed`] + The embed to edit the message with. ``None`` suppresses the embeds. + This should not be mixed with the ``embeds`` parameter. + file: :class:`File` + The file to upload. This cannot be mixed with ``files`` parameter. + files: List[:class:`File`] + A list of files to send with the content. This cannot be mixed with the + ``file`` parameter. + allowed_mentions: :class:`AllowedMentions` + Controls the mentions being processed in this message. + See :meth:`.abc.Messageable.send` for more information. + view: Optional[:class:`~discord.ui.View`] + The updated view to update this message with. If ``None`` is passed then + the view is removed. + + Raises + ------- + HTTPException + Editing the message failed. + Forbidden + Edited a message that is not yours. + TypeError + You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` + ValueError + The length of ``embeds`` was invalid + """ + + previous_mentions: Optional[AllowedMentions] = self._state.allowed_mentions + params = handle_message_parameters( + content=content, + file=file, + files=files, + embed=embed, + embeds=embeds, + view=view, + allowed_mentions=allowed_mentions, + previous_allowed_mentions=previous_mentions, + ) + adapter = async_context.get() + data = await adapter.edit_original_interaction_response( + self.application_id, + self.token, + session=self._session, + payload=params.payload, + multipart=params.multipart, + files=params.files, + ) + + if view and not view.is_finished(): + self._state.store_view(view, int(data['id'])) + + async def delete_original_message(self) -> None: + """|coro| + + Deletes the original interaction response message. + + This is a lower level interface to :meth:`InteractionMessage.delete` in case + you do not want to fetch the message and save an HTTP request. + + Raises + ------- + HTTPException + Deleting the message failed. + Forbidden + Deleted a message that is not yours. + """ + adapter = async_context.get() + await adapter.delete_original_interaction_response( + self.application_id, + self.token, + session=self._session, + ) + class InteractionResponse: """Represents a Discord interaction response. @@ -417,7 +565,6 @@ class InteractionResponse: if parent.type is not InteractionType.component: return - # TODO: embeds: List[Embed]? payload = {} if content is not MISSING: if content is None: @@ -460,3 +607,130 @@ class InteractionResponse: state.store_view(view, message_id) self._responded = True + + +class _InteractionMessageState: + __slots__ = ('_parent', '_interaction') + + def __init__(self, interaction: Interaction, parent: ConnectionState): + self._interaction: Interaction = interaction + self._parent: ConnectionState = parent + + def _get_guild(self, guild_id): + return self._parent._get_guild(guild_id) + + def store_user(self, data): + return self._parent.store_user(data) + + @property + def http(self): + return self._parent.http + + def __getattr__(self, attr): + return getattr(self._parent, attr) + + +class InteractionMessage(Message): + """Represents the original interaction response message. + + This allows you to edit or delete the message associated with + the interaction response. To retrieve this object see :meth:`Interaction.original_message`. + + This inherits from :class:`discord.Message` with changes to + :meth:`edit` and :meth:`delete` to work. + + .. versionadded:: 2.0 + """ + + __slots__ = () + _state: _InteractionMessageState + + async def edit( + self, + content: Optional[str] = MISSING, + embeds: List[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + file: File = MISSING, + files: List[File] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + ): + """|coro| + + Edits the message. + + Parameters + ------------ + content: Optional[:class:`str`] + The content to edit the message with or ``None`` to clear it. + embeds: List[:class:`Embed`] + A list of embeds to edit the message with. + embed: Optional[:class:`Embed`] + The embed to edit the message with. ``None`` suppresses the embeds. + This should not be mixed with the ``embeds`` parameter. + file: :class:`File` + The file to upload. This cannot be mixed with ``files`` parameter. + files: List[:class:`File`] + A list of files to send with the content. This cannot be mixed with the + ``file`` parameter. + allowed_mentions: :class:`AllowedMentions` + Controls the mentions being processed in this message. + See :meth:`.abc.Messageable.send` for more information. + view: Optional[:class:`~discord.ui.View`] + The updated view to update this message with. If ``None`` is passed then + the view is removed. + + Raises + ------- + HTTPException + Editing the message failed. + Forbidden + Edited a message that is not yours. + TypeError + You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` + ValueError + The length of ``embeds`` was invalid + """ + await self._state._interaction.edit_original_message( + content=content, + embeds=embeds, + embed=embed, + file=file, + files=files, + view=view, + allowed_mentions=allowed_mentions, + ) + + async def delete(self, *, delay: Optional[float] = None) -> None: + """|coro| + + Deletes the message. + + Parameters + ----------- + delay: Optional[:class:`float`] + If provided, the number of seconds to wait before deleting the message. + The waiting is done in the background and deletion failures are ignored. + + Raises + ------ + Forbidden + You do not have proper permissions to delete the message. + NotFound + The message was deleted already. + HTTPException + Deleting the message failed. + """ + + if delay is not None: + + async def inner_call(delay: float = delay): + await asyncio.sleep(delay) + try: + await self._state._interaction.delete_original_message() + except HTTPException: + pass + + asyncio.create_task(inner_call()) + else: + await self._state._interaction.delete_original_message() diff --git a/docs/api.rst b/docs/api.rst index 83dbca4ea..ee40fa1bc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3271,6 +3271,14 @@ InteractionResponse .. autoclass:: InteractionResponse() :members: +InteractionMessage +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionMessage + +.. autoclass:: InteractionMessage() + :members: + Member ~~~~~~