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 asyncio import TimeoutError
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 .errors import InvalidData
from .errors import InvalidData, InvalidArgument
from .utils import time_snowflake
if TYPE_CHECKING:
@ -39,29 +38,37 @@ if TYPE_CHECKING:
from .state import ConnectionState
class ApplicationCommand:
def __init__(
self, data: Dict[str, Any]
) -> None:
self.name: str = data['name']
self.description: str = data['description']
@runtime_checkable
class ApplicationCommand(Protocol):
__slots__ = ()
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:
channel = channel or self.target_channel # type: ignore
channel = channel or self.target_channel
if channel is None:
raise TypeError('__call__() missing 1 required keyword-only argument: \'channel\'')
state = self._state # type: ignore
state = self._state
channel = await channel._get_channel()
payload = {
'application_id': str(self._application_id), # type: ignore
'application_id': str(self._application_id),
'channel_id': str(channel.id),
'data': data,
'nonce': str(time_snowflake(datetime.utcnow())),
'type': 2, # Should be an enum but eh
}
if getattr(channel, 'guild', None):
payload['guild_id'] = str(channel.guild.id) # type: ignore
if getattr(channel, 'guild', None) is not None:
payload['guild_id'] = str(channel.guild.id)
state._interactions[payload['nonce']] = 2
await state.http.interact(payload, form_data=True)
@ -76,17 +83,33 @@ class ApplicationCommand:
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__(
self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None
) -> None:
super().__init__(data)
self.name = data['name']
self.description = data['description']
self._state = state
self._channel = channel
self._application_id: int = int(data['application_id'])
self.id: int = int(data['id'])
self.version: int = int(data['version'])
self.type: CommandType = try_enum(CommandType, data['type'])
self.version = int(data['version'])
self.type = try_enum(CommandType, data['type'])
self.default_permission: bool = data['default_permission']
self._dm_permission = data['dm_permission']
self._default_member_permissions = data['default_member_permissions']
@ -128,19 +151,23 @@ class _BaseCommand(ApplicationCommand):
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):
# This will always be used in a context where all these attributes are set
obj = getattr(self, '_parent', self)
obj = self._parent
data = {
'attachments': [],
'id': str(obj.id), # type: ignore
'name': obj.name, # type: ignore
'id': str(obj.id),
'name': obj.name,
'options': options,
'type': obj.type.value, # type: ignore
'version': str(obj.version), # type: ignore
'type': obj.type.value,
'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]]:
possible_options = {o.name: o for o in self.options}
@ -190,11 +217,11 @@ class _SlashMixin:
for child in children:
setattr(self, child.name, child)
self.options: List[Option] = options
self.children: List[SubCommand] = children
self.options = options
self.children = children
class UserCommand(_BaseCommand):
class UserCommand(BaseCommand):
"""Represents a user command.
Attributes
@ -210,6 +237,9 @@ class UserCommand(_BaseCommand):
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
"""
__slots__ = ('_user',)
def __init__(self, *, user: Optional[Snowflake] = None, **kwargs):
super().__init__(**kwargs)
self._user = user
@ -259,7 +289,7 @@ class UserCommand(_BaseCommand):
self._user = value
class MessageCommand(_BaseCommand):
class MessageCommand(BaseCommand):
"""Represents a message command.
Attributes
@ -275,6 +305,9 @@ class MessageCommand(_BaseCommand):
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
"""
__slots__ = ('_message',)
def __init__(self, *, message: Optional[Message] = None, **kwargs):
super().__init__(**kwargs)
self._message = message
@ -324,7 +357,7 @@ class MessageCommand(_BaseCommand):
self._message = value
class SlashCommand(_SlashMixin, _BaseCommand):
class SlashCommand(BaseCommand, SlashMixin):
"""Represents a slash command.
Attributes
@ -346,13 +379,16 @@ class SlashCommand(_SlashMixin, _BaseCommand):
You can access (and use) subcommands directly as attributes of the class.
"""
__slots__ = ('_parent', 'options', 'children')
def __init__(
self, *, data: Dict[str, Any], **kwargs
) -> None:
super().__init__(data=data, **kwargs)
self._parent = self
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.
Parameters
@ -363,9 +399,14 @@ class SlashCommand(_SlashMixin, _BaseCommand):
\*\*kwargs: Any
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.
Raises
------
InvalidArgument
Attempted to use a 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)
@ -388,7 +429,7 @@ class SlashCommand(_SlashMixin, _BaseCommand):
return bool(self.children)
class SubCommand(_SlashMixin, ApplicationCommand):
class SubCommand(SlashMixin):
"""Represents a slash command child.
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`.
"""
__slots__ = (
'_parent',
'_state',
'_type',
'parent',
'options',
'children',
'type',
)
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: SlashCommand = getattr(parent, 'parent', parent) # type: ignore
self.type = CommandType.chat_input # Avoid confusion I guess
@ -433,9 +486,14 @@ class SubCommand(_SlashMixin, ApplicationCommand):
\*\*kwargs: Any
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.
Raises
------
InvalidArgument
Attempted to use a group.
"""
if self.is_group():
raise TypeError('Cannot use a group')
raise InvalidArgument('Cannot use a group')
options = [{
'type': self._type.value,
@ -489,7 +547,7 @@ class SubCommand(_SlashMixin, ApplicationCommand):
return self._parent.application
@property
def target_channel(self):
def target_channel(self) -> Optional[Messageable]:
"""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.
@ -526,6 +584,19 @@ class Option:
autocomplete: :class:`bool`
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):
self.name: str = data['name']
self.description: str = data['description']
@ -557,6 +628,9 @@ class OptionChoice:
value: Any
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):
self.name: str = data['name']
if type is OptionType.string:
@ -575,7 +649,7 @@ class OptionChoice:
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)
if value is CommandType.chat_input:
return value, SlashCommand
@ -584,4 +658,4 @@ def _command_factory(command_type: int) -> Tuple[CommandType, _BaseCommand]:
elif value is CommandType.message:
return value, MessageCommand
else:
return value, _BaseCommand # IDK about this
return value, BaseCommand # IDK about this
Loading…
Cancel
Save