Skip to content

Commit

Permalink
Implement commands v3
Browse files Browse the repository at this point in the history
  • Loading branch information
dolfies committed Jan 15, 2024
1 parent e525cc3 commit 8d77474
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 88 deletions.
172 changes: 96 additions & 76 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ def _handle_commands(
limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[SlashCommand]:
...
Expand All @@ -185,7 +184,6 @@ def _handle_commands(
limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[UserCommand]:
...
Expand All @@ -200,92 +198,67 @@ def _handle_commands(
limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[MessageCommand]:
...


async def _handle_commands(
messageable: Union[Messageable, Message],
type: ApplicationCommandType,
type: Optional[ApplicationCommandType] = None,
*,
query: Optional[str] = None,
limit: Optional[int] = None,
command_ids: Optional[Collection[int]] = None,
application: Optional[Snowflake] = None,
with_applications: bool = True,
target: Optional[Snowflake] = None,
) -> AsyncIterator[BaseCommand]:
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 = messageable._state
endpoint = state.http.search_application_commands
channel = await messageable._get_channel()
_, cls = _command_factory(type.value)
cmd_ids = list(command_ids) if command_ids else None

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
target = recipient
target = channel.recipient # type: ignore
elif channel.type == ChannelType.group:
return

prev_cursor = MISSING
cursor = MISSING
while True:
# We keep two cursors because Discord just sends us an infinite loop sometimes
retrieve = min((25 if not cmd_ids else 0) if limit is None else limit, 25)

if not application_id and limit is not None:
limit -= retrieve
if (not cmd_ids and retrieve < 1) or cursor is None or (prev_cursor is not MISSING and prev_cursor == cursor):
return
cmds = await channel.application_commands()

data = await endpoint(
channel.id,
type.value,
limit=retrieve if not application_id else None,
query=query if not cmd_ids and not application_id else None,
command_ids=cmd_ids if not application_id and not cursor else None, # type: ignore
application_id=application_id,
include_applications=with_applications if (not application_id or with_applications) else None,
cursor=cursor,
)
prev_cursor = cursor
cursor = data['cursor'].get('next')
cmds = data['application_commands']
apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []}

for cmd in cmds:
# Handle faked parameters
if application_id and query and query.lower() not in cmd['name']:
continue
elif application_id and (not cmd_ids or int(cmd['id']) not in cmd_ids) and limit == 0:
continue
for cmd in cmds:
# Handle faked parameters
if type is not None and cmd.type != type:
continue
if query and query.lower() not in cmd.name:
continue
if (not cmd_ids or cmd.id not in cmd_ids) and limit == 0:
continue
if application_id and cmd.application_id != application_id:
continue
if target:
if cmd.type == ApplicationCommandType.user:
cmd._user = target
elif cmd.type == ApplicationCommandType.message:
cmd._message = target # type: ignore

# We follow Discord behavior
if application_id and limit is not None and (not cmd_ids or int(cmd['id']) not in cmd_ids):
limit -= 1
# We follow Discord behavior
if limit is not None and (not cmd_ids or cmd.id not in cmd_ids):
limit -= 1

try:
cmd_ids.remove(int(cmd['id'])) if cmd_ids else None
except ValueError:
pass
try:
cmd_ids.remove(cmd.id) if cmd_ids else None
except ValueError:
pass

application = apps.get(int(cmd['application_id']))
yield cls(state=state, data=cmd, channel=channel, target=target, application=application)
yield cmd

cmd_ids = None
if application_id or len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25:
return
cmd_ids = None
if len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25:
return


async def _handle_message_search(
Expand Down Expand Up @@ -1406,9 +1379,9 @@ async def move(self, **kwargs: Any) -> None:
An invalid position was given.
TypeError
A bad mix of arguments were passed.
Forbidden
~discord.Forbidden
You do not have permissions to move the channel.
HTTPException
~discord.HTTPException
Moving the channel failed.
"""

Expand Down Expand Up @@ -2407,6 +2380,58 @@ def search(
most_relevant=most_relevant,
)

async def application_commands(self) -> List[Union[SlashCommand, UserCommand, MessageCommand]]:
"""|coro|
Returns a list of application commands available in the channel.
.. versionadded:: 2.1
.. note::
Commands that the user does not have permission to use will not be returned.
Raises
------
TypeError
Attempted to fetch commands in a DM with a non-bot user.
ValueError
Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed.
Returns
-------
List[Union[:class:`~discord.SlashCommand`, :class:`~discord.UserCommand`, :class:`~discord.MessageCommand`]]
A list of application commands.
"""
channel = await self._get_channel()
state = self._state
if channel.type is ChannelType.private:
if not channel.recipient.bot: # type: ignore
raise TypeError('Cannot fetch commands in a DM with a non-bot user')

data = await state.http.channel_application_command_index(channel.id)
elif channel.type is ChannelType.group:
# TODO: Are commands in group DMs truly dead?
return []
else:
guild_id = getattr(channel.guild, 'id', getattr(channel, 'guild_id', None))
if not guild_id:
raise ValueError('Could not resolve channel guild ID') from None
data = await state.http.guild_application_command_index(guild_id)

cmds = data['application_commands']
apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []}

result = []
for cmd in cmds:
_, cls = _command_factory(cmd['type'])
application = apps.get(int(cmd['application_id']))
result.append(cls(state=state, data=cmd, channel=channel, application=application))
return result

@utils.deprecated('Messageable.application_commands')
def slash_commands(
self,
query: Optional[str] = None,
Expand All @@ -2418,6 +2443,8 @@ def slash_commands(
) -> AsyncIterator[SlashCommand]:
"""Returns a :term:`asynchronous iterator` of the slash commands available in the channel.
.. deprecated:: 2.1
Examples
---------
Expand All @@ -2437,13 +2464,8 @@ def slash_commands(
----------
query: Optional[:class:`str`]
The query to search for. Specifying this limits results to 25 commands max.
This parameter is faked if ``application`` is specified.
limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to 0 if ``command_ids`` is passed, else 25.
If ``None``, returns all commands.
This parameter is faked if ``application`` is specified.
The maximum number of commands to send back. If ``None``, returns all commands.
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.
Expand All @@ -2452,7 +2474,7 @@ def slash_commands(
application: Optional[:class:`~discord.abc.Snowflake`]
Whether to return this application's commands. Always set to DM recipient in a private channel context.
with_applications: :class:`bool`
Whether to include applications in the response. Defaults to ``True``.
Whether to include applications in the response.
Raises
------
Expand All @@ -2461,7 +2483,8 @@ def slash_commands(
Attempted to fetch commands in a DM with a non-bot user.
ValueError
The limit was not greater than or equal to 0.
HTTPException
Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed.
~discord.Forbidden
You do not have permissions to get the commands.
Expand All @@ -2480,9 +2503,9 @@ def slash_commands(
limit=limit,
command_ids=command_ids,
application=application,
with_applications=with_applications,
)

@utils.deprecated('Messageable.application_commands')
def user_commands(
self,
query: Optional[str] = None,
Expand All @@ -2494,6 +2517,8 @@ def user_commands(
) -> AsyncIterator[UserCommand]:
"""Returns a :term:`asynchronous iterator` of the user commands available to use on the user.
.. deprecated:: 2.1
Examples
---------
Expand All @@ -2513,13 +2538,8 @@ def user_commands(
----------
query: Optional[:class:`str`]
The query to search for. Specifying this limits results to 25 commands max.
This parameter is faked if ``application`` is specified.
limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to 0 if ``command_ids`` is passed, else 25.
If ``None``, returns all commands.
This parameter is faked if ``application`` is specified.
The maximum number of commands to send back. If ``None``, returns all commands.
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.
Expand All @@ -2528,7 +2548,7 @@ def user_commands(
application: Optional[:class:`~discord.abc.Snowflake`]
Whether to return this application's commands. Always set to DM recipient in a private channel context.
with_applications: :class:`bool`
Whether to include applications in the response. Defaults to ``True``.
Whether to include applications in the response.
Raises
------
Expand All @@ -2537,7 +2557,8 @@ def user_commands(
Attempted to fetch commands in a DM with a non-bot user.
ValueError
The limit was not greater than or equal to 0.
HTTPException
Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed.
~discord.Forbidden
You do not have permissions to get the commands.
Expand All @@ -2556,7 +2577,6 @@ def user_commands(
limit=limit,
command_ids=command_ids,
application=application,
with_applications=with_applications,
)


Expand Down
2 changes: 1 addition & 1 deletion discord/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ class Activity(BaseActivity):
'application_id',
'emoji',
'buttons',
'metadata'
'metadata',
)

def __init__(self, **kwargs: Any) -> None:
Expand Down
2 changes: 1 addition & 1 deletion discord/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def __init__(
self._data = data
self.application = application
self.name = data['name']
self.description = data['description']
self.description = data.get('description', '')
self._channel = channel
self.application_id: int = int(data['application_id'])
self.id: int = int(data['id'])
Expand Down
1 change: 1 addition & 0 deletions discord/ext/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ async def reply(
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
return await self.message.reply(content, **kwargs)

@discord.utils.deprecated("Context.application_commands")
@discord.utils.copy_doc(Message.message_commands)
def message_commands(
self,
Expand Down
Loading

0 comments on commit 8d77474

Please sign in to comment.