Browse Source

Add Interaction.command and Interaction.namespace attributes

pull/7710/head
Rapptz 3 years ago
parent
commit
202b993da3
  1. 30
      discord/app_commands/commands.py
  2. 2
      discord/app_commands/namespace.py
  3. 109
      discord/app_commands/tree.py
  4. 61
      discord/interactions.py

30
discord/app_commands/commands.py

@ -47,7 +47,6 @@ from textwrap import TextWrapper
import re import re
from ..enums import AppCommandOptionType, AppCommandType from ..enums import AppCommandOptionType, AppCommandType
from ..interactions import Interaction
from .models import Choice from .models import Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType from .transformers import annotation_to_parameter, CommandParameter, NoneType
from .errors import AppCommandError, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered from .errors import AppCommandError, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
@ -58,6 +57,7 @@ from ..utils import resolve_annotation, MISSING, is_inside_class
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import ParamSpec, Concatenate from typing_extensions import ParamSpec, Concatenate
from ..interactions import Interaction
from ..abc import Snowflake from ..abc import Snowflake
from .namespace import Namespace from .namespace import Namespace
from .models import ChoiceT from .models import ChoiceT
@ -88,32 +88,32 @@ T = TypeVar('T')
GroupT = TypeVar('GroupT', bound='Union[Group, Cog]') GroupT = TypeVar('GroupT', bound='Union[Group, Cog]')
Coro = Coroutine[Any, Any, T] Coro = Coroutine[Any, Any, T]
Error = Union[ Error = Union[
Callable[[GroupT, Interaction, AppCommandError], Coro[Any]], Callable[[GroupT, 'Interaction', AppCommandError], Coro[Any]],
Callable[[Interaction, AppCommandError], Coro[Any]], Callable[['Interaction', AppCommandError], Coro[Any]],
] ]
if TYPE_CHECKING: if TYPE_CHECKING:
CommandCallback = Union[ CommandCallback = Union[
Callable[Concatenate[GroupT, Interaction, P], Coro[T]], Callable[Concatenate[GroupT, 'Interaction', P], Coro[T]],
Callable[Concatenate[Interaction, P], Coro[T]], Callable[Concatenate['Interaction', P], Coro[T]],
] ]
ContextMenuCallback = Union[ ContextMenuCallback = Union[
# If groups end up support context menus these would be uncommented # If groups end up support context menus these would be uncommented
# Callable[[GroupT, Interaction, Member], Coro[Any]], # Callable[[GroupT, 'Interaction', Member], Coro[Any]],
# Callable[[GroupT, Interaction, User], Coro[Any]], # Callable[[GroupT, 'Interaction', User], Coro[Any]],
# Callable[[GroupT, Interaction, Message], Coro[Any]], # Callable[[GroupT, 'Interaction', Message], Coro[Any]],
# Callable[[GroupT, Interaction, Union[Member, User]], Coro[Any]], # Callable[[GroupT, 'Interaction', Union[Member, User]], Coro[Any]],
Callable[[Interaction, Member], Coro[Any]], Callable[['Interaction', Member], Coro[Any]],
Callable[[Interaction, User], Coro[Any]], Callable[['Interaction', User], Coro[Any]],
Callable[[Interaction, Message], Coro[Any]], Callable[['Interaction', Message], Coro[Any]],
Callable[[Interaction, Union[Member, User]], Coro[Any]], Callable[['Interaction', Union[Member, User]], Coro[Any]],
] ]
AutocompleteCallback = Union[ AutocompleteCallback = Union[
Callable[[GroupT, Interaction, ChoiceT, Namespace], Coro[List[Choice[ChoiceT]]]], Callable[[GroupT, 'Interaction', ChoiceT, Namespace], Coro[List[Choice[ChoiceT]]]],
Callable[[Interaction, ChoiceT, Namespace], Coro[List[Choice[ChoiceT]]]], Callable[['Interaction', ChoiceT, Namespace], Coro[List[Choice[ChoiceT]]]],
] ]
else: else:
CommandCallback = Callable[..., Coro[T]] CommandCallback = Callable[..., Coro[T]]

2
discord/app_commands/namespace.py

@ -25,7 +25,6 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, NamedTuple, Tuple from typing import TYPE_CHECKING, Any, Dict, Iterable, List, NamedTuple, Tuple
from ..interactions import Interaction
from ..member import Member from ..member import Member
from ..object import Object from ..object import Object
from ..role import Role from ..role import Role
@ -35,6 +34,7 @@ from ..enums import AppCommandOptionType
from .models import AppCommandChannel, AppCommandThread from .models import AppCommandChannel, AppCommandThread
if TYPE_CHECKING: if TYPE_CHECKING:
from ..interactions import Interaction
from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption
__all__ = ('Namespace',) __all__ = ('Namespace',)

109
discord/app_commands/tree.py

@ -878,10 +878,69 @@ class CommandTree(Generic[ClientT]):
self.client.loop.create_task(wrapper(), name='CommandTree-invoker') self.client.loop.create_task(wrapper(), name='CommandTree-invoker')
def _get_context_menu(self, data: ApplicationCommandInteractionData) -> Optional[ContextMenu]:
name = data['name']
guild_id = _get_as_snowflake(data, 'guild_id')
return self._context_menus.get((name, guild_id, data.get('type', 1)))
def _get_app_command_options(
self, data: ApplicationCommandInteractionData
) -> Tuple[Command[Any, ..., Any], List[ApplicationCommandInteractionDataOption]]:
parents: List[str] = []
name = data['name']
command_guild_id = _get_as_snowflake(data, 'guild_id')
if command_guild_id:
try:
guild_commands = self._guild_commands[command_guild_id]
except KeyError:
command = None
else:
command = guild_commands.get(name)
else:
command = self._global_commands.get(name)
# If it's not found at this point then it's not gonna be found at any point
if command is None:
raise CommandNotFound(name, parents)
# This could be done recursively but it'd be a bother due to the state needed
# to be tracked above like the parents, the actual command type, and the
# resulting options we care about
searching = True
options: List[ApplicationCommandInteractionDataOption] = data.get('options', [])
while searching:
for option in options:
# Find subcommands
if option.get('type', 0) in (1, 2):
parents.append(name)
name = option['name']
command = command._get_internal_command(name)
if command is None:
raise CommandNotFound(name, parents)
options = option.get('options', [])
break
else:
searching = False
break
else:
break
if isinstance(command, Group):
# Right now, groups can't be invoked. This is a Discord limitation in how they
# do slash commands. So if we're here and we have a Group rather than a Command instance
# then something in the code is out of date from the data that Discord has.
raise CommandSignatureMismatch(command)
return (command, options)
async def _call_context_menu(self, interaction: Interaction, data: ApplicationCommandInteractionData, type: int) -> None: async def _call_context_menu(self, interaction: Interaction, data: ApplicationCommandInteractionData, type: int) -> None:
name = data['name'] name = data['name']
guild_id = _get_as_snowflake(data, 'guild_id') guild_id = _get_as_snowflake(data, 'guild_id')
ctx_menu = self._context_menus.get((name, guild_id, type)) ctx_menu = self._context_menus.get((name, guild_id, type))
# Pre-fill the cached slot to prevent re-computation
interaction._cs_command = ctx_menu
if ctx_menu is None: if ctx_menu is None:
raise CommandNotFound(name, [], AppCommandType(type)) raise CommandNotFound(name, [], AppCommandType(type))
@ -936,56 +995,18 @@ class CommandTree(Generic[ClientT]):
await self._call_context_menu(interaction, data, type) await self._call_context_menu(interaction, data, type)
return return
parents: List[str] = [] command, options = self._get_app_command_options(data)
name = data['name']
command_guild_id = _get_as_snowflake(data, 'guild_id')
if command_guild_id:
try:
guild_commands = self._guild_commands[command_guild_id]
except KeyError:
command = None
else:
command = guild_commands.get(name)
else:
command = self._global_commands.get(name)
# If it's not found at this point then it's not gonna be found at any point
if command is None:
raise CommandNotFound(name, parents)
# This could be done recursively but it'd be a bother due to the state needed
# to be tracked above like the parents, the actual command type, and the
# resulting options we care about
searching = True
options: List[ApplicationCommandInteractionDataOption] = data.get('options', [])
while searching:
for option in options:
# Find subcommands
if option.get('type', 0) in (1, 2):
parents.append(name)
name = option['name']
command = command._get_internal_command(name)
if command is None:
raise CommandNotFound(name, parents)
options = option.get('options', [])
break
else:
searching = False
break
else:
break
if isinstance(command, Group): # Pre-fill the cached slot to prevent re-computation
# Right now, groups can't be invoked. This is a Discord limitation in how they interaction._cs_command = command
# do slash commands. So if we're here and we have a Group rather than a Command instance
# then something in the code is out of date from the data that Discord has.
raise CommandSignatureMismatch(command)
# At this point options refers to the arguments of the command # At this point options refers to the arguments of the command
# and command refers to the class type we care about # and command refers to the class type we care about
namespace = Namespace(interaction, data.get('resolved', {}), options) namespace = Namespace(interaction, data.get('resolved', {}), options)
# Same pre-fill as above
interaction._cs_namespace = namespace
# Auto complete handles the namespace differently... so at this point this is where we decide where that is. # Auto complete handles the namespace differently... so at this point this is where we decide where that is.
if interaction.type is InteractionType.autocomplete: if interaction.type is InteractionType.autocomplete:
focused = next((opt['name'] for opt in options if opt.get('focused')), None) focused = next((opt['name'] for opt in options if opt.get('focused')), None)

61
discord/interactions.py

@ -25,13 +25,13 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Sequence, Tuple, Union from typing import Any, Dict, Optional, TYPE_CHECKING, Sequence, Tuple, Union
import asyncio import asyncio
import datetime import datetime
from . import utils from . import utils
from .enums import try_enum, Locale, InteractionType, InteractionResponseType from .enums import try_enum, Locale, InteractionType, InteractionResponseType
from .errors import InteractionResponded, HTTPException, ClientException from .errors import InteractionResponded, HTTPException, ClientException, DiscordException
from .flags import MessageFlags from .flags import MessageFlags
from .channel import PartialMessageable, ChannelType from .channel import PartialMessageable, ChannelType
@ -42,6 +42,7 @@ from .object import Object
from .permissions import Permissions from .permissions import Permissions
from .http import handle_message_parameters from .http import handle_message_parameters
from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params
from .app_commands.namespace import Namespace
__all__ = ( __all__ = (
'Interaction', 'Interaction',
@ -53,6 +54,7 @@ if TYPE_CHECKING:
from .types.interactions import ( from .types.interactions import (
Interaction as InteractionPayload, Interaction as InteractionPayload,
InteractionData, InteractionData,
ApplicationCommandInteractionData,
) )
from .types.webhook import ( from .types.webhook import (
Webhook as WebhookPayload, Webhook as WebhookPayload,
@ -69,6 +71,7 @@ if TYPE_CHECKING:
from .ui.modal import Modal from .ui.modal import Modal
from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable
from .threads import Thread from .threads import Thread
from .app_commands.commands import Command, ContextMenu
InteractionChannel = Union[ InteractionChannel = Union[
VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable
@ -133,6 +136,8 @@ class Interaction:
'_cs_response', '_cs_response',
'_cs_followup', '_cs_followup',
'_cs_channel', '_cs_channel',
'_cs_namespace',
'_cs_command',
) )
def __init__(self, *, data: InteractionPayload, state: ConnectionState): def __init__(self, *, data: InteractionPayload, state: ConnectionState):
@ -220,6 +225,58 @@ class Interaction:
""" """
return Permissions(self._permissions) return Permissions(self._permissions)
@utils.cached_slot_property('_cs_namespace')
def namespace(self) -> Namespace:
""":class:`app_commands.Namespace`: The resolved namespace for this interaction.
If the interaction is not an application command related interaction or the client does not have a
tree attached to it then this returns an empty namespace.
"""
if self.type not in (InteractionType.application_command, InteractionType.autocomplete):
return Namespace(self, {}, [])
tree = self._state._command_tree
if tree is None:
return Namespace(self, {}, [])
# The type checker does not understand this narrowing
data: ApplicationCommandInteractionData = self.data # type: ignore
try:
_, options = tree._get_app_command_options(data)
except DiscordException:
options = []
return Namespace(self, data.get('resolved', {}), options)
@utils.cached_slot_property('_cs_command')
def command(self) -> Optional[Union[Command[Any, ..., Any], ContextMenu]]:
"""Optional[Union[:class:`app_commands.Command`, :class:`app_commands.ContextMenu`]]: The command being called from
this interaction.
If the interaction is not an application command related interaction or the command is not found in the client's
attached tree then ``None`` is returned.
"""
if self.type not in (InteractionType.application_command, InteractionType.autocomplete):
return None
tree = self._state._command_tree
if tree is None:
return None
# The type checker does not understand this narrowing
data: ApplicationCommandInteractionData = self.data # type: ignore
cmd_type = data.get('type', 1)
if cmd_type == 1:
try:
command, _ = tree._get_app_command_options(data)
except DiscordException:
return None
else:
return command
else:
return tree._get_context_menu(data)
@utils.cached_slot_property('_cs_response') @utils.cached_slot_property('_cs_response')
def response(self) -> InteractionResponse: def response(self) -> InteractionResponse:
""":class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction. """:class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction.

Loading…
Cancel
Save