Browse Source

Actually type commands properly

pull/10109/head
dolfies 3 years ago
parent
commit
118b2f4d01
  1. 152
      discord/commands.py

152
discord/commands.py

@ -24,12 +24,11 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from asyncio import TimeoutError
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union from typing import Any, Dict, List, Optional, Protocol, Tuple, runtime_checkable, TYPE_CHECKING, Union
from .enums import CommandType, ChannelType, OptionType, try_enum from .enums import CommandType, ChannelType, OptionType, try_enum
from .errors import InvalidData from .errors import InvalidData, InvalidArgument
from .utils import time_snowflake from .utils import time_snowflake
if TYPE_CHECKING: if TYPE_CHECKING:
@ -39,29 +38,37 @@ if TYPE_CHECKING:
from .state import ConnectionState from .state import ConnectionState
class ApplicationCommand: @runtime_checkable
def __init__( class ApplicationCommand(Protocol):
self, data: Dict[str, Any]
) -> None: __slots__ = ()
self.name: str = data['name']
self.description: str = data['description'] if TYPE_CHECKING:
_state: ConnectionState
_application_id: int
name: str
description: str
version: int
type: CommandType
target_channel: Optional[Messageable]
default_permission: bool
async def __call__(self, data, channel: Optional[Messageable] = None) -> Interaction: async def __call__(self, data, channel: Optional[Messageable] = None) -> Interaction:
channel = channel or self.target_channel # type: ignore channel = channel or self.target_channel
if channel is None: if channel is None:
raise TypeError('__call__() missing 1 required keyword-only argument: \'channel\'') raise TypeError('__call__() missing 1 required keyword-only argument: \'channel\'')
state = self._state # type: ignore state = self._state
channel = await channel._get_channel() channel = await channel._get_channel()
payload = { payload = {
'application_id': str(self._application_id), # type: ignore 'application_id': str(self._application_id),
'channel_id': str(channel.id), 'channel_id': str(channel.id),
'data': data, 'data': data,
'nonce': str(time_snowflake(datetime.utcnow())), 'nonce': str(time_snowflake(datetime.utcnow())),
'type': 2, # Should be an enum but eh 'type': 2, # Should be an enum but eh
} }
if getattr(channel, 'guild', None): if getattr(channel, 'guild', None) is not None:
payload['guild_id'] = str(channel.guild.id) # type: ignore payload['guild_id'] = str(channel.guild.id)
state._interactions[payload['nonce']] = 2 state._interactions[payload['nonce']] = 2
await state.http.interact(payload, form_data=True) await state.http.interact(payload, form_data=True)
@ -76,17 +83,33 @@ class ApplicationCommand:
return i return i
class _BaseCommand(ApplicationCommand): class BaseCommand(ApplicationCommand):
__slots__ = (
'name',
'description',
'id',
'version',
'type',
'default_permission',
'_dm_permission',
'_default_member_permissions',
'_state',
'_channel',
'_application_id'
)
def __init__( def __init__(
self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None
) -> None: ) -> None:
super().__init__(data) self.name = data['name']
self.description = data['description']
self._state = state self._state = state
self._channel = channel self._channel = channel
self._application_id: int = int(data['application_id']) self._application_id: int = int(data['application_id'])
self.id: int = int(data['id']) self.id: int = int(data['id'])
self.version: int = int(data['version']) self.version = int(data['version'])
self.type: CommandType = try_enum(CommandType, data['type']) self.type = try_enum(CommandType, data['type'])
self.default_permission: bool = data['default_permission'] self.default_permission: bool = data['default_permission']
self._dm_permission = data['dm_permission'] self._dm_permission = data['dm_permission']
self._default_member_permissions = data['default_member_permissions'] self._default_member_permissions = data['default_member_permissions']
@ -128,19 +151,23 @@ class _BaseCommand(ApplicationCommand):
self._channel = value self._channel = value
class _SlashMixin: class SlashMixin(ApplicationCommand):
if TYPE_CHECKING:
_parent: SlashCommand
options: List[Option]
children: List[SubCommand]
async def __call__(self, options, channel=None): async def __call__(self, options, channel=None):
# This will always be used in a context where all these attributes are set obj = self._parent
obj = getattr(self, '_parent', self)
data = { data = {
'attachments': [], 'attachments': [],
'id': str(obj.id), # type: ignore 'id': str(obj.id),
'name': obj.name, # type: ignore 'name': obj.name,
'options': options, 'options': options,
'type': obj.type.value, # type: ignore 'type': obj.type.value,
'version': str(obj.version), # type: ignore 'version': str(obj.version),
} }
return await super().__call__(data, channel) # type: ignore return await super().__call__(data, channel)
def _parse_kwargs(self, kwargs: Dict[str, Any]) -> List[Dict[str, Any]]: def _parse_kwargs(self, kwargs: Dict[str, Any]) -> List[Dict[str, Any]]:
possible_options = {o.name: o for o in self.options} possible_options = {o.name: o for o in self.options}
@ -190,11 +217,11 @@ class _SlashMixin:
for child in children: for child in children:
setattr(self, child.name, child) setattr(self, child.name, child)
self.options: List[Option] = options self.options = options
self.children: List[SubCommand] = children self.children = children
class UserCommand(_BaseCommand): class UserCommand(BaseCommand):
"""Represents a user command. """Represents a user command.
Attributes Attributes
@ -210,6 +237,9 @@ class UserCommand(_BaseCommand):
default_permission: :class:`bool` default_permission: :class:`bool`
Whether the command is enabled in guilds by default. Whether the command is enabled in guilds by default.
""" """
__slots__ = ('_user',)
def __init__(self, *, user: Optional[Snowflake] = None, **kwargs): def __init__(self, *, user: Optional[Snowflake] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._user = user self._user = user
@ -259,7 +289,7 @@ class UserCommand(_BaseCommand):
self._user = value self._user = value
class MessageCommand(_BaseCommand): class MessageCommand(BaseCommand):
"""Represents a message command. """Represents a message command.
Attributes Attributes
@ -275,6 +305,9 @@ class MessageCommand(_BaseCommand):
default_permission: :class:`bool` default_permission: :class:`bool`
Whether the command is enabled in guilds by default. Whether the command is enabled in guilds by default.
""" """
__slots__ = ('_message',)
def __init__(self, *, message: Optional[Message] = None, **kwargs): def __init__(self, *, message: Optional[Message] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._message = message self._message = message
@ -324,7 +357,7 @@ class MessageCommand(_BaseCommand):
self._message = value self._message = value
class SlashCommand(_SlashMixin, _BaseCommand): class SlashCommand(BaseCommand, SlashMixin):
"""Represents a slash command. """Represents a slash command.
Attributes Attributes
@ -346,13 +379,16 @@ class SlashCommand(_SlashMixin, _BaseCommand):
You can access (and use) subcommands directly as attributes of the class. You can access (and use) subcommands directly as attributes of the class.
""" """
__slots__ = ('_parent', 'options', 'children')
def __init__( def __init__(
self, *, data: Dict[str, Any], **kwargs self, *, data: Dict[str, Any], **kwargs
) -> None: ) -> None:
super().__init__(data=data, **kwargs) super().__init__(data=data, **kwargs)
self._parent = self
self._unwrap_options(data.get('options', [])) self._unwrap_options(data.get('options', []))
async def __call__(self, channel, /, **kwargs): async def __call__(self, channel: Optional[Messageable] = None, /, **kwargs):
r"""Use the slash command. r"""Use the slash command.
Parameters Parameters
@ -363,9 +399,14 @@ class SlashCommand(_SlashMixin, _BaseCommand):
\*\*kwargs: Any \*\*kwargs: Any
The options to use. These will be casted to the correct type. The options to use. These will be casted to the correct type.
If an option has choices, they are automatically converted from name to value for you. If an option has choices, they are automatically converted from name to value for you.
Raises
------
InvalidArgument
Attempted to use a group.
""" """
if self.is_group(): if self.is_group():
raise TypeError('Cannot use a group') raise InvalidArgument('Cannot use a group')
return await super().__call__(self._parse_kwargs(kwargs), channel) return await super().__call__(self._parse_kwargs(kwargs), channel)
@ -388,7 +429,7 @@ class SlashCommand(_SlashMixin, _BaseCommand):
return bool(self.children) return bool(self.children)
class SubCommand(_SlashMixin, ApplicationCommand): class SubCommand(SlashMixin):
"""Represents a slash command child. """Represents a slash command child.
This could be a subcommand, or a subgroup. This could be a subcommand, or a subgroup.
@ -405,8 +446,20 @@ class SubCommand(_SlashMixin, ApplicationCommand):
The type of application command. Always :class:`CommandType.chat_input`. The type of application command. Always :class:`CommandType.chat_input`.
""" """
__slots__ = (
'_parent',
'_state',
'_type',
'parent',
'options',
'children',
'type',
)
def __init__(self, *, parent, data): def __init__(self, *, parent, data):
super().__init__(data) self.name = data['name']
self.description = data.get('description')
self._state = parent._state
self.parent: Union[SlashCommand, SubCommand] = parent self.parent: Union[SlashCommand, SubCommand] = parent
self._parent: SlashCommand = getattr(parent, 'parent', parent) # type: ignore self._parent: SlashCommand = getattr(parent, 'parent', parent) # type: ignore
self.type = CommandType.chat_input # Avoid confusion I guess self.type = CommandType.chat_input # Avoid confusion I guess
@ -433,9 +486,14 @@ class SubCommand(_SlashMixin, ApplicationCommand):
\*\*kwargs: Any \*\*kwargs: Any
The options to use. These will be casted to the correct type. The options to use. These will be casted to the correct type.
If an option has choices, they are automatically converted from name to value for you. If an option has choices, they are automatically converted from name to value for you.
Raises
------
InvalidArgument
Attempted to use a group.
""" """
if self.is_group(): if self.is_group():
raise TypeError('Cannot use a group') raise InvalidArgument('Cannot use a group')
options = [{ options = [{
'type': self._type.value, 'type': self._type.value,
@ -489,7 +547,7 @@ class SubCommand(_SlashMixin, ApplicationCommand):
return self._parent.application return self._parent.application
@property @property
def target_channel(self): def target_channel(self) -> Optional[Messageable]:
"""Optional[:class:`abc.Messageable`]: The channel this command will be used on. """Optional[:class:`abc.Messageable`]: The channel this command will be used on.
You can set this in order to use this command on a different channel without re-fetching it. You can set this in order to use this command on a different channel without re-fetching it.
@ -526,6 +584,19 @@ class Option:
autocomplete: :class:`bool` autocomplete: :class:`bool`
Whether the option autocompletes. Always ``False`` if :attr:`choices` are present. Whether the option autocompletes. Always ``False`` if :attr:`choices` are present.
""" """
__slots__ = (
'name',
'description',
'type',
'required',
'min_value',
'max_value',
'choices',
'channel_types',
'autocomplete',
)
def __init__(self, data): def __init__(self, data):
self.name: str = data['name'] self.name: str = data['name']
self.description: str = data['description'] self.description: str = data['description']
@ -557,6 +628,9 @@ class OptionChoice:
value: Any value: Any
The choice's value. The type of this depends on the option's type. The choice's value. The type of this depends on the option's type.
""" """
__slots__ = ('name', 'value')
def __init__(self, data: Dict[str, str], type: OptionType): def __init__(self, data: Dict[str, str], type: OptionType):
self.name: str = data['name'] self.name: str = data['name']
if type is OptionType.string: if type is OptionType.string:
@ -575,7 +649,7 @@ class OptionChoice:
return value return value
def _command_factory(command_type: int) -> Tuple[CommandType, _BaseCommand]: def _command_factory(command_type: int) -> Tuple[CommandType, BaseCommand]:
value = try_enum(CommandType, command_type) value = try_enum(CommandType, command_type)
if value is CommandType.chat_input: if value is CommandType.chat_input:
return value, SlashCommand return value, SlashCommand
@ -584,4 +658,4 @@ def _command_factory(command_type: int) -> Tuple[CommandType, _BaseCommand]:
elif value is CommandType.message: elif value is CommandType.message:
return value, MessageCommand return value, MessageCommand
else: else:
return value, _BaseCommand # IDK about this return value, BaseCommand # IDK about this
Loading…
Cancel
Save