[commands] Allow Cog and app_commands interopability

This changeset allows app commands defined inside Cog to work as
expected. Likewise, by deriving app_commands.Group and Cog you can
make the cog function as a top level command on Discord.
This commit is contained in:
Rapptz
2022-03-09 19:48:51 -05:00
parent 5741ad9368
commit 446bfa78b0
3 changed files with 157 additions and 21 deletions

View File

@ -36,6 +36,8 @@ import types
from typing import Any, Callable, Mapping, List, Dict, TYPE_CHECKING, Optional, TypeVar, Type, Union, overload
import discord
from discord import app_commands
from discord.app_commands.tree import _retrieve_guild_ids
from .core import GroupMixin
from .view import StringView
@ -50,7 +52,7 @@ if TYPE_CHECKING:
import importlib.machinery
from discord.message import Message
from discord.abc import User
from discord.abc import User, Snowflake
from ._types import (
Check,
CoroFunc,
@ -135,6 +137,8 @@ class BotBase(GroupMixin):
super().__init__(**options)
self.command_prefix = command_prefix
self.extra_events: Dict[str, List[CoroFunc]] = {}
# Self doesn't have the ClientT bound, but since this is a mixin it technically does
self.__tree: app_commands.CommandTree[Self] = app_commands.CommandTree(self) # type: ignore
self.__cogs: Dict[str, Cog] = {}
self.__extensions: Dict[str, types.ModuleType] = {}
self._checks: List[Check] = []
@ -529,11 +533,22 @@ class BotBase(GroupMixin):
# cogs
def add_cog(self, cog: Cog, /, *, override: bool = False) -> None:
def add_cog(
self,
cog: Cog,
/,
*,
override: bool = False,
guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
) -> None:
"""Adds a "cog" to the bot.
A cog is a class that has its own event listeners and commands.
If the cog is a :class:`.app_commands.Group` then it is added to
the bot's :class:`~discord.app_commands.CommandTree` as well.
.. versionchanged:: 2.0
:exc:`.ClientException` is raised when a cog with the same name
@ -551,6 +566,19 @@ class BotBase(GroupMixin):
If a previously loaded cog with the same name should be ejected
instead of raising an error.
.. versionadded:: 2.0
guild: Optional[:class:`~discord.abc.Snowflake`]
If the cog is an application command group, then this would be the
guild where the cog group would be added to. If not given then
it becomes a global command instead.
.. versionadded:: 2.0
guilds: List[:class:`~discord.abc.Snowflake`]
If the cog is an application command group, then this would be the
guilds where the cog group would be added to. If not given then
it becomes a global command instead. Cannot be mixed with
``guild``.
.. versionadded:: 2.0
Raises
@ -572,7 +600,10 @@ class BotBase(GroupMixin):
if existing is not None:
if not override:
raise discord.ClientException(f'Cog named {cog_name!r} already loaded')
self.remove_cog(cog_name)
self.remove_cog(cog_name, guild=guild, guilds=guilds)
if isinstance(cog, app_commands.Group):
self.__tree.add_command(cog, override=override, guild=guild, guilds=guilds)
cog = cog._inject(self)
self.__cogs[cog_name] = cog
@ -600,7 +631,13 @@ class BotBase(GroupMixin):
"""
return self.__cogs.get(name)
def remove_cog(self, name: str, /) -> Optional[Cog]:
def remove_cog(
self,
name: str,
/,
guild: Optional[Snowflake] = MISSING,
guilds: List[Snowflake] = MISSING,
) -> Optional[Cog]:
"""Removes a cog from the bot and returns it.
All registered commands and event listeners that the
@ -616,6 +653,19 @@ class BotBase(GroupMixin):
-----------
name: :class:`str`
The name of the cog to remove.
guild: Optional[:class:`~discord.abc.Snowflake`]
If the cog is an application command group, then this would be the
guild where the cog group would be removed from. If not given then
a global command is removed instead instead.
.. versionadded:: 2.0
guilds: List[:class:`~discord.abc.Snowflake`]
If the cog is an application command group, then this would be the
guilds where the cog group would be removed from. If not given then
a global command is removed instead instead. Cannot be mixed with
``guild``.
.. versionadded:: 2.0
Returns
-------
@ -630,6 +680,15 @@ class BotBase(GroupMixin):
help_command = self._help_command
if help_command and help_command.cog is cog:
help_command.cog = None
if isinstance(cog, app_commands.Group):
guild_ids = _retrieve_guild_ids(cog, guild, guilds)
if guild_ids is None:
self.__tree.remove_command(name)
else:
for guild_id in guild_ids:
self.__tree.remove_command(name, guild=discord.Object(guild_id))
cog._eject(self)
return cog
@ -894,6 +953,20 @@ class BotBase(GroupMixin):
else:
self._help_command = None
# application command interop
# As mentioned above, this is a mixin so the Self type hint fails here.
# However, since the only classes that can use this are subclasses of Client
# anyway, then this is sound.
@property
def tree(self) -> app_commands.CommandTree[Self]: # type: ignore
""":class:`~discord.app_commands.CommandTree`: The command tree responsible for handling the application commands
in this bot.
.. versionadded:: 2.0
"""
return self.__tree
# command processing
async def get_prefix(self, message: Message) -> Union[List[str], str]: