mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-07 12:18:59 +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
|
return False
|
||||||
|
|
||||||
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T:
|
async def _transform_arguments(self, interaction: Interaction, namespace: Namespace) -> Dict[str, Any]:
|
||||||
if not await self._check_can_run(interaction):
|
|
||||||
raise CheckFailure(f'The check functions for command {self.name!r} failed.')
|
|
||||||
|
|
||||||
values = namespace.__dict__
|
values = namespace.__dict__
|
||||||
transformed_values = {}
|
transformed_values = {}
|
||||||
|
|
||||||
@ -596,12 +593,15 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
else:
|
else:
|
||||||
transformed_values[param.name] = await param.transform(interaction, value)
|
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
|
# 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.
|
# Likewise, it thinks we're missing positional arguments when there aren't any.
|
||||||
try:
|
try:
|
||||||
if self.binding is not None:
|
if self.binding is not None:
|
||||||
return await self._callback(self.binding, interaction, **transformed_values) # type: ignore
|
return await self._callback(self.binding, interaction, **params) # type: ignore
|
||||||
return await self._callback(interaction, **transformed_values) # type: ignore
|
return await self._callback(interaction, **params) # type: ignore
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
# In order to detect mismatch from the provided signature and the Discord data,
|
# 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
|
# 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:
|
except Exception as e:
|
||||||
raise CommandInvokeError(self, e) from 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):
|
async def _invoke_autocomplete(self, interaction: Interaction, name: str, namespace: Namespace):
|
||||||
# The namespace contains the Discord provided names so this will be fine
|
# The namespace contains the Discord provided names so this will be fine
|
||||||
# even if the name is renamed
|
# even if the name is renamed
|
||||||
@ -1234,7 +1241,7 @@ class Group:
|
|||||||
# <self>
|
# <self>
|
||||||
# <group>
|
# <group>
|
||||||
# this needs to be forbidden
|
# 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:
|
if not override and command.name in self._children:
|
||||||
raise CommandAlreadyRegistered(command.name, guild_id=None)
|
raise CommandAlreadyRegistered(command.name, guild_id=None)
|
||||||
|
@ -18,3 +18,4 @@ from .errors import *
|
|||||||
from .flags import *
|
from .flags import *
|
||||||
from .help import *
|
from .help import *
|
||||||
from .parameters import *
|
from .parameters import *
|
||||||
|
from .hybrid import *
|
||||||
|
@ -67,6 +67,7 @@ if TYPE_CHECKING:
|
|||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
|
|
||||||
from discord.message import Message
|
from discord.message import Message
|
||||||
|
from discord.interactions import Interaction
|
||||||
from discord.abc import User, Snowflake
|
from discord.abc import User, Snowflake
|
||||||
from ._types import (
|
from ._types import (
|
||||||
_Bot,
|
_Bot,
|
||||||
@ -76,6 +77,7 @@ if TYPE_CHECKING:
|
|||||||
ContextT,
|
ContextT,
|
||||||
MaybeAwaitableFunc,
|
MaybeAwaitableFunc,
|
||||||
)
|
)
|
||||||
|
from .core import Command
|
||||||
|
|
||||||
_Prefix = Union[Iterable[str], str]
|
_Prefix = Union[Iterable[str], str]
|
||||||
_PrefixCallable = MaybeAwaitableFunc[[BotT, Message], _Prefix]
|
_PrefixCallable = MaybeAwaitableFunc[[BotT, Message], _Prefix]
|
||||||
@ -215,6 +217,38 @@ class BotBase(GroupMixin[None]):
|
|||||||
|
|
||||||
await super().close() # type: ignore
|
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:
|
async def on_command_error(self, context: Context[BotT], exception: errors.CommandError, /) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@ -1107,7 +1141,7 @@ class BotBase(GroupMixin[None]):
|
|||||||
@overload
|
@overload
|
||||||
async def get_context(
|
async def get_context(
|
||||||
self,
|
self,
|
||||||
message: Message,
|
origin: Union[Message, Interaction],
|
||||||
/,
|
/,
|
||||||
) -> Context[Self]: # type: ignore
|
) -> Context[Self]: # type: ignore
|
||||||
...
|
...
|
||||||
@ -1115,23 +1149,23 @@ class BotBase(GroupMixin[None]):
|
|||||||
@overload
|
@overload
|
||||||
async def get_context(
|
async def get_context(
|
||||||
self,
|
self,
|
||||||
message: Message,
|
origin: Union[Message, Interaction],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
cls: Type[ContextT] = ...,
|
cls: Type[ContextT],
|
||||||
) -> ContextT:
|
) -> ContextT:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def get_context(
|
async def get_context(
|
||||||
self,
|
self,
|
||||||
message: Message,
|
origin: Union[Message, Interaction],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
cls: Type[ContextT] = MISSING,
|
cls: Type[ContextT] = MISSING,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
r"""|coro|
|
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`
|
This is a more low-level counter-part for :meth:`.process_commands`
|
||||||
to allow users more fine grained control over the processing.
|
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
|
If the context is not valid then it is not a valid candidate to be
|
||||||
invoked under :meth:`~.Bot.invoke`.
|
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
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
``message`` parameter is now positional-only.
|
``message`` parameter is now positional-only and renamed to ``origin``.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
message: :class:`discord.Message`
|
origin: Union[:class:`discord.Message`, :class:`discord.Interaction`]
|
||||||
The message to get the invocation context from.
|
The message or interaction to get the invocation context from.
|
||||||
cls
|
cls
|
||||||
The factory class that will be used to create the context.
|
The factory class that will be used to create the context.
|
||||||
By default, this is :class:`.Context`. Should a custom
|
By default, this is :class:`.Context`. Should a custom
|
||||||
@ -1164,13 +1204,16 @@ class BotBase(GroupMixin[None]):
|
|||||||
if cls is MISSING:
|
if cls is MISSING:
|
||||||
cls = Context # type: ignore
|
cls = Context # type: ignore
|
||||||
|
|
||||||
view = StringView(message.content)
|
if isinstance(origin, discord.Interaction):
|
||||||
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
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
|
return ctx
|
||||||
|
|
||||||
prefix = await self.get_prefix(message)
|
prefix = await self.get_prefix(origin)
|
||||||
invoked_prefix = prefix
|
invoked_prefix = prefix
|
||||||
|
|
||||||
if isinstance(prefix, str):
|
if isinstance(prefix, str):
|
||||||
@ -1180,7 +1223,7 @@ class BotBase(GroupMixin[None]):
|
|||||||
try:
|
try:
|
||||||
# if the context class' __init__ consumes something from the view this
|
# if the context class' __init__ consumes something from the view this
|
||||||
# will be wrong. That seems unreasonable though.
|
# 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)
|
invoked_prefix = discord.utils.find(view.skip_string, prefix)
|
||||||
else:
|
else:
|
||||||
return ctx
|
return ctx
|
||||||
|
@ -239,6 +239,9 @@ class Cog(metaclass=CogMeta):
|
|||||||
|
|
||||||
lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__}
|
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
|
# Update the Command instances dynamically as well
|
||||||
for command in self.__cog_commands__:
|
for command in self.__cog_commands__:
|
||||||
setattr(self, command.callback.__name__, command)
|
setattr(self, command.callback.__name__, command)
|
||||||
@ -250,9 +253,12 @@ class Cog(metaclass=CogMeta):
|
|||||||
# Update our parent's reference to our self
|
# Update our parent's reference to our self
|
||||||
parent.remove_command(command.name) # type: ignore
|
parent.remove_command(command.name) # type: ignore
|
||||||
parent.add_command(command) # 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__:
|
for command in cls.__cog_app_commands__:
|
||||||
copy = command._copy_with(
|
copy = command._copy_with(
|
||||||
# Type checker doesn't understand this type of narrowing.
|
# Type checker doesn't understand this type of narrowing.
|
||||||
|
@ -24,28 +24,35 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
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.abc
|
||||||
import discord.utils
|
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
|
from ._types import BotT
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import Self, ParamSpec
|
||||||
|
|
||||||
from discord.abc import MessageableChannel
|
from discord.abc import MessageableChannel
|
||||||
from discord.guild import Guild
|
from discord.guild import Guild
|
||||||
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
|
||||||
from discord.voice_client import VoiceProtocol
|
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 .cog import Cog
|
||||||
from .core import Command
|
from .core import Command
|
||||||
from .parameters import Parameter
|
from .parameters import Parameter
|
||||||
from .view import StringView
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -78,6 +85,12 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
-----------
|
-----------
|
||||||
message: :class:`.Message`
|
message: :class:`.Message`
|
||||||
The message that triggered the command being executed.
|
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`
|
bot: :class:`.Bot`
|
||||||
The bot that contains the command being executed.
|
The bot that contains the command being executed.
|
||||||
args: :class:`list`
|
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.
|
The argument string of the :attr:`current_parameter` that is currently being converted.
|
||||||
This is only of use for within converters.
|
This is only of use for within converters.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
interaction: Optional[:class:`Interaction`]
|
||||||
|
The interaction associated with this context.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
prefix: Optional[:class:`str`]
|
prefix: Optional[:class:`str`]
|
||||||
The prefix that was used to invoke the command.
|
The prefix that was used to invoke the command.
|
||||||
@ -143,6 +160,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
command_failed: bool = False,
|
command_failed: bool = False,
|
||||||
current_parameter: Optional[Parameter] = None,
|
current_parameter: Optional[Parameter] = None,
|
||||||
current_argument: Optional[str] = None,
|
current_argument: Optional[str] = None,
|
||||||
|
interaction: Optional[Interaction] = None,
|
||||||
):
|
):
|
||||||
self.message: Message = message
|
self.message: Message = message
|
||||||
self.bot: BotT = bot
|
self.bot: BotT = bot
|
||||||
@ -158,8 +176,91 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
self.command_failed: bool = command_failed
|
self.command_failed: bool = command_failed
|
||||||
self.current_parameter: Optional[Parameter] = current_parameter
|
self.current_parameter: Optional[Parameter] = current_parameter
|
||||||
self.current_argument: Optional[str] = current_argument
|
self.current_argument: Optional[str] = current_argument
|
||||||
|
self.interaction: Optional[Interaction] = interaction
|
||||||
self._state: ConnectionState = self.message._state
|
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:
|
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
r"""|coro|
|
r"""|coro|
|
||||||
|
|
||||||
@ -410,3 +511,189 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
@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)
|
||||||
|
|
||||||
|
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.require_var_positional: bool = kwargs.get('require_var_positional', False)
|
||||||
self.ignore_extra: bool = kwargs.get('ignore_extra', True)
|
self.ignore_extra: bool = kwargs.get('ignore_extra', True)
|
||||||
self.cooldown_after_parsing: bool = kwargs.get('cooldown_after_parsing', False)
|
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
|
# bandaid for the fact that sometimes parent can be the bot instance
|
||||||
parent: Optional[GroupMixin[Any]] = kwargs.get('parent')
|
parent: Optional[GroupMixin[Any]] = kwargs.get('parent')
|
||||||
@ -417,6 +417,14 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
|||||||
else:
|
else:
|
||||||
self.after_invoke(after_invoke)
|
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
|
@property
|
||||||
def callback(
|
def callback(
|
||||||
self,
|
self,
|
||||||
|
@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
|||||||
from discord.abc import GuildChannel
|
from discord.abc import GuildChannel
|
||||||
from discord.threads import Thread
|
from discord.threads import Thread
|
||||||
from discord.types.snowflake import Snowflake, SnowflakeList
|
from discord.types.snowflake import Snowflake, SnowflakeList
|
||||||
|
from discord.app_commands import AppCommandError
|
||||||
|
|
||||||
from ._types import BotT
|
from ._types import BotT
|
||||||
from .context import Context
|
from .context import Context
|
||||||
@ -100,6 +101,7 @@ __all__ = (
|
|||||||
'MissingFlagArgument',
|
'MissingFlagArgument',
|
||||||
'TooManyFlags',
|
'TooManyFlags',
|
||||||
'MissingRequiredFlag',
|
'MissingRequiredFlag',
|
||||||
|
'HybridCommandError',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1123,3 +1125,22 @@ class MissingFlagArgument(FlagError):
|
|||||||
def __init__(self, flag: Flag) -> None:
|
def __init__(self, flag: Flag) -> None:
|
||||||
self.flag: Flag = flag
|
self.flag: Flag = flag
|
||||||
super().__init__(f'Flag {flag.name!r} does not have an argument')
|
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',
|
'_state',
|
||||||
'_client',
|
'_client',
|
||||||
'_session',
|
'_session',
|
||||||
|
'_baton',
|
||||||
'_original_message',
|
'_original_message',
|
||||||
'_cs_response',
|
'_cs_response',
|
||||||
'_cs_followup',
|
'_cs_followup',
|
||||||
@ -145,6 +146,9 @@ class Interaction:
|
|||||||
self._client: Client = state._get_client()
|
self._client: Client = state._get_client()
|
||||||
self._session: ClientSession = state.http._HTTPClient__session # type: ignore # Mangled attribute for __session
|
self._session: ClientSession = state.http._HTTPClient__session # type: ignore # Mangled attribute for __session
|
||||||
self._original_message: Optional[InteractionMessage] = None
|
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)
|
self._from_data(data)
|
||||||
|
|
||||||
def _from_data(self, data: InteractionPayload):
|
def _from_data(self, data: InteractionPayload):
|
||||||
|
@ -114,6 +114,13 @@ Decorators
|
|||||||
.. autofunction:: discord.ext.commands.group
|
.. autofunction:: discord.ext.commands.group
|
||||||
:decorator:
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.ext.commands.hybrid_command
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.ext.commands.hybrid_group
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
|
||||||
Command
|
Command
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
@ -173,6 +180,51 @@ GroupMixin
|
|||||||
.. automethod:: GroupMixin.group(*args, **kwargs)
|
.. automethod:: GroupMixin.group(*args, **kwargs)
|
||||||
:decorator:
|
: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:
|
.. _ext_commands_api_cogs:
|
||||||
|
|
||||||
Cogs
|
Cogs
|
||||||
@ -631,6 +683,9 @@ Exceptions
|
|||||||
.. autoexception:: discord.ext.commands.CommandRegistrationError
|
.. autoexception:: discord.ext.commands.CommandRegistrationError
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.HybridCommandError
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Exception Hierarchy
|
Exception Hierarchy
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -687,6 +742,7 @@ Exception Hierarchy
|
|||||||
- :exc:`~.commands.CommandInvokeError`
|
- :exc:`~.commands.CommandInvokeError`
|
||||||
- :exc:`~.commands.CommandOnCooldown`
|
- :exc:`~.commands.CommandOnCooldown`
|
||||||
- :exc:`~.commands.MaxConcurrencyReached`
|
- :exc:`~.commands.MaxConcurrencyReached`
|
||||||
|
- :exc:`~.commands.HybridCommandError`
|
||||||
- :exc:`~.commands.ExtensionError`
|
- :exc:`~.commands.ExtensionError`
|
||||||
- :exc:`~.commands.ExtensionAlreadyLoaded`
|
- :exc:`~.commands.ExtensionAlreadyLoaded`
|
||||||
- :exc:`~.commands.ExtensionNotLoaded`
|
- :exc:`~.commands.ExtensionNotLoaded`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user