From 614c6bca6755df3adf94b5942d596ce30a64c955 Mon Sep 17 00:00:00 2001 From: Gnome Date: Tue, 31 Aug 2021 12:12:21 +0100 Subject: [PATCH] Implement a ctx.send helper for slash commands --- discord/ext/commands/bot.py | 6 +-- discord/ext/commands/context.py | 80 ++++++++++++++++++++++++++++++++- discord/interactions.py | 25 ++++++----- discord/ui/view.py | 2 +- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 92c00ea3..a5a727d1 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -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) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index a751c6bb..2e05208c 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -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) diff --git a/discord/interactions.py b/discord/interactions.py index f4849a2f..c9f629d0 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -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: diff --git a/discord/ui/view.py b/discord/ui/view.py index 13510eea..115d1ec4 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -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)