diff --git a/discord/appinfo.py b/discord/appinfo.py index 534bd4ee5..e62d8c7b8 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -31,6 +31,7 @@ from .asset import Asset from .enums import ApplicationType, ApplicationVerificationState, RPCApplicationState, StoreApplicationState, try_enum from .flags import ApplicationFlags from .mixins import Hashable +from .object import Object from .permissions import Permissions from .user import User @@ -554,13 +555,8 @@ class InteractionApplication(Hashable): The bot attached to the application. description: Optional[:class:`str`] The application description. - Only available from :attr:`~Modal.application`. type: Optional[:class:`ApplicationType`] The type of application. - Only available from :attr:`~Modal.application`. - command_count: Optional[:class:`int`] - The number of commands the application has. - Only available from :attr:`~BaseCommand.application`. """ __slots__ = ( @@ -583,14 +579,16 @@ class InteractionApplication(Hashable): def _update(self, data: dict) -> None: self.id: int = int(data['id']) self.name: str = data['name'] - self.description: Optional[str] = data.get('description') + self.description: str = data.get('description') or '' self._icon: Optional[str] = data.get('icon') self.type: Optional[ApplicationType] = try_enum(ApplicationType, data['type']) if 'type' in data else None - self.bot: User = None # type: ignore # This should never be None but it's volatile + self.bot: User # User data should always be available, but these payloads are volatile user = data.get('bot') if user is not None: self.bot = User(state=self._state, data=user) + else: + self.bot = Object(id=self.id) # type: ignore def __repr__(self) -> str: return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>' diff --git a/discord/commands.py b/discord/commands.py index fa8fcc1ed..0f0355b3d 100644 --- a/discord/commands.py +++ b/discord/commands.py @@ -33,6 +33,7 @@ from .utils import _generate_nonce if TYPE_CHECKING: from .abc import Messageable, Snowflake + from .appinfo import InteractionApplication from .interactions import Interaction from .message import Message from .state import ConnectionState @@ -68,19 +69,27 @@ class ApplicationCommand(Protocol): The command's description, if any. type: :class:`.AppCommandType` The type of application command. + default_permission: :class:`bool` + Whether the command is enabled in guilds by default. + application: Optional[:class:`InteractionApplication`] + The application this command belongs to. + Only available if requested. + application_id: :class:`int` + The ID of the application this command belongs to. """ __slots__ = () if TYPE_CHECKING: _state: ConnectionState - _application_id: int + application_id: int name: str description: str version: int type: AppCommandType target_channel: Optional[Messageable] default_permission: bool + application: Optional[InteractionApplication] def __str__(self) -> str: return self.name @@ -97,7 +106,7 @@ class ApplicationCommand(Protocol): state._interaction_cache[nonce] = (type.value, data['name'], acc_channel) try: await state.http.interact( - type, data, acc_channel, form_data=True, nonce=nonce, application_id=self._application_id + type, data, acc_channel, form_data=True, nonce=nonce, application_id=self.application_id ) i = await state.client.wait_for( 'interaction_finish', @@ -138,14 +147,19 @@ class BaseCommand(ApplicationCommand, Hashable): The command's ID. version: :class:`int` The command's version. - default_permission: :class:`bool` - Whether the command is enabled in guilds by default. name: :class:`str` The command's name. description: :class:`str` The command's description, if any. type: :class:`AppCommandType` The type of application command. + default_permission: :class:`bool` + Whether the command is enabled in guilds by default. + application: Optional[:class:`InteractionApplication`] + The application this command belongs to. + Only available if requested. + application_id: :class:`int` + The ID of the application this command belongs to. """ __slots__ = ( @@ -155,27 +169,29 @@ class BaseCommand(ApplicationCommand, Hashable): 'version', 'type', 'default_permission', + 'application', + 'application_id', '_data', '_state', '_channel', - '_application_id', '_dm_permission', '_default_member_permissions', ) - def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None) -> None: + def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None, application: Optional[InteractionApplication] = None) -> None: self._state = state self._data = data self.name = data['name'] self.description = data['description'] 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.version = int(data['version']) self.type = try_enum(AppCommandType, data['type']) - self.default_permission: bool = data['default_permission'] + self.default_permission: bool = data.get('default_permission', True) self._dm_permission = data.get('dm_permission') self._default_member_permissions = data['default_member_permissions'] + self.application = application def __repr__(self) -> str: return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>' @@ -192,12 +208,6 @@ class BaseCommand(ApplicationCommand, Hashable): """ return False - @property - def application(self): - """The application this command belongs to.""" - ... - # return self._state.get_application(self._application_id) - @property def target_channel(self) -> Optional[Messageable]: """Optional[:class:`.abc.Messageable`]: The channel this application command will be used on. @@ -567,8 +577,9 @@ class SubCommand(SlashMixin): return BASE + '>' @property - def _application_id(self) -> int: - return self._parent._application_id + def application_id(self) -> int: + """:class:`int`: The ID of the application this command belongs to.""" + return self._parent.application_id @property def version(self) -> int: @@ -592,7 +603,9 @@ class SubCommand(SlashMixin): @property def application(self): - """The application this command belongs to.""" + """Optional[:class:`InteractionApplication`]: The application this command belongs to. + Only available if requested. + """ return self._parent.application @property @@ -608,7 +621,7 @@ class SubCommand(SlashMixin): self._parent.target_channel = value -class Option: # TODO: Add validation +class Option: """Represents a command option. .. container:: operations @@ -633,12 +646,17 @@ class Option: # TODO: Add validation Maximum value of the option. Only applicable to :attr:`AppCommandOptionType.integer` and :attr:`AppCommandOptionType.number`. choices: List[:class:`OptionChoice`] A list of possible choices to choose from. If these are present, you must choose one from them. + Only applicable to :attr:`AppCommandOptionType.string`, :attr:`AppCommandOptionType.integer`, and :attr:`AppCommandOptionType.number`. channel_types: List[:class:`ChannelType`] A list of channel types that you can choose from. If these are present, you must choose a channel that is one of these types. + Only applicable to :attr:`AppCommandOptionType.channel`. autocomplete: :class:`bool` - Whether the option autocompletes. Always ``False`` if :attr:`choices` are present. + Whether the option autocompletes. + + Only applicable to :attr:`AppCommandOptionType.string`, :attr:`AppCommandOptionType.integer`, and :attr:`AppCommandOptionType.number`. + Always ``False`` if :attr:`choices` are present. """ __slots__ = ( diff --git a/discord/iterators.py b/discord/iterators.py index 99da7a461..13ccdab30 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -27,8 +27,9 @@ from __future__ import annotations import asyncio from typing import Awaitable, TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, Tuple, AsyncIterator, Dict +from .appinfo import InteractionApplication from .errors import InvalidData -from .utils import _generate_nonce +from .utils import _generate_nonce, _get_as_snowflake from .object import Object from .commands import _command_factory from .enums import AppCommandType @@ -97,6 +98,7 @@ class CommandIterator: self.applications: bool = kwargs.get('applications', True) self.application: Snowflake = kwargs.get('application', None) self.commands = asyncio.Queue() + self._application_cache: Dict[int, InteractionApplication] = {} async def _process_args(self) -> Tuple[DMChannel, Optional[str], Optional[Union[User, Message]]]: item = self.item @@ -189,19 +191,20 @@ class CommandIterator: kwargs['offset'] += retrieve + for app in data.get('applications', []): + self._application_cache[int(app['id'])] = InteractionApplication(state=state, data=app) + for cmd in cmds: self.commands.put_nowait(self.create_command(cmd)) - for app in data.get('applications', []): - ... - def create_command(self, data) -> ApplicationCommand: channel, item, value = self._tuple # type: ignore if item is not None: kwargs = {item: value} else: kwargs = {} - return self.cls(state=channel._state, data=data, channel=channel, **kwargs) + app_id = _get_as_snowflake(data, 'application_id') + return self.cls(state=channel._state, data=data, channel=channel, application=self._application_cache.get(app_id), **kwargs) # type: ignore class FakeCommandIterator: