diff --git a/discord/abc.py b/discord/abc.py index 9b271014c..078734825 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -48,7 +48,6 @@ from .object import OLDEST_OBJECT, Object from .context_managers import Typing from .enums import AppCommandType, ChannelType from .errors import ClientException -from .iterators import CommandIterator from .mentions import AllowedMentions from .permissions import PermissionOverwrite, Permissions from .role import Role @@ -58,7 +57,7 @@ from .http import handle_message_parameters from .voice_client import VoiceClient, VoiceProtocol from .sticker import GuildSticker, StickerItem from .settings import ChannelSettings -from .commands import ApplicationCommand +from .commands import ApplicationCommand, SlashCommand, UserCommand from . import utils __all__ = ( @@ -1728,21 +1727,16 @@ class Messageable: for raw_message in data: yield self._state.create_message(channel=channel, data=raw_message) - def slash_commands( + async def slash_commands( self, query: Optional[str] = None, *, limit: Optional[int] = None, command_ids: Optional[List[int]] = None, - applications: bool = True, application: Optional[Snowflake] = None, - ): - """Returns an iterator that allows you to see what slash commands are available to use. - - .. note:: - If this is a DM context, all parameters here are faked, as the only way to get commands is to fetch them all at once. - Because of this, all except ``query``, ``limit``, and ``command_ids`` are ignored. - It is recommended to not pass any parameters in that case. + include_applications: bool = MISSING, + ) -> AsyncIterator[SlashCommand]: + """Returns a :term:`asynchronous iterator` of the slash commands available in the channel. Examples --------- @@ -1754,7 +1748,7 @@ class Messageable: Flattening into a list :: - commands = await channel.slash_commands().flatten() + commands = [command async for command in channel.slash_commands()] # commands is now a list of SlashCommand... All parameters are optional. @@ -1763,42 +1757,229 @@ class Messageable: ---------- query: Optional[:class:`str`] The query to search for. + + This parameter is faked if ``application`` is specified. limit: Optional[:class:`int`] - The maximum number of commands to send back. - cache: :class:`bool` - Whether to cache the commands internally. + The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. + + This parameter is faked if ``application`` is specified. command_ids: Optional[List[:class:`int`]] - List of command IDs to search for. If the command doesn't exist it won't be returned. - applications: :class:`bool` - Whether to include applications in the response. This defaults to ``False``. + List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. application: Optional[:class:`~discord.abc.Snowflake`] - Query commands only for this application. + Return this application's commands. Always set to DM recipient in a private channel context. + include_applications: :class:`bool` + Whether to include applications in the response. This defaults to ``True`` if possible. + + Cannot be set to ``True`` if ``application`` is specified. Raises ------ TypeError - The user is not a bot. - Both query and command_ids were passed. + Both query and command_ids are passed. + Both application and include_applications are passed. + Attempted to fetch commands in a DM with a non-bot user. ValueError - The limit was not > 0. + The limit was not greater than or equal to 0. HTTPException Getting the commands failed. + ~discord.Forbidden + You do not have permissions to get the commands. + ~discord.HTTPException + The request to get the commands failed. Yields ------- :class:`.SlashCommand` A slash command. """ - iterator = CommandIterator( - self, - AppCommandType.chat_input, - query, - limit, - command_ids, - applications=applications, - application=application, + if limit is not None and limit < 0: + raise ValueError('limit must be greater than or equal to 0') + if query and command_ids: + raise TypeError('Cannot specify both query and command_ids') + + state = self._state + endpoint = state.http.search_application_commands + channel = await self._get_channel() + + application_id = application.id if application else None + if channel.type == ChannelType.private: + recipient: User = channel.recipient # type: ignore + if not recipient.bot: + raise TypeError('Cannot fetch commands in a DM with a non-bot user') + application_id = recipient.id + elif channel.type == ChannelType.group: + return + + if application_id and include_applications: + raise TypeError('Cannot specify both application and include_applications') + include_applications = ( + (False if application_id else True) if include_applications is MISSING else include_applications ) - return iterator.iterate() + + while True: + cursor = MISSING + retrieve = min(25 if limit is None else limit, 25) + if retrieve < 1 or cursor is None: + return + + data = await endpoint( + channel.id, + AppCommandType.chat_input.value, + limit=retrieve if not application_id else None, + query=query if not command_ids and not application_id else None, + command_ids=command_ids if not application_id else None, # type: ignore + application_id=application_id, + include_applications=include_applications if not application_id else None, + cursor=cursor if cursor is not MISSING else None, + ) + cursor = data['cursor'].get('next') + cmds = data['application_commands'] + apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []} + + if len(cmds) < 25: + limit = 0 + + for cmd in cmds: + # Handle faked parameters + if command_ids and int(cmd['id']) not in command_ids: + continue + elif application_id and query and query.lower() not in cmd['name']: + continue + elif application_id and limit == 0: + return + + if limit is not None: + limit -= 1 + + cmd['application'] = apps.get(int(cmd['application_id'])) + yield SlashCommand(state=state, data=cmd, channel=channel) + + async def user_commands( + self, + query: Optional[str] = None, + *, + limit: Optional[int] = None, + command_ids: Optional[List[int]] = None, + application: Optional[Snowflake] = None, + include_applications: bool = MISSING, + ) -> AsyncIterator[UserCommand]: + """Returns a :term:`asynchronous iterator` of the user commands available to use on the user. + + Examples + --------- + + Usage :: + + async for command in user.user_commands(): + print(command.name) + + Flattening into a list :: + + commands = [command async for command in user.user_commands()] + # commands is now a list of UserCommand... + + All parameters are optional. + + Parameters + ---------- + query: Optional[:class:`str`] + The query to search for. + + This parameter is faked if ``application`` is specified. + limit: Optional[:class:`int`] + The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. + + This parameter is faked if ``application`` is specified. + command_ids: Optional[List[:class:`int`]] + List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. + application: Optional[:class:`~discord.abc.Snowflake`] + Return this application's commands. Always set to DM recipient in a private channel context. + include_applications: :class:`bool` + Whether to include applications in the response. This defaults to ``True`` if possible. + + Cannot be set to ``True`` if ``application`` is specified. + + Raises + ------ + TypeError + Both query and command_ids are passed. + Both application and include_applications are passed. + Attempted to fetch commands in a DM with a non-bot user. + ValueError + The limit was not greater than or equal to 0. + HTTPException + Getting the commands failed. + ~discord.Forbidden + You do not have permissions to get the commands. + ~discord.HTTPException + The request to get the commands failed. + + Yields + ------- + :class:`.UserCommand` + A user command. + """ + if limit is not None and limit < 0: + raise ValueError('limit must be greater than or equal to 0') + if query and command_ids: + raise TypeError('Cannot specify both query and command_ids') + + state = self._state + endpoint = state.http.search_application_commands + channel = await self._get_channel() + + application_id = application.id if application else None + if channel.type == ChannelType.private: + recipient: User = channel.recipient # type: ignore + if not recipient.bot: + raise TypeError('Cannot fetch commands in a DM with a non-bot user') + application_id = recipient.id + elif channel.type == ChannelType.group: + return + + if application_id and include_applications: + raise TypeError('Cannot specify both application and include_applications') + include_applications = ( + (False if application_id else True) if include_applications is MISSING else include_applications + ) + + while True: + cursor = MISSING + retrieve = min(25 if limit is None else limit, 25) + if retrieve < 1 or cursor is None: + return + + data = await endpoint( + channel.id, + AppCommandType.user.value, + limit=retrieve if not application_id else None, + query=query if not command_ids and not application_id else None, + command_ids=command_ids if not application_id else None, # type: ignore + application_id=application_id, + include_applications=include_applications if not application_id else None, + cursor=cursor if cursor is not MISSING else None, + ) + cursor = data['cursor'].get('next') + cmds = data['application_commands'] + apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []} + + if len(cmds) < 25: + limit = 0 + + for cmd in cmds: + # Handle faked parameters + if command_ids and int(cmd['id']) not in command_ids: + continue + elif application_id and query and query.lower() not in cmd['name'].lower(): + continue + elif application_id and limit == 0: + return + + if limit is not None: + limit -= 1 + + cmd['application'] = apps.get(int(cmd['application_id'])) + yield UserCommand(state=state, data=cmd, channel=channel, user=getattr(channel, 'recipient', None)) class Connectable(Protocol): diff --git a/discord/appinfo.py b/discord/appinfo.py index e62d8c7b8..ee28e3af1 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -468,9 +468,10 @@ class Application(PartialApplication): if flags is not MISSING: payload['flags'] = flags.value - data = await self._state.http.edit_application(self.id, payload) if team is not MISSING: - data = await self._state.http.transfer_application(self.id, team.id) + await self._state.http.transfer_application(self.id, team.id) + + data = await self._state.http.edit_application(self.id, payload) self._update(data) @@ -586,7 +587,7 @@ class InteractionApplication(Hashable): 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) + self.bot = self._state.create_user(user) else: self.bot = Object(id=self.id) # type: ignore diff --git a/discord/commands.py b/discord/commands.py index 215e96e11..3c61e4262 100644 --- a/discord/commands.py +++ b/discord/commands.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import Any, Dict, List, Optional, Protocol, Tuple, Type, runtime_checkable, Tuple, TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Tuple, Type, Union, runtime_checkable from .enums import AppCommandOptionType, AppCommandType, ChannelType, InteractionType, try_enum from .errors import InvalidData @@ -100,7 +100,9 @@ class ApplicationCommand(Protocol): def __str__(self) -> str: return self.name - async def __call__(self, data: dict, files: Optional[List[File]] = None, channel: Optional[Messageable] = None) -> Interaction: + async def __call__( + self, data: dict, files: Optional[List[File]] = None, channel: Optional[Messageable] = None + ) -> Interaction: channel = channel or self.target_channel if channel is None: raise TypeError('__call__() missing 1 required argument: \'channel\'') @@ -111,9 +113,7 @@ class ApplicationCommand(Protocol): state._interaction_cache[nonce] = (type.value, data['name'], acc_channel) try: - await state.http.interact( - type, data, acc_channel, files=files, nonce=nonce, application_id=self.application_id - ) + await state.http.interact(type, data, acc_channel, files=files, nonce=nonce, application_id=self.application_id) i = await state.client.wait_for( 'interaction_finish', check=lambda d: d.nonce == nonce, @@ -222,7 +222,7 @@ class BaseCommand(ApplicationCommand, Hashable): '_default_member_permissions', ) - def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None, application: Optional[InteractionApplication] = None) -> None: + def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None) -> None: self._state = state self._data = data self.name = data['name'] @@ -232,7 +232,9 @@ class BaseCommand(ApplicationCommand, Hashable): self.id: int = int(data['id']) self.version = int(data['version']) self.type = try_enum(AppCommandType, data['type']) - self.application = application + + application = data.get('application') + self.application = state.create_interaction_application(application) if application else None self._default_member_permissions = _get_as_snowflake(data, 'default_member_permissions') self.default_permission: bool = data.get('default_permission', True) @@ -249,7 +251,13 @@ class SlashMixin(ApplicationCommand, Protocol): options: List[Option] children: List[SubCommand] - async def __call__(self, options: List[dict], files: Optional[List[File]], attachments: List[Attachment], channel: Optional[Messageable] = None) -> Interaction: + async def __call__( + self, + options: List[dict], + files: Optional[List[File]], + attachments: List[Attachment], + channel: Optional[Messageable] = None, + ) -> Interaction: obj = self._parent command = obj._data command['name_localized'] = command['name'] diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index d5269ee84..c520dcc1e 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Generic, List, Optional, TypeVar, Union import discord.abc import discord.utils @@ -36,6 +36,7 @@ if TYPE_CHECKING: from typing_extensions import ParamSpec from discord.abc import MessageableChannel + from discord.commands import MessageCommand from discord.guild import Guild from discord.member import Member from discord.state import ConnectionState @@ -411,16 +412,16 @@ class Context(discord.abc.Messageable, Generic[BotT]): async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: return await self.message.reply(content, **kwargs) - @discord.utils.copy_doc(Message.message_commands) - def message_commands( + async def message_commands( self, query: Optional[str] = None, *, limit: Optional[int] = None, command_ids: Optional[List[int]] = None, - applications: bool = True, application: Optional[discord.abc.Snowflake] = None, - ): - return self.message.message_commands( - query, limit=limit, command_ids=command_ids, applications=applications, application=application - ) + include_applications: bool = True, + ) -> AsyncIterator[MessageCommand]: + async for command in self.message.message_commands( + query, limit=limit, command_ids=command_ids, include_applications=include_applications, application=application + ): + yield command diff --git a/discord/http.py b/discord/http.py index 5078940fd..4030dfc9e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2362,8 +2362,40 @@ class HTTPClient: return self.request(Route('POST', '/report'), json=payload) - def get_application_commands(self, id): - return self.request(Route('GET', '/applications/{user_id}/commands', user_id=id)) + def get_application_commands(self, app_id): + return self.request(Route('GET', '/applications/{application_id}/commands', application_id=app_id)) + + def search_application_commands( + self, + channel_id: Snowflake, + type: int, + *, + limit: Optional[int] = None, + query: Optional[str] = None, + cursor: Optional[str] = None, + command_ids: Optional[List[Snowflake]] = None, + application_id: Optional[Snowflake] = None, + include_applications: Optional[bool] = None, + ): + params: Dict[str, Any] = { + 'type': type, + } + if include_applications is not None: + params['include_applications'] = str(include_applications).lower() + if limit is not None: + params['limit'] = limit + if query: + params['query'] = query + if cursor: + params['cursor'] = cursor + if command_ids: + params['command_ids'] = command_ids + if application_id: + params['application_id'] = application_id + + return self.request( + Route('GET', '/channels/{channel_id}/application-commands/search', channel_id=channel_id), params=params + ) def interact( self, @@ -2407,5 +2439,6 @@ class HTTPClient: 'content_type': 'application/octet-stream', } ) + payload = None return self.request(Route('POST', '/interactions'), json=payload, form=form, files=files) diff --git a/discord/iterators.py b/discord/iterators.py deleted file mode 100644 index 13ccdab30..000000000 --- a/discord/iterators.py +++ /dev/null @@ -1,306 +0,0 @@ -""" -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 - -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, _get_as_snowflake -from .object import Object -from .commands import _command_factory -from .enums import AppCommandType - -__all__ = ( - 'CommandIterator', - 'FakeCommandIterator', -) - -if TYPE_CHECKING: - from .user import User - from .message import Message - from .abc import Snowflake, Messageable - from .commands import ApplicationCommand - from .channel import DMChannel - -T = TypeVar('T') -OT = TypeVar('OT') -_Func = Callable[[T], Union[OT, Awaitable[OT]]] - -OLDEST_OBJECT = Object(id=0) - - -def _is_fake(item: Union[Messageable, Message]) -> bool: # I hate this too, but and performance exist - try: - item.guild # type: ignore - except AttributeError: - return True - try: - item.channel.me # type: ignore - except AttributeError: - return False - return True - - -class CommandIterator: - def __new__(cls, *args, **kwargs) -> Union[CommandIterator, FakeCommandIterator]: - if _is_fake(args[0]): - return FakeCommandIterator(*args) - else: - return super().__new__(cls) - - def __init__( - self, - item: Union[Messageable, Message], - type: AppCommandType, - query: Optional[str] = None, - limit: Optional[int] = None, - command_ids: Optional[List[int]] = None, - **kwargs, - ) -> None: - if query and command_ids: - raise TypeError('Cannot specify both query and command_ids') - if limit is not None and limit <= 0: - raise ValueError('limit must be > 0') - - self.item = item - self.channel = None - self.state = item._state - self._tuple = None - self.type = type - _, self.cls = _command_factory(int(type)) - self.query = query - self.limit = limit - self.command_ids = command_ids - 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 - if self.type is AppCommandType.user: - channel = await item._get_channel() # type: ignore - if getattr(item, 'bot', None): - item = item - else: - item = None - text = 'user' - elif self.type is AppCommandType.message: - message = self.item - channel = message.channel # type: ignore - text = 'message' - elif self.type is AppCommandType.chat_input: - channel = await item._get_channel() # type: ignore - item = None - text = None - self._process_kwargs(channel) # type: ignore - return channel, text, item # type: ignore - - def _process_kwargs(self, channel) -> None: - kwargs = { - 'guild_id': channel.guild.id, - 'type': self.type.value, - 'offset': 0, - } - if self.applications: - kwargs['applications'] = True # Only sent if it's True... - if app := self.application: - kwargs['application'] = app.id - if (query := self.query) is not None: - kwargs['query'] = query - if cmds := self.command_ids: - kwargs['command_ids'] = cmds - self.kwargs = kwargs - - async def iterate(self) -> AsyncIterator[ApplicationCommand]: - while True: - if self.commands.empty(): - await self.fill_commands() - - try: - yield self.commands.get_nowait() - except asyncio.QueueEmpty: - break - - def _get_retrieve(self): - l = self.limit - if l is None or l > 100: - r = 100 - else: - r = l - self.retrieve = r - return r > 0 - - async def fill_commands(self) -> None: - if not self._tuple: # Do the required setup - self._tuple = await self._process_args() - - if not self._get_retrieve(): - return - - state = self.state - kwargs = self.kwargs - retrieve = self.retrieve - nonce = _generate_nonce() - - def predicate(d): - return d.get('nonce') == nonce - - data = None - for _ in range(3): - await state.ws.request_commands(**kwargs, limit=retrieve, nonce=nonce) - try: - data: Optional[Dict[str, Any]] = await asyncio.wait_for( - state.ws.wait_for('guild_application_commands_update', predicate), timeout=3 - ) - except asyncio.TimeoutError: - pass - - if data is None: - raise InvalidData('Didn\'t receive a response from Discord') - - cmds = data['application_commands'] - if len(cmds) < retrieve: - self.limit = 0 - elif self.limit is not None: - self.limit -= retrieve - - 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)) - - def create_command(self, data) -> ApplicationCommand: - channel, item, value = self._tuple # type: ignore - if item is not None: - kwargs = {item: value} - else: - 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: - def __init__( - self, - item: Union[User, Message, DMChannel], - type: AppCommandType, - query: Optional[str] = None, - limit: Optional[int] = None, - command_ids: Optional[List[int]] = None, - ) -> None: - if query and command_ids: - raise TypeError('Cannot specify both query and command_ids') - if limit is not None and limit <= 0: - raise ValueError('limit must be > 0') - - self.item = item - self.channel = None - self._tuple = None - self.type = type - _, self.cls = _command_factory(int(type)) - self.query = query - self.limit = limit - self.command_ids = command_ids - self.has_more = False - self.commands = asyncio.Queue() - - async def _process_args(self) -> Tuple[DMChannel, Optional[str], Optional[Union[User, Message]]]: - item = self.item - if self.type is AppCommandType.user: - channel = await item._get_channel() # type: ignore - if getattr(item, 'bot', None): - item = item - else: - item = None - text = 'user' - elif self.type is AppCommandType.message: - message = self.item - channel = message.channel # type: ignore - text = 'message' - elif self.type is AppCommandType.chat_input: - channel = await item._get_channel() # type: ignore - item = None - text = None - if not channel.recipient.bot: # type: ignore # Type checker cannot understand this - raise TypeError('User is not a bot') - return channel, text, item # type: ignore - - async def iterate(self) -> AsyncIterator[ApplicationCommand]: - while True: - if self.commands.empty(): - await self.fill_commands() - - try: - yield self.commands.get_nowait() - except asyncio.QueueEmpty: - break - - async def fill_commands(self) -> None: - if self.has_more: - return - - if not (stuff := self._tuple): - self._tuple = channel, _, _ = await self._process_args() - else: - channel = stuff[0] - - limit = self.limit or -1 - data = await channel._state.http.get_application_commands(channel.recipient.id) - ids = self.command_ids - query = self.query and self.query.lower() - type = self.type.value - - for cmd in data: - if cmd['type'] != type: - continue - - if ids: - if not int(cmd['id']) in ids: - continue - - if query: - if not query in cmd['name'].lower(): - continue - - self.commands.put_nowait(self.create_command(cmd)) - limit -= 1 - if limit == 0: - break - - self.has_more = True - - 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) diff --git a/discord/member.py b/discord/member.py index b9f167433..730cebe02 100644 --- a/discord/member.py +++ b/discord/member.py @@ -38,11 +38,10 @@ from .utils import MISSING from .user import BaseUser, User, _UserTag from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import AppCommandType, RelationshipAction, Status, try_enum +from .enums import RelationshipAction, Status, try_enum from .errors import ClientException from .colour import Colour from .object import Object -from .iterators import CommandIterator __all__ = ( 'VoiceState', @@ -1108,68 +1107,3 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): Sending the friend request failed. """ await self._state.http.add_relationship(self._user.id, action=RelationshipAction.send_friend_request) - - def user_commands( - self, - query: Optional[str] = None, - *, - limit: Optional[int] = None, - command_ids: Optional[List[int]] = None, - applications: bool = True, - application: Optional[Snowflake] = None, - ): - """Returns an iterator that allows you to see what user commands are available to use. - - Examples - --------- - - Usage :: - - async for command in member.user_commands(): - print(command.name) - - Flattening into a list :: - - commands = await member.user_commands().flatten() - # commands is now a list of UserCommand... - - All parameters are optional. - - Parameters - ---------- - query: Optional[:class:`str`] - The query to search for. - limit: Optional[:class:`int`] - The maximum number of commands to send back. - cache: :class:`bool` - Whether to cache the commands internally. - command_ids: Optional[List[:class:`int`]] - List of command IDs to search for. If the command doesn't exist it won't be returned. - applications: :class:`bool` - Whether to include applications in the response. This defaults to ``False``. - application: Optional[:class:`~abc.Snowflake`] - Query commands only for this application. - - Raises - ------ - TypeError - The limit was not > 0. - Both query and command_ids were passed. - HTTPException - Getting the commands failed. - - Yields - ------- - :class:`.UserCommand` - A user command. - """ - iterator = CommandIterator( - self, - AppCommandType.user, - query, - limit, - command_ids, - applications=applications, - application=application, - ) - return iterator.iterate() diff --git a/discord/message.py b/discord/message.py index 397738354..4b1286c83 100644 --- a/discord/message.py +++ b/discord/message.py @@ -30,6 +30,7 @@ import re import io from os import PathLike from typing import ( + AsyncIterator, Dict, TYPE_CHECKING, Sequence, @@ -63,8 +64,9 @@ from .mixins import Hashable from .sticker import StickerItem from .threads import Thread from .channel import PartialMessageable -from .iterators import CommandIterator from .interactions import Interaction +from .commands import MessageCommand + if TYPE_CHECKING: from typing_extensions import Self @@ -1978,21 +1980,16 @@ class Message(PartialMessage, Hashable): """ return await self.edit(attachments=[a for a in self.attachments if a not in attachments]) - def message_commands( + async def message_commands( self, query: Optional[str] = None, *, limit: Optional[int] = None, command_ids: Optional[List[int]] = None, - applications: bool = True, application: Optional[Snowflake] = None, - ): - """Returns an iterator that allows you to see what message commands are available to use. - - .. note:: - If this is a DM context, all parameters here are faked, as the only way to get commands is to fetch them all at once. - Because of this, all except ``query``, ``limit``, and ``command_ids`` are ignored. - It is recommended to not pass any parameters in that case. + include_applications: bool = MISSING, + ) -> AsyncIterator[MessageCommand]: + """Returns a :term:`asynchronous iterator` of the message commands available to use on the message. Examples --------- @@ -2004,8 +2001,8 @@ class Message(PartialMessage, Hashable): Flattening into a list :: - commands = await message.message_commands().flatten() - # commands is now a list of SlashCommand... + commands = [command async for command in message.message_commands()] + # commands is now a list of MessageCommand... All parameters are optional. @@ -2013,37 +2010,99 @@ class Message(PartialMessage, Hashable): ---------- query: Optional[:class:`str`] The query to search for. + + This parameter is faked if ``application`` is specified. limit: Optional[:class:`int`] - The maximum number of commands to send back. + The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. + + This parameter is faked if ``application`` is specified. command_ids: Optional[List[:class:`int`]] - List of command IDs to search for. If the command doesn't exist it won't be returned. - applications: :class:`bool` - Whether to include applications in the response. This defaults to ``False``. + List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. application: Optional[:class:`~discord.abc.Snowflake`] - Query commands only for this application. + Return this application's commands. Always set to DM recipient in a private channel context. + include_applications: :class:`bool` + Whether to include applications in the response. This defaults to ``True`` if possible. + + Cannot be set to ``True`` if ``application`` is specified. Raises ------ TypeError - The user is not a bot. - Both query and command_ids were passed. + Both query and command_ids are passed. + Both application and include_applications are passed. + Attempted to fetch commands in a DM with a non-bot user. ValueError - The limit was not > 0. + The limit was not greater than or equal to 0. HTTPException Getting the commands failed. + ~discord.Forbidden + You do not have permissions to get the commands. + ~discord.HTTPException + The request to get the commands failed. Yields ------- :class:`.MessageCommand` A message command. """ - iterator = CommandIterator( - self, - AppCommandType.message, - query, - limit, - command_ids, - applications=applications, - application=application, + if limit is not None and limit < 0: + raise ValueError('limit must be greater than or equal to 0') + if query and command_ids: + raise TypeError('Cannot specify both query and command_ids') + + state = self._state + endpoint = state.http.search_application_commands + channel = self.channel + + application_id = application.id if application else None + if channel.type == ChannelType.private: + recipient: User = channel.recipient # type: ignore + if not recipient.bot: + raise TypeError('Cannot fetch commands in a DM with a non-bot user') + application_id = recipient.id + elif channel.type == ChannelType.group: + return + + if application_id and include_applications: + raise TypeError('Cannot specify both application and include_applications') + include_applications = ( + (False if application_id else True) if include_applications is MISSING else include_applications ) - return iterator.iterate() + + while True: + cursor = MISSING + retrieve = min(25 if limit is None else limit, 25) + if retrieve < 1 or cursor is None: + return + + data = await endpoint( + channel.id, + AppCommandType.message.value, + limit=retrieve if not application_id else None, + query=query if not command_ids and not application_id else None, + command_ids=command_ids if not application_id else None, # type: ignore + application_id=application_id, + include_applications=include_applications if not application_id else None, + cursor=cursor if cursor is not MISSING else None, + ) + cursor = data['cursor'].get('next') + cmds = data['application_commands'] + apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []} + + if len(cmds) < 25: + limit = 0 + + for cmd in cmds: + # Handle faked parameters + if command_ids and int(cmd['id']) not in command_ids: + continue + elif application_id and query and query.lower() not in cmd['name'].lower(): + continue + elif application_id and limit == 0: + return + + if limit is not None: + limit -= 1 + + cmd['application'] = apps.get(int(cmd['application_id'])) + yield MessageCommand(state=state, data=cmd, channel=channel, message=self) diff --git a/discord/modal.py b/discord/modal.py index 422739d5c..44b184351 100644 --- a/discord/modal.py +++ b/discord/modal.py @@ -25,7 +25,6 @@ from __future__ import annotations from typing import List, Optional, TYPE_CHECKING, Union -from .appinfo import InteractionApplication from .components import _component_factory from .enums import InteractionType from .errors import InvalidData @@ -33,6 +32,7 @@ from .mixins import Hashable from .utils import _generate_nonce if TYPE_CHECKING: + from .appinfo import InteractionApplication from .components import Component from .interactions import Interaction @@ -92,7 +92,7 @@ class Modal(Hashable): self.title: str = data.get('title', '') self.custom_id: str = data.get('custom_id', '') self.components: List[Component] = [_component_factory(d) for d in data.get('components', [])] - self.application: InteractionApplication = InteractionApplication(state=interaction._state, data=data['application']) + self.application: InteractionApplication = interaction._state.create_interaction_application(data['application']) def __str__(self) -> str: return self.title diff --git a/discord/state.py b/discord/state.py index 39e76d014..065e127cf 100644 --- a/discord/state.py +++ b/discord/state.py @@ -78,6 +78,7 @@ from .permissions import Permissions, PermissionOverwrite from .member import _ClientStatus from .modal import Modal from .member import VoiceState +from .appinfo import InteractionApplication if TYPE_CHECKING: from .abc import PrivateChannel, Snowflake as abcSnowflake @@ -2275,3 +2276,6 @@ class ConnectionState: def create_message(self, *, channel: MessageableChannel, data: MessagePayload) -> Message: return Message(state=self, channel=channel, data=data) + + def create_interaction_application(self, data: dict) -> InteractionApplication: + return InteractionApplication(state=self, data=data) diff --git a/discord/user.py b/discord/user.py index 26760eeaa..593d0d5dc 100644 --- a/discord/user.py +++ b/discord/user.py @@ -31,7 +31,6 @@ from .asset import Asset from .colour import Colour from .enums import ( Locale, - AppCommandType, HypeSquadHouse, PremiumType, RelationshipAction, @@ -40,7 +39,6 @@ from .enums import ( ) from .errors import ClientException, NotFound from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags -from .iterators import FakeCommandIterator from .object import Object from .relationship import Relationship from .settings import UserSettings @@ -985,66 +983,6 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): data: DMChannelPayload = await state.http.start_private_message(self.id) return state.add_dm_channel(data) - def user_commands( - self, - query: Optional[str] = None, - *, - limit: Optional[int] = None, - command_ids: Optional[List[int]] = [], - **_, - ): - """Returns an iterator that allows you to see what user commands are available to use on this user. - - Only available on bots. - - .. note:: - - All parameters here are faked, as the only way to get commands in a DM is to fetch them all at once. - Because of this, some are silently ignored. The ones below currently work. - It is recommended to not pass any parameters to this iterator. - - Examples - --------- - - Usage :: - - async for command in user.user_commands(): - print(command.name) - - Flattening into a list :: - - commands = await user.user_commands().flatten() - # commands is now a list of UserCommand... - - All parameters are optional. - - Parameters - ---------- - query: Optional[:class:`str`] - The query to search for. - limit: Optional[:class:`int`] - The maximum number of commands to send back. Defaults to ``None`` to iterate over all results. Must be at least 1. - command_ids: Optional[List[:class:`int`]] - List of command IDs to search for. If the command doesn't exist it won't be returned. - - Raises - ------ - TypeError - The user is not a bot. - Both query and command_ids were passed. - ValueError - The limit was not > 0. - HTTPException - Getting the commands failed. - - Yields - ------- - :class:`.UserCommand` - A user command. - """ - iterator = FakeCommandIterator(self, AppCommandType.user, query, limit, command_ids) - return iterator.iterate() - def is_friend(self) -> bool: """:class:`bool`: Checks if the user is your friend.""" r = self.relationship