Implement a ctx.send helper for slash commands

This commit is contained in:
Gnome 2021-08-31 12:12:21 +01:00
parent a19e43675f
commit 614c6bca67
4 changed files with 95 additions and 18 deletions

View File

@ -74,7 +74,7 @@ CXT = TypeVar('CXT', bound='Context')
class _FakeSlashMessage(discord.PartialMessage): class _FakeSlashMessage(discord.PartialMessage):
activity = application = edited_at = reference = webhook_id = None activity = application = edited_at = reference = webhook_id = None
attachments = components = reactions = stickers = [] attachments = components = reactions = stickers = mentions = []
author: Union[discord.User, discord.Member] author: Union[discord.User, discord.Member]
tts = False tts = False
@ -1066,7 +1066,6 @@ class BotBase(GroupMixin):
return return
assert interaction.user is not None assert interaction.user is not None
interaction.data = cast(ApplicationCommandInteractionData, interaction.data) interaction.data = cast(ApplicationCommandInteractionData, interaction.data)
# Ensure the interaction channel is usable # Ensure the interaction channel is usable
@ -1105,7 +1104,6 @@ class BotBase(GroupMixin):
message.content = f'{prefix}{command_name} ' message.content = f'{prefix}{command_name} '
for name, param in command.clean_params.items(): for name, param in command.clean_params.items():
option = next((o for o in command_options if o['name'] == name), None) # type: ignore option = next((o for o in command_options if o['name'] == name), None) # type: ignore
print(name, param, option)
if option is None: if option is None:
if not command._is_typing_optional(param.annotation): if not command._is_typing_optional(param.annotation):
@ -1224,7 +1222,7 @@ class Bot(BotBase, discord.Client):
return return
application = self.application_id or (await self.application_info()).id application = self.application_id or (await self.application_info()).id
commands = [scmd for cmd in self.commands if (scmd := cmd.to_application_command()) is not None] commands = [scmd for cmd in self.commands if not cmd.hidden and (scmd := cmd.to_application_command()) is not None]
await self.http.bulk_upsert_guild_commands(application, self.slash_command_guild, payload=commands) await self.http.bulk_upsert_guild_commands(application, self.slash_command_guild, payload=commands)

View File

@ -25,8 +25,8 @@ from __future__ import annotations
import inspect import inspect
import re import re
from datetime import timedelta
from typing import Any, Dict, Generic, List, Optional, TYPE_CHECKING, TypeVar, Union from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
import discord.abc import discord.abc
import discord.utils import discord.utils
@ -41,6 +41,7 @@ if TYPE_CHECKING:
from discord.member import Member from discord.member import Member
from discord.state import ConnectionState from discord.state import ConnectionState
from discord.user import ClientUser, User from discord.user import ClientUser, User
from discord.webhook import WebhookMessage
from discord.interactions import Interaction from discord.interactions import Interaction
from discord.voice_client import VoiceProtocol from discord.voice_client import VoiceProtocol
@ -397,6 +398,81 @@ class Context(discord.abc.Messageable, Generic[BotT]):
except CommandError as e: except CommandError as e:
await cmd.on_help_command_error(self, e) await cmd.on_help_command_error(self, e)
@overload
async def send(self,
content: Optional[str] = None,
return_message: Literal[False] = False,
ephemeral: bool = False, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]: ...
@overload
async def send(self,
content: Optional[str] = None,
return_message: Literal[True] = True,
ephemeral: bool = False, **kwargs: Any
) -> Union[Message, WebhookMessage]: ...
async def send(self,
content: Optional[str] = None,
return_message: bool = True,
ephemeral: bool = False, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]:
"""
|coro|
A shortcut method to :meth:`.abc.Messageable.send` with interaction helpers.
This function takes all the parameters of :meth:`.abc.Messageable.send` plus the following:
Parameters
------------
return_message: :class:`bool`
Ignored if not in a slash command context.
If this is set to False more native interaction methods will be used.
ephemeral: :class:`bool`
Ignored if not in a slash command context.
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
is set to 15 minutes.
Returns
--------
Optional[Union[:class:`.Message`, :class:`.WebhookMessage`]]
In a slash command context, the message that was sent if return_message is True.
In a normal context, it always returns a :class:`.Message`
"""
if (
self.interaction is None
or (
self.interaction.response.responded_at is not None
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
)):
return await super().send(content, **kwargs)
# Remove unsupported arguments from kwargs
kwargs.pop("nonce", None)
kwargs.pop("stickers", None)
kwargs.pop("reference", None)
kwargs.pop("delete_after", None)
kwargs.pop("mention_author", None)
if not (
return_message
or self.interaction.response.is_done()
or any(arg in kwargs for arg in ("file", "files", "allowed_mentions"))
):
send = self.interaction.response.send_message
else:
# We have to defer in order to use the followup webhook
if not self.interaction.response.is_done():
await self.interaction.response.defer(ephemeral=ephemeral)
send = self.interaction.followup.send
return await send(content, ephemeral=ephemeral, **kwargs) # type: ignore
@discord.utils.copy_doc(Message.reply) @discord.utils.copy_doc(Message.reply)
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
return await self.message.reply(content, **kwargs) return await self.message.reply(content, **kwargs)

View File

@ -47,6 +47,8 @@ __all__ = (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from datetime import datetime
from .types.interactions import ( from .types.interactions import (
Interaction as InteractionPayload, Interaction as InteractionPayload,
InteractionData, InteractionData,
@ -369,20 +371,20 @@ class InteractionResponse:
""" """
__slots__: Tuple[str, ...] = ( __slots__: Tuple[str, ...] = (
'_responded', 'responded_at',
'_parent', '_parent',
) )
def __init__(self, parent: Interaction): def __init__(self, parent: Interaction):
self.responded_at: Optional[datetime] = None
self._parent: Interaction = parent self._parent: Interaction = parent
self._responded: bool = False
def is_done(self) -> bool: def is_done(self) -> bool:
""":class:`bool`: Indicates whether an interaction response has been done before. """:class:`bool`: Indicates whether an interaction response has been done before.
An interaction can only be responded to once. An interaction can only be responded to once.
""" """
return self._responded return self.responded_at is not None
async def defer(self, *, ephemeral: bool = False) -> None: async def defer(self, *, ephemeral: bool = False) -> None:
"""|coro| """|coro|
@ -405,7 +407,7 @@ class InteractionResponse:
InteractionResponded InteractionResponded
This interaction has already been responded to before. This interaction has already been responded to before.
""" """
if self._responded: if self.is_done():
raise InteractionResponded(self._parent) raise InteractionResponded(self._parent)
defer_type: int = 0 defer_type: int = 0
@ -423,7 +425,8 @@ class InteractionResponse:
await adapter.create_interaction_response( await adapter.create_interaction_response(
parent.id, parent.token, session=parent._session, type=defer_type, data=data parent.id, parent.token, session=parent._session, type=defer_type, data=data
) )
self._responded = True
self.responded_at = utils.utcnow()
async def pong(self) -> None: async def pong(self) -> None:
"""|coro| """|coro|
@ -439,7 +442,7 @@ class InteractionResponse:
InteractionResponded InteractionResponded
This interaction has already been responded to before. This interaction has already been responded to before.
""" """
if self._responded: if self.is_done():
raise InteractionResponded(self._parent) raise InteractionResponded(self._parent)
parent = self._parent parent = self._parent
@ -448,7 +451,7 @@ class InteractionResponse:
await adapter.create_interaction_response( await adapter.create_interaction_response(
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
) )
self._responded = True self.responded_at = utils.utcnow()
async def send_message( async def send_message(
self, self,
@ -494,7 +497,7 @@ class InteractionResponse:
InteractionResponded InteractionResponded
This interaction has already been responded to before. This interaction has already been responded to before.
""" """
if self._responded: if self.is_done():
raise InteractionResponded(self._parent) raise InteractionResponded(self._parent)
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
@ -537,7 +540,7 @@ class InteractionResponse:
self._parent._state.store_view(view) self._parent._state.store_view(view)
self._responded = True self.responded_at = utils.utcnow()
async def edit_message( async def edit_message(
self, self,
@ -578,7 +581,7 @@ class InteractionResponse:
InteractionResponded InteractionResponded
This interaction has already been responded to before. This interaction has already been responded to before.
""" """
if self._responded: if self.is_done():
raise InteractionResponded(self._parent) raise InteractionResponded(self._parent)
parent = self._parent parent = self._parent
@ -629,7 +632,7 @@ class InteractionResponse:
if view and not view.is_finished(): if view and not view.is_finished():
state.store_view(view, message_id) state.store_view(view, message_id)
self._responded = True self.responded_at = utils.utcnow()
class _InteractionMessageState: class _InteractionMessageState:

View File

@ -357,7 +357,7 @@ class View:
return return
await item.callback(interaction) await item.callback(interaction)
if not interaction.response._responded: if not interaction.response.is_done():
await interaction.response.defer() await interaction.response.defer()
except Exception as e: except Exception as e:
return await self.on_error(e, item, interaction) return await self.on_error(e, item, interaction)