Browse Source

Implement commands v3

pull/10109/head
dolfies 1 year ago
parent
commit
8d77474269
  1. 172
      discord/abc.py
  2. 2
      discord/activity.py
  3. 2
      discord/commands.py
  4. 1
      discord/ext/commands/context.py
  5. 37
      discord/guild.py
  6. 9
      discord/http.py
  7. 13
      discord/message.py
  8. 9
      discord/types/command.py
  9. 5
      discord/utils.py

172
discord/abc.py

@ -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]:
...
@ -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]:
...
@ -200,7 +198,6 @@ def _handle_commands(
limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[MessageCommand]:
...
@ -208,13 +205,12 @@ def _handle_commands(
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:
@ -222,70 +218,47 @@ async def _handle_commands(
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(
@ -1406,9 +1379,9 @@ class GuildChannel:
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.
"""
@ -2407,6 +2380,58 @@ class Messageable:
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,
@ -2418,6 +2443,8 @@ class Messageable:
) -> AsyncIterator[SlashCommand]:
"""Returns a :term:`asynchronous iterator` of the slash commands available in the channel.
.. deprecated:: 2.1
Examples
---------
@ -2437,13 +2464,8 @@ class Messageable:
----------
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.
@ -2452,7 +2474,7 @@ class Messageable:
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
------
@ -2461,7 +2483,8 @@ class Messageable:
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.
@ -2480,9 +2503,9 @@ class Messageable:
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,
@ -2494,6 +2517,8 @@ class Messageable:
) -> AsyncIterator[UserCommand]:
"""Returns a :term:`asynchronous iterator` of the user commands available to use on the user.
.. deprecated:: 2.1
Examples
---------
@ -2513,13 +2538,8 @@ class Messageable:
----------
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.
@ -2528,7 +2548,7 @@ class Messageable:
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
------
@ -2537,7 +2557,8 @@ class Messageable:
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.
@ -2556,7 +2577,6 @@ class Messageable:
limit=limit,
command_ids=command_ids,
application=application,
with_applications=with_applications,
)

2
discord/activity.py

@ -211,7 +211,7 @@ class Activity(BaseActivity):
'application_id',
'emoji',
'buttons',
'metadata'
'metadata',
)
def __init__(self, **kwargs: Any) -> None:

2
discord/commands.py

@ -219,7 +219,7 @@ class BaseCommand(ApplicationCommand, Hashable):
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'])

1
discord/ext/commands/context.py

@ -516,6 +516,7 @@ 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.deprecated("Context.application_commands")
@discord.utils.copy_doc(Message.message_commands)
def message_commands(
self,

37
discord/guild.py

@ -100,6 +100,7 @@ from .guild_premium import PremiumGuildSubscription
from .entitlements import Entitlement
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
from .partial_emoji import _EmojiTag, PartialEmoji
from .commands import _command_factory
if TYPE_CHECKING:
from .abc import Snowflake, SnowflakeTime
@ -140,6 +141,7 @@ if TYPE_CHECKING:
from .types.oauth2 import OAuth2Guild as OAuth2GuildPayload
from .message import EmojiInputType, Message
from .read_state import ReadState
from .commands import UserCommand, MessageCommand, SlashCommand
VocalGuildChannel = Union[VoiceChannel, StageChannel]
NonCategoryChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, DirectoryChannel]
@ -1318,7 +1320,7 @@ class Guild(Hashable):
@property
def application_command_count(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns the application command count if available.
"""Optional[:class:`int`]: Returns the application command count, if available.
.. versionadded:: 2.0
"""
@ -3273,6 +3275,39 @@ class Guild(Hashable):
return [convert(d) for d in data]
async def application_commands(self) -> List[Union[SlashCommand, UserCommand, MessageCommand]]:
"""|coro|
Returns a list of all application commands available in the guild.
.. versionadded:: 2.1
.. note::
Commands that the user does not have permission to use will not be returned.
Raises
-------
HTTPException
Fetching the commands failed.
Returns
--------
List[Union[:class:`SlashCommand`, :class:`UserCommand`, :class:`MessageCommand`]]
The list of application commands that are available in the guild.
"""
state = self._state
data = await state.http.guild_application_command_index(self.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, application=application))
return result
async def fetch_stickers(self) -> List[GuildSticker]:
r"""|coro|

9
discord/http.py

@ -4451,6 +4451,15 @@ class HTTPClient:
Route('GET', '/channels/{channel_id}/application-commands/search', channel_id=channel_id), params=params
)
def guild_application_command_index(self, guild_id: Snowflake) -> Response[command.GuildApplicationCommandIndex]:
return self.request(Route('GET', '/guilds/{guild_id}/application-command-index', guild_id=guild_id))
def channel_application_command_index(self, channel_id: Snowflake) -> Response[command.ApplicationCommandIndex]:
return self.request(Route('GET', '/channels/{channel_id}/application-command-index', channel_id=channel_id))
def user_application_command_index(self) -> Response[command.ApplicationCommandIndex]:
return self.request(Route('GET', '/users/@me/application-command-index'))
def interact(
self,
type: InteractionType,

13
discord/message.py

@ -2338,6 +2338,7 @@ class Message(PartialMessage, Hashable):
"""
return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
@utils.deprecated("Message.channel.application_commands")
def message_commands(
self,
query: Optional[str] = None,
@ -2349,6 +2350,8 @@ class Message(PartialMessage, Hashable):
) -> AsyncIterator[MessageCommand]:
"""Returns a :term:`asynchronous iterator` of the message commands available to use on the message.
.. deprecated:: 2.1
Examples
---------
@ -2368,13 +2371,8 @@ class Message(PartialMessage, Hashable):
----------
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.
@ -2383,7 +2381,7 @@ class Message(PartialMessage, Hashable):
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
------
@ -2411,6 +2409,5 @@ class Message(PartialMessage, Hashable):
limit=limit,
command_ids=command_ids,
application=application,
with_applications=with_applications,
target=self,
)

9
discord/types/command.py

@ -222,7 +222,14 @@ class ApplicationCommandCursor(TypedDict):
repaired: Optional[str]
class ApplicationCommandSearch(TypedDict):
class ApplicationCommandIndex(TypedDict):
application_commands: List[ApplicationCommand]
applications: Optional[List[IntegrationApplication]]
class GuildApplicationCommandIndex(ApplicationCommandIndex):
version: Snowflake
class ApplicationCommandSearch(ApplicationCommandIndex):
cursor: ApplicationCommandCursor

5
discord/utils.py

@ -1354,6 +1354,7 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None)
return f'<t:{int(dt.timestamp())}:{style}>'
@deprecated()
def set_target(
items: Iterable[ApplicationCommand],
*,
@ -1368,6 +1369,10 @@ def set_target(
Suppresses all AttributeErrors so you can pass multiple types of commands and
not worry about which elements support which parameter.
.. versionadded:: 2.0
.. deprecated:: 2.1
Parameters
-----------
items: Iterable[:class:`.abc.ApplicationCommand`]

Loading…
Cancel
Save