Browse Source

Add support for adding app commands locally to many guilds

This affects the context_menu and command decorators as well. Removing
and syncing do not support multiple guild IDs.
pull/7596/head
Rapptz 3 years ago
parent
commit
e6a87e0782
  1. 113
      discord/app_commands/tree.py

113
discord/app_commands/tree.py

@ -26,7 +26,7 @@ from __future__ import annotations
import inspect import inspect
import sys import sys
import traceback import traceback
from typing import Callable, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Union, overload from typing import Callable, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, Set, Tuple, TypeVar, Union, overload
from .namespace import Namespace, ResolveKey from .namespace import Namespace, ResolveKey
@ -54,6 +54,23 @@ __all__ = ('CommandTree',)
ClientT = TypeVar('ClientT', bound='Client') ClientT = TypeVar('ClientT', bound='Client')
def _retrieve_guild_ids(guild: Optional[Snowflake] = MISSING, guilds: List[Snowflake] = MISSING) -> Optional[Set[int]]:
if guild is not MISSING and guilds is not MISSING:
raise TypeError('cannot mix guild and guilds keyword arguments')
# guilds=[] or guilds=[...] or no args at all
if guild is MISSING:
if not guilds:
return None
return {g.id for g in guilds}
# At this point it should be...
# guild=None or guild=Object
if guild is None:
return None
return {guild.id}
class CommandTree(Generic[ClientT]): class CommandTree(Generic[ClientT]):
"""Represents a container that holds application command information. """Represents a container that holds application command information.
@ -121,7 +138,8 @@ class CommandTree(Generic[ClientT]):
command: Union[Command, ContextMenu, Group], command: Union[Command, ContextMenu, Group],
/, /,
*, *,
guild: Optional[Snowflake] = None, guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
override: bool = False, override: bool = False,
): ):
"""Adds an application command to the tree. """Adds an application command to the tree.
@ -138,6 +156,10 @@ class CommandTree(Generic[ClientT]):
guild: Optional[:class:`~discord.abc.Snowflake`] guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given then it The guild to add the command to. If not given then it
becomes a global command instead. becomes a global command instead.
guilds: List[:class:`~discord.abc.Snowflake`]
The list of guilds to add the command to. This cannot be mixed
with the ``guild`` parameter. If no guilds are given at all
then it becomes a global command instead.
override: :class:`bool` override: :class:`bool`
Whether to override a command with the same name. If ``False`` Whether to override a command with the same name. If ``False``
an exception is raised. Default is ``False``. an exception is raised. Default is ``False``.
@ -148,23 +170,44 @@ class CommandTree(Generic[ClientT]):
The command was already registered and no override was specified. The command was already registered and no override was specified.
TypeError TypeError
The application command passed is not a valid application command. The application command passed is not a valid application command.
Or, ``guild`` and ``guilds`` were both given.
ValueError ValueError
The maximum number of commands was reached globally or for that guild. The maximum number of commands was reached globally or for that guild.
This is currently 100 for slash commands and 5 for context menu commands. This is currently 100 for slash commands and 5 for context menu commands.
""" """
guild_ids = _retrieve_guild_ids(guild, guilds)
if isinstance(command, ContextMenu): if isinstance(command, ContextMenu):
guild_id = None if guild is None else guild.id
type = command.type.value type = command.type.value
key = (command.name, guild_id, type) name = command.name
found = key in self._context_menus
if found and not override: def _context_menu_add_helper(
raise CommandAlreadyRegistered(command.name, guild_id) guild_id: Optional[int],
data: Dict[Tuple[str, Optional[int], int], ContextMenu],
total = sum(1 for _, g, t in self._context_menus if g == guild_id and t == type) name: str = name,
if total + found > 5: type: int = type,
raise ValueError('maximum number of context menu commands exceeded (5)') ) -> None:
self._context_menus[key] = command key = (name, guild_id, type)
found = key in self._context_menus
if found and not override:
raise CommandAlreadyRegistered(name, guild_id)
total = sum(1 for _, g, t in self._context_menus if g == guild_id and t == type)
if total + found > 5:
raise ValueError('maximum number of context menu commands exceeded (5)')
data[key] = command
if guild_ids is None:
_context_menu_add_helper(None, self._context_menus)
else:
current: Dict[Tuple[str, Optional[int], int], ContextMenu] = {}
for guild_id in guild_ids:
_context_menu_add_helper(guild_id, current)
# Update at the end in order to make sure the update is atomic.
# An error during addition could end up making the context menu mapping
# have a partial state
self._context_menus.update(current)
return return
elif not isinstance(command, (Command, Group)): elif not isinstance(command, (Command, Group)):
raise TypeError(f'Expected a application command, received {command.__class__!r} instead') raise TypeError(f'Expected a application command, received {command.__class__!r} instead')
@ -173,20 +216,27 @@ class CommandTree(Generic[ClientT]):
root = command.root_parent or command root = command.root_parent or command
name = root.name name = root.name
if guild is not None: if guild_ids is not None:
commands = self._guild_commands.setdefault(guild.id, {}) # Validate that the command can be added first, before actually
found = name in commands # adding it into the mapping. This ensures atomicity.
if found and not override: for guild_id in guild_ids:
raise CommandAlreadyRegistered(name, guild.id) commands = self._guild_commands.get(guild_id, {})
if len(commands) + found > 100: found = name in commands
raise ValueError('maximum number of slash commands exceeded (100)') if found and not override:
commands[name] = root raise CommandAlreadyRegistered(name, guild_id)
if len(commands) + found > 100:
raise ValueError(f'maximum number of slash commands exceeded (100) for guild_id {guild_id}')
# Actually add the command now that it has been verified to be okay.
for guild_id in guild_ids:
commands = self._guild_commands.setdefault(guild_id, {})
commands[name] = root
else: else:
found = name in self._global_commands found = name in self._global_commands
if found and not override: if found and not override:
raise CommandAlreadyRegistered(name, None) raise CommandAlreadyRegistered(name, None)
if len(self._global_commands) + found > 100: if len(self._global_commands) + found > 100:
raise ValueError('maximum number of slash commands exceeded (100)') raise ValueError('maximum number of global slash commands exceeded (100)')
self._global_commands[name] = root self._global_commands[name] = root
@overload @overload
@ -459,7 +509,8 @@ class CommandTree(Generic[ClientT]):
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING, description: str = MISSING,
guild: Optional[Snowflake] = None, guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
) -> Callable[[CommandCallback[Group, P, T]], Command[Group, P, T]]: ) -> Callable[[CommandCallback[Group, P, T]], Command[Group, P, T]]:
"""Creates an application command directly under this tree. """Creates an application command directly under this tree.
@ -475,6 +526,10 @@ class CommandTree(Generic[ClientT]):
guild: Optional[:class:`~discord.abc.Snowflake`] guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given then it The guild to add the command to. If not given then it
becomes a global command instead. becomes a global command instead.
guilds: List[:class:`~discord.abc.Snowflake`]
The list of guilds to add the command to. This cannot be mixed
with the ``guild`` parameter. If no guilds are given at all
then it becomes a global command instead.
""" """
def decorator(func: CommandCallback[Group, P, T]) -> Command[Group, P, T]: def decorator(func: CommandCallback[Group, P, T]) -> Command[Group, P, T]:
@ -495,13 +550,17 @@ class CommandTree(Generic[ClientT]):
callback=func, callback=func,
parent=None, parent=None,
) )
self.add_command(command, guild=guild) self.add_command(command, guild=guild, guilds=guilds)
return command return command
return decorator return decorator
def context_menu( def context_menu(
self, *, name: str = MISSING, guild: Optional[Snowflake] = None self,
*,
name: str = MISSING,
guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
) -> Callable[[ContextMenuCallback], ContextMenu]: ) -> Callable[[ContextMenuCallback], ContextMenu]:
"""Creates a application command context menu from a regular function directly under this tree. """Creates a application command context menu from a regular function directly under this tree.
@ -531,6 +590,10 @@ class CommandTree(Generic[ClientT]):
guild: Optional[:class:`~discord.abc.Snowflake`] guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given then it The guild to add the command to. If not given then it
becomes a global command instead. becomes a global command instead.
guilds: List[:class:`~discord.abc.Snowflake`]
The list of guilds to add the command to. This cannot be mixed
with the ``guild`` parameter. If no guilds are given at all
then it becomes a global command instead.
""" """
def decorator(func: ContextMenuCallback) -> ContextMenu: def decorator(func: ContextMenuCallback) -> ContextMenu:
@ -538,7 +601,7 @@ class CommandTree(Generic[ClientT]):
raise TypeError('context menu function must be a coroutine function') raise TypeError('context menu function must be a coroutine function')
context_menu = ContextMenu._from_decorator(func, name=name) context_menu = ContextMenu._from_decorator(func, name=name)
self.add_command(context_menu, guild=guild) self.add_command(context_menu, guild=guild, guilds=guilds)
return context_menu return context_menu
return decorator return decorator

Loading…
Cancel
Save