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):
activity = application = edited_at = reference = webhook_id = None
attachments = components = reactions = stickers = []
attachments = components = reactions = stickers = mentions = []
author: Union[discord.User, discord.Member]
tts = False
@ -1066,7 +1066,6 @@ class BotBase(GroupMixin):
return
assert interaction.user is not None
interaction.data = cast(ApplicationCommandInteractionData, interaction.data)
# Ensure the interaction channel is usable
@ -1105,7 +1104,6 @@ class BotBase(GroupMixin):
message.content = f'{prefix}{command_name} '
for name, param in command.clean_params.items():
option = next((o for o in command_options if o['name'] == name), None) # type: ignore
print(name, param, option)
if option is None:
if not command._is_typing_optional(param.annotation):
@ -1224,7 +1222,7 @@ class Bot(BotBase, discord.Client):
return
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)

View File

@ -25,8 +25,8 @@ from __future__ import annotations
import inspect
import re
from typing import Any, Dict, Generic, List, Optional, TYPE_CHECKING, TypeVar, Union
from datetime import timedelta
from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
import discord.abc
import discord.utils
@ -41,6 +41,7 @@ if TYPE_CHECKING:
from discord.member import Member
from discord.state import ConnectionState
from discord.user import ClientUser, User
from discord.webhook import WebhookMessage
from discord.interactions import Interaction
from discord.voice_client import VoiceProtocol
@ -397,6 +398,81 @@ class Context(discord.abc.Messageable, Generic[BotT]):
except CommandError as 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)
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
return await self.message.reply(content, **kwargs)

View File

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

View File

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