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 sys
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
@ -54,6 +54,23 @@ __all__ = ('CommandTree',)
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]):
"""Represents a container that holds application command information.
@ -121,7 +138,8 @@ class CommandTree(Generic[ClientT]):
command: Union[Command, ContextMenu, Group],
/,
*,
guild: Optional[Snowflake] = None,
guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
override: bool = False,
):
"""Adds an application command to the tree.
@ -138,6 +156,10 @@ class CommandTree(Generic[ClientT]):
guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given then it
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`
Whether to override a command with the same name. If ``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.
TypeError
The application command passed is not a valid application command.
Or, ``guild`` and ``guilds`` were both given.
ValueError
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.
"""
guild_ids = _retrieve_guild_ids(guild, guilds)
if isinstance(command, ContextMenu):
guild_id = None if guild is None else guild.id
type = command.type.value
key = (command.name, guild_id, type)
found = key in self._context_menus
if found and not override:
raise CommandAlreadyRegistered(command.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)')
self._context_menus[key] = command
name = command.name
def _context_menu_add_helper(
guild_id: Optional[int],
data: Dict[Tuple[str, Optional[int], int], ContextMenu],
name: str = name,
type: int = type,
) -> None:
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
elif not isinstance(command, (Command, Group)):
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
name = root.name
if guild is not None:
commands = self._guild_commands.setdefault(guild.id, {})
found = name in commands
if found and not override:
raise CommandAlreadyRegistered(name, guild.id)
if len(commands) + found > 100:
raise ValueError('maximum number of slash commands exceeded (100)')
commands[name] = root
if guild_ids is not None:
# Validate that the command can be added first, before actually
# adding it into the mapping. This ensures atomicity.
for guild_id in guild_ids:
commands = self._guild_commands.get(guild_id, {})
found = name in commands
if found and not override:
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:
found = name in self._global_commands
if found and not override:
raise CommandAlreadyRegistered(name, None)
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
@overload
@ -459,7 +509,8 @@ class CommandTree(Generic[ClientT]):
*,
name: 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]]:
"""Creates an application command directly under this tree.
@ -475,6 +526,10 @@ class CommandTree(Generic[ClientT]):
guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given then it
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]:
@ -495,13 +550,17 @@ class CommandTree(Generic[ClientT]):
callback=func,
parent=None,
)
self.add_command(command, guild=guild)
self.add_command(command, guild=guild, guilds=guilds)
return command
return decorator
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]:
"""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`]
The guild to add the command to. If not given then it
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:
@ -538,7 +601,7 @@ class CommandTree(Generic[ClientT]):
raise TypeError('context menu function must be a coroutine function')
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 decorator

Loading…
Cancel
Save