mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-18 23:15:48 +00:00
[commands] Add initial implementation of hybrid commands
Hybrid commands allow a regular command to also double as a slash command, assuming it meets the subset required to function.
This commit is contained in:
parent
151806ec94
commit
840eb577d4
@ -578,10 +578,7 @@ class Command(Generic[GroupT, P, T]):
|
||||
|
||||
return False
|
||||
|
||||
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T:
|
||||
if not await self._check_can_run(interaction):
|
||||
raise CheckFailure(f'The check functions for command {self.name!r} failed.')
|
||||
|
||||
async def _transform_arguments(self, interaction: Interaction, namespace: Namespace) -> Dict[str, Any]:
|
||||
values = namespace.__dict__
|
||||
transformed_values = {}
|
||||
|
||||
@ -596,12 +593,15 @@ class Command(Generic[GroupT, P, T]):
|
||||
else:
|
||||
transformed_values[param.name] = await param.transform(interaction, value)
|
||||
|
||||
return transformed_values
|
||||
|
||||
async def _do_call(self, interaction: Interaction, params: Dict[str, Any]) -> T:
|
||||
# These type ignores are because the type checker doesn't quite understand the narrowing here
|
||||
# Likewise, it thinks we're missing positional arguments when there aren't any.
|
||||
try:
|
||||
if self.binding is not None:
|
||||
return await self._callback(self.binding, interaction, **transformed_values) # type: ignore
|
||||
return await self._callback(interaction, **transformed_values) # type: ignore
|
||||
return await self._callback(self.binding, interaction, **params) # type: ignore
|
||||
return await self._callback(interaction, **params) # type: ignore
|
||||
except TypeError as e:
|
||||
# In order to detect mismatch from the provided signature and the Discord data,
|
||||
# there are many ways it can go wrong yet all of them eventually lead to a TypeError
|
||||
@ -621,6 +621,13 @@ class Command(Generic[GroupT, P, T]):
|
||||
except Exception as e:
|
||||
raise CommandInvokeError(self, e) from e
|
||||
|
||||
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T:
|
||||
if not await self._check_can_run(interaction):
|
||||
raise CheckFailure(f'The check functions for command {self.name!r} failed.')
|
||||
|
||||
transformed_values = await self._transform_arguments(interaction, namespace)
|
||||
return await self._do_call(interaction, transformed_values)
|
||||
|
||||
async def _invoke_autocomplete(self, interaction: Interaction, name: str, namespace: Namespace):
|
||||
# The namespace contains the Discord provided names so this will be fine
|
||||
# even if the name is renamed
|
||||
@ -1234,7 +1241,7 @@ class Group:
|
||||
# <self>
|
||||
# <group>
|
||||
# this needs to be forbidden
|
||||
raise ValueError('groups can only be nested at most one level')
|
||||
raise ValueError(f'{command.name!r} is too nested, groups can only be nested at most one level')
|
||||
|
||||
if not override and command.name in self._children:
|
||||
raise CommandAlreadyRegistered(command.name, guild_id=None)
|
||||
|
@ -18,3 +18,4 @@ from .errors import *
|
||||
from .flags import *
|
||||
from .help import *
|
||||
from .parameters import *
|
||||
from .hybrid import *
|
||||
|
@ -67,6 +67,7 @@ if TYPE_CHECKING:
|
||||
import importlib.machinery
|
||||
|
||||
from discord.message import Message
|
||||
from discord.interactions import Interaction
|
||||
from discord.abc import User, Snowflake
|
||||
from ._types import (
|
||||
_Bot,
|
||||
@ -76,6 +77,7 @@ if TYPE_CHECKING:
|
||||
ContextT,
|
||||
MaybeAwaitableFunc,
|
||||
)
|
||||
from .core import Command
|
||||
|
||||
_Prefix = Union[Iterable[str], str]
|
||||
_PrefixCallable = MaybeAwaitableFunc[[BotT, Message], _Prefix]
|
||||
@ -215,6 +217,38 @@ class BotBase(GroupMixin[None]):
|
||||
|
||||
await super().close() # type: ignore
|
||||
|
||||
# GroupMixin overrides
|
||||
|
||||
@discord.utils.copy_doc(GroupMixin.add_command)
|
||||
def add_command(self, command: Command[Any, ..., Any], /) -> None:
|
||||
super().add_command(command)
|
||||
if hasattr(command, '__commands_is_hybrid__'):
|
||||
# If a cog is also inheriting from app_commands.Group then it'll also
|
||||
# add the hybrid commands as text commands, which would recursively add the
|
||||
# hybrid commands as slash commands. This check just terminates that recursion
|
||||
# from happening
|
||||
if command.cog is None or not command.cog.__cog_is_app_commands_group__:
|
||||
self.tree.add_command(command.app_command) # type: ignore
|
||||
|
||||
@discord.utils.copy_doc(GroupMixin.remove_command)
|
||||
def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]:
|
||||
cmd = super().remove_command(name)
|
||||
if cmd is not None and hasattr(cmd, '__commands_is_hybrid__'):
|
||||
# See above
|
||||
if cmd.cog is not None and cmd.cog.__cog_is_app_commands_group__:
|
||||
return cmd
|
||||
|
||||
guild_ids: Optional[List[int]] = cmd.app_command._guild_ids # type: ignore
|
||||
if guild_ids is None:
|
||||
self.__tree.remove_command(name)
|
||||
else:
|
||||
for guild_id in guild_ids:
|
||||
self.__tree.remove_command(name, guild=discord.Object(id=guild_id))
|
||||
|
||||
return cmd
|
||||
|
||||
# Error handler
|
||||
|
||||
async def on_command_error(self, context: Context[BotT], exception: errors.CommandError, /) -> None:
|
||||
"""|coro|
|
||||
|
||||
@ -1107,7 +1141,7 @@ class BotBase(GroupMixin[None]):
|
||||
@overload
|
||||
async def get_context(
|
||||
self,
|
||||
message: Message,
|
||||
origin: Union[Message, Interaction],
|
||||
/,
|
||||
) -> Context[Self]: # type: ignore
|
||||
...
|
||||
@ -1115,23 +1149,23 @@ class BotBase(GroupMixin[None]):
|
||||
@overload
|
||||
async def get_context(
|
||||
self,
|
||||
message: Message,
|
||||
origin: Union[Message, Interaction],
|
||||
/,
|
||||
*,
|
||||
cls: Type[ContextT] = ...,
|
||||
cls: Type[ContextT],
|
||||
) -> ContextT:
|
||||
...
|
||||
|
||||
async def get_context(
|
||||
self,
|
||||
message: Message,
|
||||
origin: Union[Message, Interaction],
|
||||
/,
|
||||
*,
|
||||
cls: Type[ContextT] = MISSING,
|
||||
) -> Any:
|
||||
r"""|coro|
|
||||
|
||||
Returns the invocation context from the message.
|
||||
Returns the invocation context from the message or interaction.
|
||||
|
||||
This is a more low-level counter-part for :meth:`.process_commands`
|
||||
to allow users more fine grained control over the processing.
|
||||
@ -1141,14 +1175,20 @@ class BotBase(GroupMixin[None]):
|
||||
If the context is not valid then it is not a valid candidate to be
|
||||
invoked under :meth:`~.Bot.invoke`.
|
||||
|
||||
.. note::
|
||||
|
||||
In order for the custom context to be used inside an interaction-based
|
||||
context (such as :class:`HybridCommand`) then this method must be
|
||||
overridden to return that class.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
``message`` parameter is now positional-only.
|
||||
``message`` parameter is now positional-only and renamed to ``origin``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message: :class:`discord.Message`
|
||||
The message to get the invocation context from.
|
||||
origin: Union[:class:`discord.Message`, :class:`discord.Interaction`]
|
||||
The message or interaction to get the invocation context from.
|
||||
cls
|
||||
The factory class that will be used to create the context.
|
||||
By default, this is :class:`.Context`. Should a custom
|
||||
@ -1164,13 +1204,16 @@ class BotBase(GroupMixin[None]):
|
||||
if cls is MISSING:
|
||||
cls = Context # type: ignore
|
||||
|
||||
view = StringView(message.content)
|
||||
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
||||
if isinstance(origin, discord.Interaction):
|
||||
return await cls.from_interaction(origin)
|
||||
|
||||
if message.author.id == self.user.id: # type: ignore
|
||||
view = StringView(origin.content)
|
||||
ctx = cls(prefix=None, view=view, bot=self, message=origin)
|
||||
|
||||
if origin.author.id == self.user.id: # type: ignore
|
||||
return ctx
|
||||
|
||||
prefix = await self.get_prefix(message)
|
||||
prefix = await self.get_prefix(origin)
|
||||
invoked_prefix = prefix
|
||||
|
||||
if isinstance(prefix, str):
|
||||
@ -1180,7 +1223,7 @@ class BotBase(GroupMixin[None]):
|
||||
try:
|
||||
# if the context class' __init__ consumes something from the view this
|
||||
# will be wrong. That seems unreasonable though.
|
||||
if message.content.startswith(tuple(prefix)):
|
||||
if origin.content.startswith(tuple(prefix)):
|
||||
invoked_prefix = discord.utils.find(view.skip_string, prefix)
|
||||
else:
|
||||
return ctx
|
||||
|
@ -239,6 +239,9 @@ class Cog(metaclass=CogMeta):
|
||||
|
||||
lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__}
|
||||
|
||||
# Register the application commands
|
||||
children: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = []
|
||||
|
||||
# Update the Command instances dynamically as well
|
||||
for command in self.__cog_commands__:
|
||||
setattr(self, command.callback.__name__, command)
|
||||
@ -250,9 +253,12 @@ class Cog(metaclass=CogMeta):
|
||||
# Update our parent's reference to our self
|
||||
parent.remove_command(command.name) # type: ignore
|
||||
parent.add_command(command) # type: ignore
|
||||
elif cls.__cog_is_app_commands_group__:
|
||||
if hasattr(command, '__commands_is_hybrid__') and command.parent is None:
|
||||
# In both of these, the type checker does not see the app_command attribute even though it exists
|
||||
command.app_command = command.app_command._copy_with(parent=self, binding=self) # type: ignore
|
||||
children.append(command.app_command) # type: ignore
|
||||
|
||||
# Register the application commands
|
||||
children: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = []
|
||||
for command in cls.__cog_app_commands__:
|
||||
copy = command._copy_with(
|
||||
# Type checker doesn't understand this type of narrowing.
|
||||
|
@ -24,28 +24,35 @@ DEALINGS IN THE SOFTWARE.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union, Sequence
|
||||
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
from discord.message import Message
|
||||
from discord import Interaction, Message, Attachment, MessageType, User, PartialMessageable
|
||||
from .view import StringView
|
||||
|
||||
from ._types import BotT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
from typing_extensions import Self, ParamSpec
|
||||
|
||||
from discord.abc import MessageableChannel
|
||||
from discord.guild import Guild
|
||||
from discord.member import Member
|
||||
from discord.state import ConnectionState
|
||||
from discord.user import ClientUser, User
|
||||
from discord.user import ClientUser
|
||||
from discord.voice_client import VoiceProtocol
|
||||
from discord.embeds import Embed
|
||||
from discord.file import File
|
||||
from discord.mentions import AllowedMentions
|
||||
from discord.sticker import GuildSticker, StickerItem
|
||||
from discord.message import MessageReference, PartialMessage
|
||||
from discord.ui import View
|
||||
from discord.types.interactions import ApplicationCommandInteractionData
|
||||
|
||||
from .cog import Cog
|
||||
from .core import Command
|
||||
from .parameters import Parameter
|
||||
from .view import StringView
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
@ -78,6 +85,12 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
-----------
|
||||
message: :class:`.Message`
|
||||
The message that triggered the command being executed.
|
||||
|
||||
.. note::
|
||||
|
||||
In the case of an interaction based context, this message is "synthetic"
|
||||
and does not actually exist. Therefore, the ID on it is invalid similar
|
||||
to ephemeral messages.
|
||||
bot: :class:`.Bot`
|
||||
The bot that contains the command being executed.
|
||||
args: :class:`list`
|
||||
@ -97,6 +110,10 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
The argument string of the :attr:`current_parameter` that is currently being converted.
|
||||
This is only of use for within converters.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
interaction: Optional[:class:`Interaction`]
|
||||
The interaction associated with this context.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
prefix: Optional[:class:`str`]
|
||||
The prefix that was used to invoke the command.
|
||||
@ -143,6 +160,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
command_failed: bool = False,
|
||||
current_parameter: Optional[Parameter] = None,
|
||||
current_argument: Optional[str] = None,
|
||||
interaction: Optional[Interaction] = None,
|
||||
):
|
||||
self.message: Message = message
|
||||
self.bot: BotT = bot
|
||||
@ -158,8 +176,91 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
self.command_failed: bool = command_failed
|
||||
self.current_parameter: Optional[Parameter] = current_parameter
|
||||
self.current_argument: Optional[str] = current_argument
|
||||
self.interaction: Optional[Interaction] = interaction
|
||||
self._state: ConnectionState = self.message._state
|
||||
|
||||
@classmethod
|
||||
async def from_interaction(cls, interaction: Interaction, /) -> Self:
|
||||
"""|coro|
|
||||
|
||||
Creates a context from a :class:`discord.Interaction`. This only
|
||||
works on application command based interactions, such as slash commands
|
||||
or context menus.
|
||||
|
||||
On slash command based interactions this creates a synthetic :class:`~discord.Message`
|
||||
that points to an ephemeral message that the command invoker has executed. This means
|
||||
that :attr:`Context.author` returns the member that invoked the command.
|
||||
|
||||
In a message context menu based interaction, the :attr:`Context.message` attribute
|
||||
is the message that the command is being executed on. This means that :attr:`Context.author`
|
||||
returns the author of the message being targetted. To get the member that invoked
|
||||
the command then :attr:`discord.Interaction.user` should be used instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`discord.Interaction`
|
||||
The interaction to create a context with.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The interaction does not have a valid command.
|
||||
TypeError
|
||||
The interaction client is not derived from :class:`Bot` or :class:`AutoShardedBot`.
|
||||
"""
|
||||
|
||||
# Circular import
|
||||
from .bot import BotBase
|
||||
|
||||
if not isinstance(interaction.client, BotBase):
|
||||
raise TypeError('Interaction client is not derived from commands.Bot or commands.AutoShardedBot')
|
||||
|
||||
command = interaction.command
|
||||
if command is None:
|
||||
raise ValueError('interaction does not have command data')
|
||||
|
||||
bot: BotT = interaction.client # type: ignore
|
||||
data: ApplicationCommandInteractionData = interaction.data # type: ignore
|
||||
if interaction.message is None:
|
||||
synthetic_payload = {
|
||||
'id': interaction.id,
|
||||
'reactions': [],
|
||||
'embeds': [],
|
||||
'mention_everyone': False,
|
||||
'tts': False,
|
||||
'pinned': False,
|
||||
'edited_timestamp': None,
|
||||
'type': MessageType.chat_input_command if data.get('type', 1) == 1 else MessageType.context_menu_command,
|
||||
'flags': 64,
|
||||
'content': '',
|
||||
'mentions': [],
|
||||
'mention_roles': [],
|
||||
'attachments': [],
|
||||
}
|
||||
|
||||
if interaction.channel_id is None:
|
||||
raise RuntimeError('interaction channel ID is null, this is probably a Discord bug')
|
||||
|
||||
channel = interaction.channel or PartialMessageable(state=interaction._state, id=interaction.channel_id)
|
||||
message = Message(state=interaction._state, channel=channel, data=synthetic_payload) # type: ignore
|
||||
message.author = interaction.user
|
||||
message.attachments = [a for _, a in interaction.namespace if isinstance(a, Attachment)]
|
||||
else:
|
||||
message = interaction.message
|
||||
|
||||
return cls(
|
||||
message=message,
|
||||
bot=bot,
|
||||
view=StringView(''),
|
||||
args=[],
|
||||
kwargs={},
|
||||
interaction=interaction,
|
||||
invoked_with=command.name,
|
||||
command=command, # type: ignore # this will be a hybrid command, technically
|
||||
)
|
||||
|
||||
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||
r"""|coro|
|
||||
|
||||
@ -410,3 +511,189 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
@discord.utils.copy_doc(Message.reply)
|
||||
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
|
||||
return await self.message.reply(content, **kwargs)
|
||||
|
||||
async def defer(self, *, ephemeral: bool = False) -> None:
|
||||
"""|coro|
|
||||
|
||||
Defers the interaction based contexts.
|
||||
|
||||
This is typically used when the interaction is acknowledged
|
||||
and a secondary action will be done later.
|
||||
|
||||
If this isn't an interaction based context then it does nothing.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ephemeral: :class:`bool`
|
||||
Indicates whether the deferred message will eventually be ephemeral.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Deferring the interaction failed.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
|
||||
if self.interaction:
|
||||
await self.interaction.response.defer(ephemeral=ephemeral)
|
||||
|
||||
async def send(
|
||||
self,
|
||||
content: Optional[str] = None,
|
||||
*,
|
||||
tts: bool = False,
|
||||
embed: Optional[Embed] = None,
|
||||
embeds: Optional[Sequence[Embed]] = None,
|
||||
file: Optional[File] = None,
|
||||
files: Optional[Sequence[File]] = None,
|
||||
stickers: Optional[Sequence[Union[GuildSticker, StickerItem]]] = None,
|
||||
delete_after: Optional[float] = None,
|
||||
nonce: Optional[Union[str, int]] = None,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
|
||||
mention_author: Optional[bool] = None,
|
||||
view: Optional[View] = None,
|
||||
suppress_embeds: bool = False,
|
||||
ephemeral: bool = False,
|
||||
) -> Message:
|
||||
"""|coro|
|
||||
|
||||
Sends a message to the destination with the content given.
|
||||
|
||||
This works similarly to :meth:`~discord.abc.Messageable.send` for non-interaction contexts.
|
||||
|
||||
For interaction based contexts this does one of the following:
|
||||
|
||||
- :meth:`discord.InteractionResponse.send_message` if no response has been given.
|
||||
- A followup message if a response has been given.
|
||||
- Regular send if the interaction has expired
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
This function will now raise :exc:`TypeError` or
|
||||
:exc:`ValueError` instead of ``InvalidArgument``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
content: Optional[:class:`str`]
|
||||
The content of the message to send.
|
||||
tts: :class:`bool`
|
||||
Indicates if the message should be sent using text-to-speech.
|
||||
embed: :class:`~discord.Embed`
|
||||
The rich embed for the content.
|
||||
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.
|
||||
nonce: :class:`int`
|
||||
The nonce to use for sending this message. If the message was successfully sent,
|
||||
then the message will have a nonce with this value.
|
||||
delete_after: :class:`float`
|
||||
If provided, the number of seconds to wait in the background
|
||||
before deleting the message we just sent. If the deletion fails,
|
||||
then it is silently ignored. This is ignored for interaction based contexts.
|
||||
allowed_mentions: :class:`~discord.AllowedMentions`
|
||||
Controls the mentions being processed in this message. If this is
|
||||
passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
|
||||
The merging behaviour only overrides attributes that have been explicitly passed
|
||||
to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
|
||||
If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
|
||||
are used instead.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`, :class:`~discord.PartialMessage`]
|
||||
A reference to the :class:`~discord.Message` to which you are replying, this can be created using
|
||||
:meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. You can control
|
||||
whether this mentions the author of the referenced message using the :attr:`~discord.AllowedMentions.replied_user`
|
||||
attribute of ``allowed_mentions`` or by setting ``mention_author``.
|
||||
|
||||
This is ignored for interaction based contexts.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
mention_author: Optional[:class:`bool`]
|
||||
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
|
||||
This is ignored for interaction based contexts.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
view: :class:`discord.ui.View`
|
||||
A Discord UI View to add to the message.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
embeds: List[:class:`~discord.Embed`]
|
||||
A list of embeds to upload. Must be a maximum of 10.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]]
|
||||
A list of stickers to upload. Must be a maximum of 3. This is ignored for interaction based contexts.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
suppress_embeds: :class:`bool`
|
||||
Whether to suppress embeds for the message. This sends the message without any embeds if set to ``True``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
ephemeral: :class:`bool`
|
||||
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. **This is only applicable in contexts with an interaction**.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Raises
|
||||
--------
|
||||
~discord.HTTPException
|
||||
Sending the message failed.
|
||||
~discord.Forbidden
|
||||
You do not have the proper permissions to send the message.
|
||||
ValueError
|
||||
The ``files`` list is not of the appropriate size.
|
||||
TypeError
|
||||
You specified both ``file`` and ``files``,
|
||||
or you specified both ``embed`` and ``embeds``,
|
||||
or the ``reference`` object is not a :class:`~discord.Message`,
|
||||
:class:`~discord.MessageReference` or :class:`~discord.PartialMessage`.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`~discord.Message`
|
||||
The message that was sent.
|
||||
"""
|
||||
|
||||
if self.interaction is None or self.interaction.is_expired():
|
||||
return await super().send(
|
||||
content=content,
|
||||
tts=tts,
|
||||
embed=embed,
|
||||
embeds=embeds,
|
||||
file=file,
|
||||
files=files,
|
||||
stickers=stickers,
|
||||
delete_after=delete_after,
|
||||
nonce=nonce,
|
||||
allowed_mentions=allowed_mentions,
|
||||
reference=reference,
|
||||
mention_author=mention_author,
|
||||
view=view,
|
||||
suppress_embeds=suppress_embeds,
|
||||
) # type: ignore # The overloads don't support Optional but the implementation does
|
||||
|
||||
# Convert the kwargs from None to MISSING to appease the remaining implementations
|
||||
kwargs = {
|
||||
'content': content,
|
||||
'tts': tts,
|
||||
'embed': MISSING if embed is None else embed,
|
||||
'embeds': MISSING if embeds is None else embeds,
|
||||
'file': MISSING if file is None else file,
|
||||
'files': MISSING if files is None else files,
|
||||
'allowed_mentions': MISSING if allowed_mentions is None else allowed_mentions,
|
||||
'view': MISSING if view is None else view,
|
||||
'suppress_embeds': suppress_embeds,
|
||||
'ephemeral': ephemeral,
|
||||
}
|
||||
|
||||
if self.interaction.response.is_done():
|
||||
return await self.interaction.followup.send(**kwargs, wait=True)
|
||||
|
||||
await self.interaction.response.send_message(**kwargs)
|
||||
return await self.interaction.original_message()
|
||||
|
@ -395,7 +395,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
||||
self.require_var_positional: bool = kwargs.get('require_var_positional', False)
|
||||
self.ignore_extra: bool = kwargs.get('ignore_extra', True)
|
||||
self.cooldown_after_parsing: bool = kwargs.get('cooldown_after_parsing', False)
|
||||
self.cog: CogT = None
|
||||
self._cog: CogT = None
|
||||
|
||||
# bandaid for the fact that sometimes parent can be the bot instance
|
||||
parent: Optional[GroupMixin[Any]] = kwargs.get('parent')
|
||||
@ -417,6 +417,14 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
||||
else:
|
||||
self.after_invoke(after_invoke)
|
||||
|
||||
@property
|
||||
def cog(self) -> CogT:
|
||||
return self._cog
|
||||
|
||||
@cog.setter
|
||||
def cog(self, value: CogT) -> None:
|
||||
self._cog = value
|
||||
|
||||
@property
|
||||
def callback(
|
||||
self,
|
||||
|
@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
||||
from discord.abc import GuildChannel
|
||||
from discord.threads import Thread
|
||||
from discord.types.snowflake import Snowflake, SnowflakeList
|
||||
from discord.app_commands import AppCommandError
|
||||
|
||||
from ._types import BotT
|
||||
from .context import Context
|
||||
@ -100,6 +101,7 @@ __all__ = (
|
||||
'MissingFlagArgument',
|
||||
'TooManyFlags',
|
||||
'MissingRequiredFlag',
|
||||
'HybridCommandError',
|
||||
)
|
||||
|
||||
|
||||
@ -1123,3 +1125,22 @@ class MissingFlagArgument(FlagError):
|
||||
def __init__(self, flag: Flag) -> None:
|
||||
self.flag: Flag = flag
|
||||
super().__init__(f'Flag {flag.name!r} does not have an argument')
|
||||
|
||||
|
||||
class HybridCommandError(CommandError):
|
||||
"""An exception raised when a :class:`~discord.ext.commands.HybridCommand` raises
|
||||
an :exc:`~discord.app_commands.AppCommandError` derived exception that could not be
|
||||
sufficiently converted to an equivalent :exc:`CommandError` exception.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original: :exc:`~discord.app_commands.AppCommandError`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, original: AppCommandError) -> None:
|
||||
self.original: AppCommandError = original
|
||||
super().__init__(f'Hybrid command raised an error: {original}')
|
||||
|
458
discord/ext/commands/hybrid.py
Normal file
458
discord/ext/commands/hybrid.py
Normal file
@ -0,0 +1,458 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
Optional,
|
||||
)
|
||||
|
||||
import discord
|
||||
import inspect
|
||||
from discord import app_commands
|
||||
from discord.utils import MISSING, maybe_coroutine, async_all
|
||||
from .core import Command, Group
|
||||
from .errors import CommandRegistrationError, CommandError, HybridCommandError, ConversionError
|
||||
from .converter import Converter
|
||||
from .cog import Cog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self, ParamSpec, Concatenate
|
||||
from ._types import ContextT, Coro, BotT
|
||||
from .bot import Bot
|
||||
from .context import Context
|
||||
from .parameters import Parameter
|
||||
from discord.app_commands.commands import Check as AppCommandCheck
|
||||
|
||||
|
||||
__all__ = (
|
||||
'HybridCommand',
|
||||
'HybridGroup',
|
||||
'hybrid_command',
|
||||
'hybrid_group',
|
||||
)
|
||||
|
||||
T = TypeVar('T')
|
||||
CogT = TypeVar('CogT', bound='Cog')
|
||||
CommandT = TypeVar('CommandT', bound='Command')
|
||||
# CHT = TypeVar('CHT', bound='Check')
|
||||
GroupT = TypeVar('GroupT', bound='Group')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
P = ParamSpec('P')
|
||||
P2 = ParamSpec('P2')
|
||||
|
||||
CommandCallback = Union[
|
||||
Callable[Concatenate[CogT, ContextT, P], Coro[T]],
|
||||
Callable[Concatenate[ContextT, P], Coro[T]],
|
||||
]
|
||||
else:
|
||||
P = TypeVar('P')
|
||||
P2 = TypeVar('P2')
|
||||
|
||||
|
||||
def is_converter(converter: Any) -> bool:
|
||||
return (inspect.isclass(converter) and issubclass(converter, Converter)) or isinstance(converter, Converter)
|
||||
|
||||
|
||||
def make_converter_transformer(converter: Any) -> Type[app_commands.Transformer]:
|
||||
async def transform(cls, interaction: discord.Interaction, value: str) -> Any:
|
||||
try:
|
||||
if inspect.isclass(converter) and issubclass(converter, Converter):
|
||||
if inspect.ismethod(converter.convert):
|
||||
return await converter.convert(interaction._baton, value)
|
||||
else:
|
||||
return await converter().convert(interaction._baton, value) # type: ignore
|
||||
elif isinstance(converter, Converter):
|
||||
return await converter.convert(interaction._baton, value) # type: ignore
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise ConversionError(converter, exc) from exc # type: ignore
|
||||
|
||||
return type('ConverterTransformer', (app_commands.Transformer,), {'transform': classmethod(transform)})
|
||||
|
||||
|
||||
def replace_parameters(parameters: Dict[str, Parameter], signature: inspect.Signature) -> List[inspect.Parameter]:
|
||||
# Need to convert commands.Parameter back to inspect.Parameter so this will be a bit ugly
|
||||
params = signature.parameters.copy()
|
||||
for name, parameter in parameters.items():
|
||||
if is_converter(parameter.converter) and not hasattr(parameter.converter, '__discord_app_commands_transformer__'):
|
||||
params[name] = params[name].replace(annotation=make_converter_transformer(parameter.converter))
|
||||
|
||||
return list(params.values())
|
||||
|
||||
|
||||
class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
|
||||
def __init__(self, wrapped: HybridCommand[CogT, Any, T]) -> None:
|
||||
signature = inspect.signature(wrapped.callback)
|
||||
params = replace_parameters(wrapped.params, signature)
|
||||
wrapped.callback.__signature__ = signature.replace(parameters=params)
|
||||
|
||||
try:
|
||||
super().__init__(
|
||||
name=wrapped.name,
|
||||
callback=wrapped.callback, # type: ignore # Signature doesn't match but we're overriding the invoke
|
||||
description=wrapped.description or wrapped.short_doc or '…',
|
||||
)
|
||||
finally:
|
||||
del wrapped.callback.__signature__
|
||||
|
||||
self.wrapped: HybridCommand[CogT, Any, T] = wrapped
|
||||
self.binding = wrapped.cog
|
||||
|
||||
def _copy_with(self, **kwargs) -> Self:
|
||||
copy: Self = super()._copy_with(**kwargs) # type: ignore
|
||||
copy.wrapped = self.wrapped
|
||||
return copy
|
||||
|
||||
async def _check_can_run(self, interaction: discord.Interaction) -> bool:
|
||||
# Hybrid checks must run like so:
|
||||
# - Bot global check once
|
||||
# - Bot global check
|
||||
# - Parent interaction check
|
||||
# - Cog/group interaction check
|
||||
# - Cog check
|
||||
# - Local interaction checks
|
||||
# - Local command checks
|
||||
|
||||
bot: Bot = interaction.client # type: ignore
|
||||
ctx: Context[Bot] = interaction._baton
|
||||
|
||||
if not await bot.can_run(ctx, call_once=True):
|
||||
return False
|
||||
|
||||
if not await bot.can_run(ctx):
|
||||
return False
|
||||
|
||||
if self.parent is not None and self.parent is not self.binding:
|
||||
# For commands with a parent which isn't the binding, i.e.
|
||||
# <binding>
|
||||
# <parent>
|
||||
# <command>
|
||||
# The parent check needs to be called first
|
||||
if not await maybe_coroutine(self.parent.interaction_check, interaction):
|
||||
return False
|
||||
|
||||
if self.binding is not None:
|
||||
try:
|
||||
# Type checker does not like runtime attribute retrieval
|
||||
check: AppCommandCheck = self.binding.interaction_check # type: ignore
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
ret = await maybe_coroutine(check, interaction)
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
local_check = Cog._get_overridden_method(self.binding.cog_check)
|
||||
if local_check is not None:
|
||||
ret = await maybe_coroutine(local_check, ctx)
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
if self.checks and not await async_all(f(interaction) for f in self.checks): # type: ignore
|
||||
return False
|
||||
|
||||
if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): # type: ignore
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _invoke_with_namespace(self, interaction: discord.Interaction, namespace: app_commands.Namespace) -> Any:
|
||||
# Wrap the interaction into a Context
|
||||
bot: Bot = interaction.client # type: ignore
|
||||
|
||||
# Unfortunately, `get_context` has to be called for this to work.
|
||||
# If someone doesn't inherit this to replace it with their custom class
|
||||
# then this doesn't work.
|
||||
interaction._baton = ctx = await bot.get_context(interaction)
|
||||
|
||||
exc: CommandError
|
||||
try:
|
||||
await self.wrapped.prepare(ctx)
|
||||
# This lies and just always passes a Context instead of an Interaction.
|
||||
return await self._do_call(ctx, ctx.kwargs) # type: ignore
|
||||
except app_commands.CommandSignatureMismatch:
|
||||
raise
|
||||
except app_commands.TransformerError as e:
|
||||
if isinstance(e.__cause__, CommandError):
|
||||
exc = e.__cause__
|
||||
else:
|
||||
exc = HybridCommandError(e)
|
||||
exc.__cause__ = e
|
||||
except app_commands.AppCommandError as e:
|
||||
exc = HybridCommandError(e)
|
||||
exc.__cause__ = e
|
||||
except CommandError as e:
|
||||
exc = e
|
||||
|
||||
await self.wrapped.dispatch_error(ctx, exc)
|
||||
|
||||
|
||||
class HybridCommand(Command[CogT, P, T]):
|
||||
r"""A class that is both an application command and a regular text command.
|
||||
|
||||
This has the same parameters and attributes as a regular :class:`~discord.ext.commands.Command`.
|
||||
However, it also doubles as an :class:`application command <discord.app_commands.Command>`. In order
|
||||
for this to work, the callbacks must have the same subset that is supported by application
|
||||
commands.
|
||||
|
||||
These are not created manually, instead they are created via the
|
||||
decorator or functional interface.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__commands_is_hybrid__: ClassVar[bool] = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: CommandCallback[CogT, ContextT, P, T],
|
||||
/,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(func, **kwargs)
|
||||
self.app_command: HybridAppCommand[CogT, Any, T] = HybridAppCommand(self)
|
||||
|
||||
@property
|
||||
def cog(self) -> CogT:
|
||||
return self._cog
|
||||
|
||||
@cog.setter
|
||||
def cog(self, value: CogT) -> None:
|
||||
self._cog = value
|
||||
self.app_command.binding = value
|
||||
|
||||
async def can_run(self, ctx: Context[BotT], /) -> bool:
|
||||
if ctx.interaction is None:
|
||||
return await super().can_run(ctx)
|
||||
else:
|
||||
return await self.app_command._check_can_run(ctx.interaction)
|
||||
|
||||
async def _parse_arguments(self, ctx: Context[BotT]) -> None:
|
||||
interaction = ctx.interaction
|
||||
if interaction is None:
|
||||
return await super()._parse_arguments(ctx)
|
||||
else:
|
||||
ctx.kwargs = await self.app_command._transform_arguments(interaction, interaction.namespace)
|
||||
|
||||
|
||||
class HybridGroup(Group[CogT, P, T]):
|
||||
r"""A class that is both an application command group and a regular text group.
|
||||
|
||||
This has the same parameters and attributes as a regular :class:`~discord.ext.commands.Group`.
|
||||
However, it also doubles as an :class:`application command group <discord.app_commands.Group>`.
|
||||
Note that application commands groups cannot have callbacks associated with them, so the callback
|
||||
is only called if it's not invoked as an application command.
|
||||
|
||||
These are not created manually, instead they are created via the
|
||||
decorator or functional interface.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__commands_is_hybrid__: ClassVar[bool] = True
|
||||
|
||||
def __init__(self, *args: Any, **attrs: Any) -> None:
|
||||
super().__init__(*args, **attrs)
|
||||
parent = None
|
||||
if self.parent is not None:
|
||||
if isinstance(self.parent, HybridGroup):
|
||||
parent = self.parent.app_command
|
||||
else:
|
||||
raise TypeError(f'HybridGroup parent must be HybridGroup not {self.parent.__class__}')
|
||||
|
||||
guild_ids = attrs.pop('guild_ids', None) or getattr(self.callback, '__discord_app_commands_default_guilds__', None)
|
||||
self.app_command: app_commands.Group = app_commands.Group(
|
||||
name=self.name,
|
||||
description=self.description or self.short_doc or '…',
|
||||
guild_ids=guild_ids,
|
||||
)
|
||||
|
||||
# This prevents the group from re-adding the command at __init__
|
||||
self.app_command.parent = parent
|
||||
|
||||
def add_command(self, command: Union[HybridGroup[CogT, ..., Any], HybridCommand[CogT, ..., Any]], /) -> None:
|
||||
"""Adds a :class:`.HybridCommand` into the internal list of commands.
|
||||
|
||||
This is usually not called, instead the :meth:`~.GroupMixin.command` or
|
||||
:meth:`~.GroupMixin.group` shortcut decorators are used instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`HybridCommand`
|
||||
The command to add.
|
||||
|
||||
Raises
|
||||
-------
|
||||
CommandRegistrationError
|
||||
If the command or its alias is already registered by different command.
|
||||
TypeError
|
||||
If the command passed is not a subclass of :class:`.HybridCommand`.
|
||||
"""
|
||||
|
||||
if not isinstance(command, (HybridCommand, HybridGroup)):
|
||||
raise TypeError('The command passed must be a subclass of HybridCommand or HybridGroup')
|
||||
|
||||
if isinstance(command, HybridGroup) and self.parent is not None:
|
||||
raise ValueError(f'{command.qualified_name!r} is too nested, groups can only be nested at most one level')
|
||||
|
||||
self.app_command.add_command(command.app_command)
|
||||
command.parent = self
|
||||
|
||||
if command.name in self.all_commands:
|
||||
raise CommandRegistrationError(command.name)
|
||||
|
||||
self.all_commands[command.name] = command
|
||||
for alias in command.aliases:
|
||||
if alias in self.all_commands:
|
||||
self.remove_command(command.name)
|
||||
raise CommandRegistrationError(alias, alias_conflict=True)
|
||||
self.all_commands[alias] = command
|
||||
|
||||
def remove_command(self, name: str, /) -> Optional[Command[CogT, ..., Any]]:
|
||||
cmd = super().remove_command(name)
|
||||
self.app_command.remove_command(name)
|
||||
return cmd
|
||||
|
||||
def command(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[CommandCallback[CogT, ContextT, P2, T]], HybridCommand[CogT, P2, T]]:
|
||||
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
|
||||
the internal command list via :meth:`add_command`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[..., :class:`Command`]
|
||||
A decorator that converts the provided method into a Command, adds it to the bot, then returns it.
|
||||
"""
|
||||
|
||||
def decorator(func: CommandCallback[CogT, ContextT, P2, T]):
|
||||
kwargs.setdefault('parent', self)
|
||||
result = hybrid_command(name=name, *args, **kwargs)(func)
|
||||
self.add_command(result)
|
||||
return result
|
||||
|
||||
return decorator
|
||||
|
||||
def group(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[CommandCallback[CogT, ContextT, P2, T]], HybridGroup[CogT, P2, T]]:
|
||||
"""A shortcut decorator that invokes :func:`.group` and adds it to
|
||||
the internal command list via :meth:`~.GroupMixin.add_command`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[..., :class:`Group`]
|
||||
A decorator that converts the provided method into a Group, adds it to the bot, then returns it.
|
||||
"""
|
||||
|
||||
def decorator(func: CommandCallback[CogT, ContextT, P2, T]):
|
||||
kwargs.setdefault('parent', self)
|
||||
result = hybrid_group(name=name, *args, **kwargs)(func)
|
||||
self.add_command(result)
|
||||
return result
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def hybrid_command(
|
||||
name: str = MISSING,
|
||||
**attrs: Any,
|
||||
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]:
|
||||
"""A decorator that transforms a function into a :class:`.HybridCommand`.
|
||||
|
||||
A hybrid command is one that functions both as a regular :class:`.Command`
|
||||
and one that is also a :class:`app_commands.Command <discord.app_commands.Command>`.
|
||||
|
||||
The callback being attached to the command must be representable as an
|
||||
application command callback. Converters are silently converted into a
|
||||
:class:`~discord.app_commands.Transformer` with a
|
||||
:attr:`discord.AppCommandOptionType.string` type.
|
||||
|
||||
Checks and error handlers are dispatched and called as-if they were commands
|
||||
similar to :class:`.Command`. This means that they take :class:`Context` as
|
||||
a parameter rather than :class:`discord.Interaction`.
|
||||
|
||||
All checks added using the :func:`.check` & co. decorators are added into
|
||||
the function. There is no way to supply your own checks through this
|
||||
decorator.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name to create the command with. By default this uses the
|
||||
function name unchanged.
|
||||
attrs
|
||||
Keyword arguments to pass into the construction of the
|
||||
hybrid command.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
If the function is not a coroutine or is already a command.
|
||||
"""
|
||||
|
||||
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
|
||||
if isinstance(func, Command):
|
||||
raise TypeError('Callback is already a command.')
|
||||
return HybridCommand(func, name=name, **attrs)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def hybrid_group(
|
||||
name: str = MISSING,
|
||||
**attrs: Any,
|
||||
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]:
|
||||
"""A decorator that transforms a function into a :class:`.HybridGroup`.
|
||||
|
||||
This is similar to the :func:`~discord.ext.commands.group` decorator except it creates
|
||||
a hybrid group instead.
|
||||
"""
|
||||
|
||||
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
|
||||
if isinstance(func, Command):
|
||||
raise TypeError('Callback is already a command.')
|
||||
return HybridGroup(func, name=name, **attrs)
|
||||
|
||||
return decorator # type: ignore
|
@ -132,6 +132,7 @@ class Interaction:
|
||||
'_state',
|
||||
'_client',
|
||||
'_session',
|
||||
'_baton',
|
||||
'_original_message',
|
||||
'_cs_response',
|
||||
'_cs_followup',
|
||||
@ -145,6 +146,9 @@ class Interaction:
|
||||
self._client: Client = state._get_client()
|
||||
self._session: ClientSession = state.http._HTTPClient__session # type: ignore # Mangled attribute for __session
|
||||
self._original_message: Optional[InteractionMessage] = None
|
||||
# This baton is used for extra data that might be useful for the lifecycle of
|
||||
# an interaction. This is mainly for internal purposes and it gives it a free-for-all slot.
|
||||
self._baton: Any = MISSING
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data: InteractionPayload):
|
||||
|
@ -114,6 +114,13 @@ Decorators
|
||||
.. autofunction:: discord.ext.commands.group
|
||||
:decorator:
|
||||
|
||||
.. autofunction:: discord.ext.commands.hybrid_command
|
||||
:decorator:
|
||||
|
||||
.. autofunction:: discord.ext.commands.hybrid_group
|
||||
:decorator:
|
||||
|
||||
|
||||
Command
|
||||
~~~~~~~~~
|
||||
|
||||
@ -173,6 +180,51 @@ GroupMixin
|
||||
.. automethod:: GroupMixin.group(*args, **kwargs)
|
||||
:decorator:
|
||||
|
||||
HybridCommand
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ext.commands.HybridCommand
|
||||
|
||||
.. autoclass:: discord.ext.commands.HybridCommand
|
||||
:members:
|
||||
:special-members: __call__
|
||||
:exclude-members: after_invoke, before_invoke, error
|
||||
|
||||
.. automethod:: HybridCommand.after_invoke()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridCommand.before_invoke()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridCommand.error()
|
||||
:decorator:
|
||||
|
||||
HybridGroup
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ext.commands.HybridGroup
|
||||
|
||||
.. autoclass:: discord.ext.commands.HybridGroup
|
||||
:members:
|
||||
:inherited-members:
|
||||
:exclude-members: after_invoke, before_invoke, command, error, group
|
||||
|
||||
.. automethod:: HybridGroup.after_invoke()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridGroup.before_invoke()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridGroup.command(*args, **kwargs)
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridGroup.error()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: HybridGroup.group(*args, **kwargs)
|
||||
:decorator:
|
||||
|
||||
|
||||
.. _ext_commands_api_cogs:
|
||||
|
||||
Cogs
|
||||
@ -631,6 +683,9 @@ Exceptions
|
||||
.. autoexception:: discord.ext.commands.CommandRegistrationError
|
||||
:members:
|
||||
|
||||
.. autoexception:: discord.ext.commands.HybridCommandError
|
||||
:members:
|
||||
|
||||
|
||||
Exception Hierarchy
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -687,6 +742,7 @@ Exception Hierarchy
|
||||
- :exc:`~.commands.CommandInvokeError`
|
||||
- :exc:`~.commands.CommandOnCooldown`
|
||||
- :exc:`~.commands.MaxConcurrencyReached`
|
||||
- :exc:`~.commands.HybridCommandError`
|
||||
- :exc:`~.commands.ExtensionError`
|
||||
- :exc:`~.commands.ExtensionAlreadyLoaded`
|
||||
- :exc:`~.commands.ExtensionNotLoaded`
|
||||
|
Loading…
x
Reference in New Issue
Block a user