Browse Source

Refactor interaction response handling to support files

This adds support for file sending and allowed_mentions
pull/7494/head
Rapptz 3 years ago
parent
commit
92d1b4cd2b
  1. 112
      discord/interactions.py
  2. 130
      discord/webhook/async_.py

112
discord/interactions.py

@ -31,6 +31,7 @@ import asyncio
from . import utils from . import utils
from .enums import try_enum, InteractionType, InteractionResponseType from .enums import try_enum, InteractionType, InteractionResponseType
from .errors import InteractionResponded, HTTPException, ClientException from .errors import InteractionResponded, HTTPException, ClientException
from .flags import MessageFlags
from .channel import PartialMessageable, ChannelType from .channel import PartialMessageable, ChannelType
from .user import User from .user import User
@ -39,7 +40,7 @@ from .message import Message, Attachment
from .object import Object from .object import Object
from .permissions import Permissions from .permissions import Permissions
from .http import handle_message_parameters from .http import handle_message_parameters
from .webhook.async_ import async_context, Webhook from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params
__all__ = ( __all__ = (
'Interaction', 'Interaction',
@ -421,9 +422,8 @@ class InteractionResponse:
if defer_type: if defer_type:
adapter = async_context.get() adapter = async_context.get()
await adapter.create_interaction_response( params = interaction_response_params(type=defer_type, data=data)
parent.id, parent.token, session=parent._session, type=defer_type, data=data await adapter.create_interaction_response(parent.id, parent.token, session=parent._session, params=params)
)
self._responded = True self._responded = True
async def pong(self) -> None: async def pong(self) -> None:
@ -446,9 +446,8 @@ class InteractionResponse:
parent = self._parent parent = self._parent
if parent.type is InteractionType.ping: if parent.type is InteractionType.ping:
adapter = async_context.get() adapter = async_context.get()
await adapter.create_interaction_response( params = interaction_response_params(InteractionResponseType.pong.value)
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value await adapter.create_interaction_response(parent.id, parent.token, session=parent._session, params=params)
)
self._responded = True self._responded = True
async def send_message( async def send_message(
@ -457,9 +456,12 @@ class InteractionResponse:
*, *,
embed: Embed = MISSING, embed: Embed = MISSING,
embeds: List[Embed] = MISSING, embeds: List[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
view: View = MISSING, view: View = MISSING,
tts: bool = False, tts: bool = False,
ephemeral: bool = False, ephemeral: bool = False,
allowed_mentions: AllowedMentions = MISSING,
) -> None: ) -> None:
"""|coro| """|coro|
@ -475,6 +477,10 @@ class InteractionResponse:
embed: :class:`Embed` embed: :class:`Embed`
The rich embed for the content to send. This cannot be mixed with The rich embed for the content to send. This cannot be mixed with
``embeds`` parameter. ``embeds`` parameter.
file: :class:`~discord.File`
The file to upload.
files: List[:class:`~discord.File`]
A list of files to upload. Must be a maximum of 10.
tts: :class:`bool` tts: :class:`bool`
Indicates if the message should be sent using text-to-speech. Indicates if the message should be sent using text-to-speech.
view: :class:`discord.ui.View` view: :class:`discord.ui.View`
@ -483,13 +489,16 @@ class InteractionResponse:
Indicates if the message should only be visible to the user who started the interaction. Indicates if the message should only be visible to the user who started the interaction.
If a view is sent with an ephemeral message and it has no timeout set then the timeout If a view is sent with an ephemeral message and it has no timeout set then the timeout
is set to 15 minutes. is set to 15 minutes.
allowed_mentions: :class:`~discord.AllowedMentions`
Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for
more information.
Raises Raises
------- -------
HTTPException HTTPException
Sending the message failed. Sending the message failed.
TypeError TypeError
You specified both ``embed`` and ``embeds``. You specified both ``embed`` and ``embeds`` or ``file`` and ``files``.
ValueError ValueError
The length of ``embeds`` was invalid. The length of ``embeds`` was invalid.
InteractionResponded InteractionResponded
@ -498,38 +507,32 @@ class InteractionResponse:
if self._responded: if self._responded:
raise InteractionResponded(self._parent) raise InteractionResponded(self._parent)
payload: Dict[str, Any] = {
'tts': tts,
}
if embed is not MISSING and embeds is not MISSING:
raise TypeError('cannot mix embed and embeds keyword arguments')
if embed is not MISSING:
embeds = [embed]
if embeds:
if len(embeds) > 10:
raise ValueError('embeds cannot exceed maximum of 10 elements')
payload['embeds'] = [e.to_dict() for e in embeds]
if content is not None:
payload['content'] = str(content)
if ephemeral: if ephemeral:
payload['flags'] = 64 flags = MessageFlags._from_value(64)
else:
if view is not MISSING: flags = MISSING
payload['components'] = view.to_components()
parent = self._parent parent = self._parent
adapter = async_context.get() adapter = async_context.get()
params = interaction_message_response_params(
type=InteractionResponseType.channel_message.value,
content=content,
tts=tts,
embeds=embeds,
embed=embed,
file=file,
files=files,
previous_allowed_mentions=parent._state.allowed_mentions,
allowed_mentions=allowed_mentions,
flags=flags,
view=view,
)
await adapter.create_interaction_response( await adapter.create_interaction_response(
parent.id, parent.id,
parent.token, parent.token,
session=parent._session, session=parent._session,
type=InteractionResponseType.channel_message.value, params=params,
data=payload,
) )
if view is not MISSING: if view is not MISSING:
@ -548,6 +551,7 @@ class InteractionResponse:
embeds: List[Embed] = MISSING, embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING, attachments: List[Attachment] = MISSING,
view: Optional[View] = MISSING, view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
) -> None: ) -> None:
"""|coro| """|coro|
@ -569,6 +573,9 @@ class InteractionResponse:
view: Optional[:class:`~discord.ui.View`] view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then The updated view to update this message with. If ``None`` is passed then
the view is removed. the view is removed.
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
Controls the mentions being processed in this message. See :meth:`.Message.edit`
for more information.
Raises Raises
------- -------
@ -589,42 +596,25 @@ class InteractionResponse:
if parent.type is not InteractionType.component: if parent.type is not InteractionType.component:
return return
payload = {} if view is not MISSING and message_id is not None:
if content is not MISSING:
if content is None:
payload['content'] = None
else:
payload['content'] = str(content)
if embed is not MISSING and embeds is not MISSING:
raise TypeError('cannot mix both embed and embeds keyword arguments')
if embed is not MISSING:
if embed is None:
embeds = []
else:
embeds = [embed]
if embeds is not MISSING:
payload['embeds'] = [e.to_dict() for e in embeds]
if attachments is not MISSING:
payload['attachments'] = [a.to_dict() for a in attachments]
if view is not MISSING:
state.prevent_view_updates_for(message_id) state.prevent_view_updates_for(message_id)
if view is None:
payload['components'] = []
else:
payload['components'] = view.to_components()
adapter = async_context.get() adapter = async_context.get()
params = interaction_message_response_params(
type=InteractionResponseType.message_update.value,
content=content,
embed=embed,
embeds=embeds,
attachments=attachments,
previous_allowed_mentions=parent._state.allowed_mentions,
allowed_mentions=allowed_mentions,
)
await adapter.create_interaction_response( await adapter.create_interaction_response(
parent.id, parent.id,
parent.token, parent.token,
session=parent._session, session=parent._session,
type=InteractionResponseType.message_update.value, params=params,
data=payload,
) )
if view and not view.is_finished(): if view and not view.is_finished():

130
discord/webhook/async_.py

@ -43,7 +43,7 @@ from ..enums import try_enum, WebhookType
from ..user import BaseUser, User from ..user import BaseUser, User
from ..flags import MessageFlags from ..flags import MessageFlags
from ..asset import Asset from ..asset import Asset
from ..http import Route, handle_message_parameters from ..http import Route, handle_message_parameters, MultipartParameters
from ..mixins import Hashable from ..mixins import Hashable
from ..channel import PartialMessageable from ..channel import PartialMessageable
@ -60,6 +60,7 @@ if TYPE_CHECKING:
from ..file import File from ..file import File
from ..embeds import Embed from ..embeds import Embed
from ..mentions import AllowedMentions from ..mentions import AllowedMentions
from ..message import Attachment
from ..state import ConnectionState from ..state import ConnectionState
from ..http import Response from ..http import Response
from ..types.webhook import ( from ..types.webhook import (
@ -349,16 +350,8 @@ class AsyncWebhookAdapter:
token: str, token: str,
*, *,
session: aiohttp.ClientSession, session: aiohttp.ClientSession,
type: int, params: MultipartParameters,
data: Optional[Dict[str, Any]] = None,
) -> Response[None]: ) -> Response[None]:
payload: Dict[str, Any] = {
'type': type,
}
if data is not None:
payload['data'] = data
route = Route( route = Route(
'POST', 'POST',
'/interactions/{webhook_id}/{webhook_token}/callback', '/interactions/{webhook_id}/{webhook_token}/callback',
@ -366,7 +359,10 @@ class AsyncWebhookAdapter:
webhook_token=token, webhook_token=token,
) )
return self.request(route, session=session, payload=payload) if params.files:
return self.request(route, session=session, files=params.files, multipart=params.multipart)
else:
return self.request(route, session=session, payload=params.payload)
def get_original_interaction_response( def get_original_interaction_response(
self, self,
@ -417,6 +413,118 @@ class AsyncWebhookAdapter:
return self.request(r, session=session) return self.request(r, session=session)
def interaction_response_params(type: int, data: Optional[Dict[str, Any]] = None) -> MultipartParameters:
payload: Dict[str, Any] = {
'type': type,
}
if data is not None:
payload['data'] = data
return MultipartParameters(payload=payload, multipart=None, files=None)
# This is a subset of handle_message_parameters
def interaction_message_response_params(
*,
type: int,
content: Optional[str] = MISSING,
tts: bool = False,
flags: MessageFlags = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
previous_allowed_mentions: Optional[AllowedMentions] = None,
) -> MultipartParameters:
if files is not MISSING and file is not MISSING:
raise TypeError('Cannot mix file and files keyword arguments.')
if embeds is not MISSING and embed is not MISSING:
raise TypeError('Cannot mix embed and embeds keyword arguments.')
data: Optional[Dict[str, Any]] = {
'tts': tts,
}
if embeds is not MISSING:
if len(embeds) > 10:
raise InvalidArgument('embeds has a maximum of 10 elements.')
data['embeds'] = [e.to_dict() for e in embeds]
if embed is not MISSING:
if embed is None:
data['embeds'] = []
else:
data['embeds'] = [embed.to_dict()]
if content is not MISSING:
if content is not None:
data['content'] = str(content)
else:
data['content'] = None
if view is not MISSING:
if view is not None:
data['components'] = view.to_components()
else:
data['components'] = []
if attachments is not MISSING:
# Note: This will be overwritten if file or files is provided
# However, right now this is only passed via edit not send
data['attachments'] = [a.to_dict() for a in attachments]
if flags is not MISSING:
data['flags'] = flags.value
if allowed_mentions:
if previous_allowed_mentions is not None:
data['allowed_mentions'] = previous_allowed_mentions.merge(allowed_mentions).to_dict()
else:
data['allowed_mentions'] = allowed_mentions.to_dict()
elif previous_allowed_mentions is not None:
data['allowed_mentions'] = previous_allowed_mentions.to_dict()
multipart = []
if file is not MISSING:
files = [file]
if files:
for index, file in enumerate(files):
attachments_payload = []
for index, file in enumerate(files):
attachment = {
'id': index,
'filename': file.filename,
}
if file.description is not None:
attachment['description'] = file.description
attachments_payload.append(attachment)
data['attachments'] = attachments_payload
data = {'type': type, 'data': data}
multipart.append({'name': 'payload_json', 'value': utils._to_json(data)})
data = None
for index, file in enumerate(files):
multipart.append(
{
'name': f'files[{index}]',
'value': file.fp,
'filename': file.filename,
'content_type': 'application/octet-stream',
}
)
else:
data = {'type': type, 'data': data}
return MultipartParameters(payload=data, multipart=multipart, files=files)
async_context: ContextVar[AsyncWebhookAdapter] = ContextVar('async_webhook_context', default=AsyncWebhookAdapter()) async_context: ContextVar[AsyncWebhookAdapter] = ContextVar('async_webhook_context', default=AsyncWebhookAdapter())

Loading…
Cancel
Save