d4bf750c9e
Initial commit
1686 lines
65 KiB
Python
1686 lines
65 KiB
Python
"""
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2015-2021 Rapptz
|
|
Copyright (c) 2021-present Pycord Development
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a
|
|
copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import collections
|
|
import collections.abc
|
|
import copy
|
|
import inspect
|
|
import logging
|
|
import sys
|
|
import traceback
|
|
from abc import ABC, abstractmethod
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Any,
|
|
Callable,
|
|
Coroutine,
|
|
Generator,
|
|
Literal,
|
|
Mapping,
|
|
TypeVar,
|
|
)
|
|
|
|
from .client import Client
|
|
from .cog import CogMixin
|
|
from .commands import (
|
|
ApplicationCommand,
|
|
ApplicationContext,
|
|
AutocompleteContext,
|
|
MessageCommand,
|
|
SlashCommand,
|
|
SlashCommandGroup,
|
|
UserCommand,
|
|
command,
|
|
)
|
|
from .enums import IntegrationType, InteractionContextType, InteractionType, TeamRole
|
|
from .errors import CheckFailure, DiscordException
|
|
from .interactions import Interaction
|
|
from .shard import AutoShardedClient
|
|
from .types import interactions
|
|
from .user import User
|
|
from .utils import MISSING, async_all, find, get
|
|
|
|
if TYPE_CHECKING:
|
|
from typing_extensions import Never
|
|
|
|
from .cog import Cog
|
|
from .commands import Option
|
|
from .ext.commands import Cooldown
|
|
from .member import Member
|
|
from .permissions import Permissions
|
|
|
|
C = TypeVar("C", bound=MessageCommand | SlashCommand | UserCommand)
|
|
CoroFunc = Callable[..., Coroutine[Any, Any, Any]]
|
|
CFT = TypeVar("CFT", bound=CoroFunc)
|
|
|
|
__all__ = (
|
|
"ApplicationCommandMixin",
|
|
"Bot",
|
|
"AutoShardedBot",
|
|
)
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
class ApplicationCommandMixin(ABC):
|
|
"""A mixin that implements common functionality for classes that need
|
|
application command compatibility.
|
|
|
|
Attributes
|
|
----------
|
|
application_commands: :class:`dict`
|
|
A mapping of command id string to :class:`.ApplicationCommand` objects.
|
|
pending_application_commands: :class:`list`
|
|
A list of commands that have been added but not yet registered. This is read-only and is modified via other
|
|
methods.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
self._pending_application_commands = []
|
|
self._application_commands = {}
|
|
|
|
@property
|
|
def all_commands(self):
|
|
return self._application_commands
|
|
|
|
@property
|
|
def pending_application_commands(self):
|
|
return self._pending_application_commands
|
|
|
|
@property
|
|
def commands(self) -> list[ApplicationCommand | Any]:
|
|
commands = self.application_commands
|
|
if self._bot._supports_prefixed_commands and hasattr(
|
|
self._bot, "prefixed_commands"
|
|
):
|
|
commands += getattr(self._bot, "prefixed_commands")
|
|
return commands
|
|
|
|
@property
|
|
def application_commands(self) -> list[ApplicationCommand]:
|
|
return list(self._application_commands.values())
|
|
|
|
def add_application_command(self, command: ApplicationCommand) -> None:
|
|
"""Adds an :class:`.ApplicationCommand` into the internal list of commands.
|
|
|
|
This is usually not called, instead the :meth:`command` or
|
|
other shortcut decorators are used instead.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
command: :class:`.ApplicationCommand`
|
|
The command to add.
|
|
"""
|
|
if isinstance(command, SlashCommand) and command.is_subcommand:
|
|
raise TypeError("The provided command is a sub-command of group")
|
|
|
|
if self._bot.debug_guilds and command.guild_ids is None:
|
|
command.guild_ids = self._bot.debug_guilds
|
|
if self._bot.default_command_contexts and command.contexts is None:
|
|
command.contexts = self._bot.default_command_contexts
|
|
if (
|
|
self._bot.default_command_integration_types
|
|
and command.integration_types is None
|
|
):
|
|
command.integration_types = self._bot.default_command_integration_types
|
|
|
|
for cmd in self.pending_application_commands:
|
|
if cmd == command:
|
|
command.id = cmd.id
|
|
self._application_commands[command.id] = command
|
|
break
|
|
self._pending_application_commands.append(command)
|
|
|
|
def remove_application_command(
|
|
self, command: ApplicationCommand
|
|
) -> ApplicationCommand | None:
|
|
"""Remove an :class:`.ApplicationCommand` from the internal list
|
|
of commands.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
command: :class:`.ApplicationCommand`
|
|
The command to remove.
|
|
|
|
Returns
|
|
-------
|
|
Optional[:class:`.ApplicationCommand`]
|
|
The command that was removed. If the command has not been added,
|
|
``None`` is returned instead.
|
|
"""
|
|
if command.id:
|
|
self._application_commands.pop(command.id, None)
|
|
|
|
if command in self._pending_application_commands:
|
|
self._pending_application_commands.remove(command)
|
|
return command
|
|
|
|
@property
|
|
def get_command(self):
|
|
"""Shortcut for :meth:`.get_application_command`.
|
|
|
|
.. note::
|
|
Overridden in :class:`ext.commands.Bot`.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
# TODO: Do something like we did in self.commands for this
|
|
return self.get_application_command
|
|
|
|
def get_application_command(
|
|
self,
|
|
name: str,
|
|
guild_ids: list[int] | None = None,
|
|
type: type[ApplicationCommand] = ApplicationCommand,
|
|
) -> ApplicationCommand | None:
|
|
"""Get an :class:`.ApplicationCommand` from the internal list
|
|
of commands.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
name: :class:`str`
|
|
The qualified name of the command to get.
|
|
guild_ids: List[:class:`int`]
|
|
The guild ids associated to the command to get.
|
|
type: Type[:class:`.ApplicationCommand`]
|
|
The type of the command to get. Defaults to :class:`.ApplicationCommand`.
|
|
|
|
Returns
|
|
-------
|
|
Optional[:class:`.ApplicationCommand`]
|
|
The command that was requested. If not found, returns ``None``.
|
|
"""
|
|
commands = self._application_commands.values()
|
|
for command in commands:
|
|
if command.name == name and isinstance(command, type):
|
|
if guild_ids is not None and command.guild_ids != guild_ids:
|
|
return
|
|
return command
|
|
elif (names := name.split())[0] == command.name and isinstance(
|
|
command, SlashCommandGroup
|
|
):
|
|
while len(names) > 1:
|
|
command = get(commands, name=names.pop(0))
|
|
if not isinstance(command, SlashCommandGroup) or (
|
|
guild_ids is not None and command.guild_ids != guild_ids
|
|
):
|
|
return
|
|
commands = command.subcommands
|
|
command = get(commands, name=names.pop())
|
|
if not isinstance(command, type) or (
|
|
guild_ids is not None and command.guild_ids != guild_ids
|
|
):
|
|
return
|
|
return command
|
|
|
|
async def get_desynced_commands(
|
|
self,
|
|
guild_id: int | None = None,
|
|
prefetched: list[interactions.ApplicationCommand] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""|coro|
|
|
|
|
Gets the list of commands that are desynced from discord. If ``guild_id`` is specified, it will only return
|
|
guild commands that are desynced from said guild, else it will return global commands.
|
|
|
|
.. note::
|
|
This function is meant to be used internally, and should only be used if you want to override the default
|
|
command registration behavior.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
guild_id: Optional[:class:`int`]
|
|
The guild id to get the desynced commands for, else global commands if unspecified.
|
|
prefetched: Optional[List[:class:`.ApplicationCommand`]]
|
|
If you already fetched the commands, you can pass them here to be used. Not recommended for typical usage.
|
|
|
|
Returns
|
|
-------
|
|
List[Dict[:class:`str`, Any]]
|
|
A list of the desynced commands. Each will come with at least the ``cmd`` and ``action`` keys, which
|
|
respectively contain the command and the action to perform. Other keys may also be present depending on
|
|
the action, including ``id``.
|
|
"""
|
|
|
|
# We can suggest the user to upsert, edit, delete, or bulk upsert the commands
|
|
|
|
def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool:
|
|
if isinstance(cmd, SlashCommandGroup):
|
|
if len(cmd.subcommands) != len(match.get("options", [])):
|
|
return True
|
|
for i, subcommand in enumerate(cmd.subcommands):
|
|
match_ = next(
|
|
(
|
|
data
|
|
for data in match["options"]
|
|
if data["name"] == subcommand.name
|
|
),
|
|
MISSING,
|
|
)
|
|
if match_ is not MISSING and _check_command(subcommand, match_):
|
|
return True
|
|
else:
|
|
as_dict = cmd.to_dict()
|
|
to_check = {
|
|
"nsfw": None,
|
|
"default_member_permissions": None,
|
|
"name": None,
|
|
"description": None,
|
|
"name_localizations": None,
|
|
"description_localizations": None,
|
|
"options": [
|
|
"type",
|
|
"name",
|
|
"description",
|
|
"autocomplete",
|
|
"choices",
|
|
"name_localizations",
|
|
"description_localizations",
|
|
],
|
|
"contexts": None,
|
|
"integration_types": None,
|
|
}
|
|
for check, value in to_check.items():
|
|
if type(to_check[check]) == list:
|
|
# We need to do some falsy conversion here
|
|
# The API considers False (autocomplete) and [] (choices) to be falsy values
|
|
falsy_vals = (False, [])
|
|
for opt in value:
|
|
cmd_vals = (
|
|
[val.get(opt, MISSING) for val in as_dict[check]]
|
|
if check in as_dict
|
|
else []
|
|
)
|
|
for i, val in enumerate(cmd_vals):
|
|
if val in falsy_vals:
|
|
cmd_vals[i] = MISSING
|
|
if match.get(
|
|
check, MISSING
|
|
) is not MISSING and cmd_vals != [
|
|
val.get(opt, MISSING) for val in match[check]
|
|
]:
|
|
# We have a difference
|
|
return True
|
|
elif (attr := getattr(cmd, check, None)) != (
|
|
found := match.get(check)
|
|
):
|
|
# We might have a difference
|
|
if "localizations" in check and bool(attr) == bool(found):
|
|
# unlike other attrs, localizations are MISSING by default
|
|
continue
|
|
elif (
|
|
check == "default_permission"
|
|
and attr is True
|
|
and found is None
|
|
):
|
|
# This is a special case
|
|
# TODO: Remove for perms v2
|
|
continue
|
|
return True
|
|
return False
|
|
|
|
return_value = []
|
|
cmds = self.pending_application_commands.copy()
|
|
|
|
if guild_id is None:
|
|
pending = [cmd for cmd in cmds if cmd.guild_ids is None]
|
|
else:
|
|
pending = [
|
|
cmd
|
|
for cmd in cmds
|
|
if cmd.guild_ids is not None and guild_id in cmd.guild_ids
|
|
]
|
|
|
|
registered_commands: list[interactions.ApplicationCommand] = []
|
|
if prefetched is not None:
|
|
registered_commands = prefetched
|
|
elif self._bot.user:
|
|
if guild_id is None:
|
|
registered_commands = await self._bot.http.get_global_commands(
|
|
self._bot.user.id
|
|
)
|
|
else:
|
|
registered_commands = await self._bot.http.get_guild_commands(
|
|
self._bot.user.id, guild_id
|
|
)
|
|
|
|
registered_commands_dict = {cmd["name"]: cmd for cmd in registered_commands}
|
|
# First let's check if the commands we have locally are the same as the ones on discord
|
|
for cmd in pending:
|
|
match = registered_commands_dict.get(cmd.name)
|
|
if match is None:
|
|
# We don't have this command registered
|
|
return_value.append({"command": cmd, "action": "upsert"})
|
|
elif _check_command(cmd, match):
|
|
return_value.append(
|
|
{
|
|
"command": cmd,
|
|
"action": "edit",
|
|
"id": int(registered_commands_dict[cmd.name]["id"]),
|
|
}
|
|
)
|
|
else:
|
|
# We have this command registered but it's the same
|
|
return_value.append(
|
|
{"command": cmd, "action": None, "id": int(match["id"])}
|
|
)
|
|
|
|
# Now let's see if there are any commands on discord that we need to delete
|
|
for cmd, value_ in registered_commands_dict.items():
|
|
match = get(pending, name=registered_commands_dict[cmd]["name"])
|
|
if match is None:
|
|
# We have this command registered but not in our list
|
|
return_value.append(
|
|
{
|
|
"command": registered_commands_dict[cmd]["name"],
|
|
"id": int(value_["id"]),
|
|
"action": "delete",
|
|
}
|
|
)
|
|
|
|
continue
|
|
|
|
return return_value
|
|
|
|
async def register_command(
|
|
self,
|
|
command: ApplicationCommand,
|
|
force: bool = True,
|
|
guild_ids: list[int] | None = None,
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
Registers a command. If the command has ``guild_ids`` set, or if the ``guild_ids`` parameter is passed,
|
|
the command will be registered as a guild command for those guilds.
|
|
|
|
Parameters
|
|
----------
|
|
command: :class:`~.ApplicationCommand`
|
|
The command to register.
|
|
force: :class:`bool`
|
|
Whether to force the command to be registered. If this is set to False, the command will only be registered
|
|
if it seems to already be registered and up to date with our internal cache. Defaults to True.
|
|
guild_ids: :class:`list`
|
|
A list of guild ids to register the command for. If this is not set, the command's
|
|
:attr:`ApplicationCommand.guild_ids` attribute will be used.
|
|
|
|
Returns
|
|
-------
|
|
:class:`~.ApplicationCommand`
|
|
The command that was registered
|
|
"""
|
|
# TODO: Write this
|
|
raise NotImplementedError
|
|
|
|
async def register_commands(
|
|
self,
|
|
commands: list[ApplicationCommand] | None = None,
|
|
guild_id: int | None = None,
|
|
method: Literal["individual", "bulk", "auto"] = "bulk",
|
|
force: bool = False,
|
|
delete_existing: bool = True,
|
|
) -> list[interactions.ApplicationCommand]:
|
|
"""|coro|
|
|
|
|
Register a list of commands.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
commands: Optional[List[:class:`~.ApplicationCommand`]]
|
|
A list of commands to register. If this is not set (``None``), then all commands will be registered.
|
|
guild_id: Optional[int]
|
|
If this is set, the commands will be registered as a guild command for the respective guild. If it is not
|
|
set, the commands will be registered according to their :attr:`ApplicationCommand.guild_ids` attribute.
|
|
method: Literal['individual', 'bulk', 'auto']
|
|
The method to use when registering the commands. If this is set to "individual", then each command will be
|
|
registered individually. If this is set to "bulk", then all commands will be registered in bulk. If this is
|
|
set to "auto", then the method will be determined automatically. Defaults to "bulk".
|
|
force: :class:`bool`
|
|
Registers the commands regardless of the state of the command on Discord. This uses one less API call, but
|
|
can result in hitting rate limits more often. Defaults to False.
|
|
delete_existing: :class:`bool`
|
|
Whether to delete existing commands that are not in the list of commands to register. Defaults to True.
|
|
"""
|
|
if commands is None:
|
|
commands = self.pending_application_commands
|
|
|
|
commands = [copy.copy(cmd) for cmd in commands]
|
|
|
|
if guild_id is not None:
|
|
for cmd in commands:
|
|
to_rep_with = [guild_id]
|
|
cmd.guild_ids = to_rep_with
|
|
|
|
is_global = guild_id is None
|
|
|
|
registered = []
|
|
|
|
if is_global:
|
|
pending = list(filter(lambda c: c.guild_ids is None, commands))
|
|
registration_methods = {
|
|
"bulk": self._bot.http.bulk_upsert_global_commands,
|
|
"upsert": self._bot.http.upsert_global_command,
|
|
"delete": self._bot.http.delete_global_command,
|
|
"edit": self._bot.http.edit_global_command,
|
|
}
|
|
|
|
def _register(
|
|
method: Literal["bulk", "upsert", "delete", "edit"], *args, **kwargs
|
|
):
|
|
return registration_methods[method](
|
|
self._bot.user and self._bot.user.id, *args, **kwargs
|
|
)
|
|
|
|
else:
|
|
pending = list(
|
|
filter(
|
|
lambda c: c.guild_ids is not None and guild_id in c.guild_ids,
|
|
commands,
|
|
)
|
|
)
|
|
registration_methods = {
|
|
"bulk": self._bot.http.bulk_upsert_guild_commands,
|
|
"upsert": self._bot.http.upsert_guild_command,
|
|
"delete": self._bot.http.delete_guild_command,
|
|
"edit": self._bot.http.edit_guild_command,
|
|
}
|
|
|
|
def _register(
|
|
method: Literal["bulk", "upsert", "delete", "edit"], *args, **kwargs
|
|
):
|
|
return registration_methods[method](
|
|
self._bot.user and self._bot.user.id, guild_id, *args, **kwargs
|
|
)
|
|
|
|
def register(
|
|
method: Literal["bulk", "upsert", "delete", "edit"],
|
|
*args,
|
|
cmd_name: str = None,
|
|
guild_id: int | None = None,
|
|
**kwargs,
|
|
):
|
|
if kwargs.pop("_log", True):
|
|
if method == "bulk":
|
|
_log.debug(
|
|
f"Bulk updating commands {[c['name'] for c in args[0]]} for"
|
|
f" guild {guild_id}"
|
|
)
|
|
elif method == "upsert":
|
|
_log.debug(f"Creating command {cmd_name} for guild {guild_id}") # type: ignore
|
|
elif method == "edit":
|
|
_log.debug(f"Editing command {cmd_name} for guild {guild_id}") # type: ignore
|
|
elif method == "delete":
|
|
_log.debug(f"Deleting command {cmd_name} for guild {guild_id}") # type: ignore
|
|
return _register(method, *args, **kwargs)
|
|
|
|
pending_actions = []
|
|
|
|
if not force:
|
|
prefetched_commands: list[interactions.ApplicationCommand] = []
|
|
if self._bot.user:
|
|
if guild_id is None:
|
|
prefetched_commands = await self._bot.http.get_global_commands(
|
|
self._bot.user.id
|
|
)
|
|
else:
|
|
prefetched_commands = await self._bot.http.get_guild_commands(
|
|
self._bot.user.id, guild_id
|
|
)
|
|
desynced = await self.get_desynced_commands(
|
|
guild_id=guild_id, prefetched=prefetched_commands
|
|
)
|
|
|
|
for cmd in desynced:
|
|
if cmd["action"] == "delete":
|
|
pending_actions.append(
|
|
{
|
|
"action": "delete" if delete_existing else None,
|
|
"command": collections.namedtuple("Command", ["name"])(
|
|
name=cmd["command"]
|
|
),
|
|
"id": cmd["id"],
|
|
}
|
|
)
|
|
continue
|
|
# We can assume the command item is a command, since it's only a string if action is delete
|
|
match = get(pending, name=cmd["command"].name, type=cmd["command"].type)
|
|
if match is None:
|
|
continue
|
|
if cmd["action"] == "edit":
|
|
pending_actions.append(
|
|
{
|
|
"action": "edit",
|
|
"command": match,
|
|
"id": cmd["id"],
|
|
}
|
|
)
|
|
elif cmd["action"] == "upsert":
|
|
pending_actions.append(
|
|
{
|
|
"action": "upsert",
|
|
"command": match,
|
|
}
|
|
)
|
|
elif cmd["action"] is None:
|
|
pending_actions.append(
|
|
{
|
|
"action": None,
|
|
"command": match,
|
|
}
|
|
)
|
|
else:
|
|
raise ValueError(f"Unknown action: {cmd['action']}")
|
|
filtered_no_action = list(
|
|
filter(lambda c: c["action"] is not None, pending_actions)
|
|
)
|
|
filtered_deleted = list(
|
|
filter(lambda a: a["action"] != "delete", pending_actions)
|
|
)
|
|
if method == "bulk" or (
|
|
method == "auto" and len(filtered_deleted) == len(pending)
|
|
):
|
|
# Either the method is bulk or all the commands need to be modified, so we can just do a bulk upsert
|
|
data = [cmd["command"].to_dict() for cmd in filtered_deleted]
|
|
# If there's nothing to update, don't bother
|
|
if len(filtered_no_action) == 0:
|
|
_log.debug("Skipping bulk command update: Commands are up to date")
|
|
registered = prefetched_commands
|
|
else:
|
|
_log.debug(
|
|
"Bulk updating commands %s for guild %s",
|
|
{c["command"].name: c["action"] for c in pending_actions},
|
|
guild_id,
|
|
)
|
|
registered = await register("bulk", data, _log=False)
|
|
else:
|
|
if not filtered_no_action:
|
|
registered = []
|
|
for cmd in filtered_no_action:
|
|
if cmd["action"] == "delete":
|
|
await register(
|
|
"delete",
|
|
cmd["id"],
|
|
cmd_name=cmd["command"].name,
|
|
guild_id=guild_id,
|
|
)
|
|
continue
|
|
if cmd["action"] == "edit":
|
|
registered.append(
|
|
await register(
|
|
"edit",
|
|
cmd["id"],
|
|
cmd["command"].to_dict(),
|
|
cmd_name=cmd["command"].name,
|
|
guild_id=guild_id,
|
|
)
|
|
)
|
|
elif cmd["action"] == "upsert":
|
|
registered.append(
|
|
await register(
|
|
"upsert",
|
|
cmd["command"].to_dict(),
|
|
cmd_name=cmd["command"].name,
|
|
guild_id=guild_id,
|
|
)
|
|
)
|
|
else:
|
|
raise ValueError(f"Unknown action: {cmd['action']}")
|
|
|
|
# TODO: Our lists dont work sometimes, see if that can be fixed so we can avoid this second API call
|
|
if method != "bulk":
|
|
if self._bot.user:
|
|
if guild_id is None:
|
|
registered = await self._bot.http.get_global_commands(
|
|
self._bot.user.id
|
|
)
|
|
else:
|
|
registered = await self._bot.http.get_guild_commands(
|
|
self._bot.user.id, guild_id
|
|
)
|
|
else:
|
|
data = [cmd.to_dict() for cmd in pending]
|
|
registered = await register("bulk", data, guild_id=guild_id)
|
|
|
|
for i in registered:
|
|
cmd = get(
|
|
self.pending_application_commands,
|
|
name=i["name"],
|
|
type=i.get("type"),
|
|
)
|
|
if not cmd:
|
|
raise ValueError(
|
|
f"Registered command {i['name']}, type {i.get('type')} not found in"
|
|
" pending commands"
|
|
)
|
|
cmd.id = i["id"]
|
|
self._application_commands[cmd.id] = cmd
|
|
|
|
return registered
|
|
|
|
async def sync_commands(
|
|
self,
|
|
commands: list[ApplicationCommand] | None = None,
|
|
method: Literal["individual", "bulk", "auto"] = "bulk",
|
|
force: bool = False,
|
|
guild_ids: list[int] | None = None,
|
|
register_guild_commands: bool = True,
|
|
check_guilds: list[int] | None = [],
|
|
delete_existing: bool = True,
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
Registers all commands that have been added through :meth:`.add_application_command`. This method cleans up all
|
|
commands over the API and should sync them with the internal cache of commands. It attempts to register the
|
|
commands in the most efficient way possible, unless ``force`` is set to ``True``, in which case it will always
|
|
register all commands.
|
|
|
|
By default, this coroutine is called inside the :func:`.on_connect` event. If you choose to override the
|
|
:func:`.on_connect` event, then you should invoke this coroutine as well such as the following:
|
|
|
|
.. code-block:: python
|
|
|
|
@bot.event
|
|
async def on_connect():
|
|
if bot.auto_sync_commands:
|
|
await bot.sync_commands()
|
|
print(f"{bot.user.name} connected.")
|
|
|
|
.. note::
|
|
If you remove all guild commands from a particular guild, the library may not be able to detect and update
|
|
the commands accordingly, as it would have to individually check for each guild. To force the library to
|
|
unregister a guild's commands, call this function with ``commands=[]`` and ``guild_ids=[guild_id]``.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
commands: Optional[List[:class:`~.ApplicationCommand`]]
|
|
A list of commands to register. If this is not set (None), then all commands will be registered.
|
|
method: Literal['individual', 'bulk', 'auto']
|
|
The method to use when registering the commands. If this is set to "individual", then each command will be
|
|
registered individually. If this is set to "bulk", then all commands will be registered in bulk. If this is
|
|
set to "auto", then the method will be determined automatically. Defaults to "bulk".
|
|
force: :class:`bool`
|
|
Registers the commands regardless of the state of the command on Discord. This uses one less API call, but
|
|
can result in hitting rate limits more often. Defaults to False.
|
|
guild_ids: Optional[List[:class:`int`]]
|
|
A list of guild ids to register the commands for. If this is not set, the commands'
|
|
:attr:`~.ApplicationCommand.guild_ids` attribute will be used.
|
|
register_guild_commands: :class:`bool`
|
|
Whether to register guild commands. Defaults to True.
|
|
check_guilds: Optional[List[:class:`int`]]
|
|
A list of guilds ids to check for commands to unregister, since the bot would otherwise have to check all
|
|
guilds. Unlike ``guild_ids``, this does not alter the commands' :attr:`~.ApplicationCommand.guild_ids`
|
|
attribute, instead it adds the guild ids to a list of guilds to sync commands for. If
|
|
``register_guild_commands`` is set to False, then this parameter is ignored.
|
|
delete_existing: :class:`bool`
|
|
Whether to delete existing commands that are not in the list of commands to register. Defaults to True.
|
|
"""
|
|
|
|
check_guilds = list(set((check_guilds or []) + (self._bot.debug_guilds or [])))
|
|
|
|
if commands is None:
|
|
commands = self.pending_application_commands
|
|
|
|
if guild_ids is not None:
|
|
for cmd in commands:
|
|
cmd.guild_ids = guild_ids
|
|
|
|
global_commands = [cmd for cmd in commands if cmd.guild_ids is None]
|
|
registered_commands = await self.register_commands(
|
|
global_commands, method=method, force=force, delete_existing=delete_existing
|
|
)
|
|
|
|
registered_guild_commands: dict[int, list[interactions.ApplicationCommand]] = {}
|
|
|
|
if register_guild_commands:
|
|
cmd_guild_ids: list[int] = []
|
|
for cmd in commands:
|
|
if cmd.guild_ids is not None:
|
|
cmd_guild_ids.extend(cmd.guild_ids)
|
|
if check_guilds is not None:
|
|
cmd_guild_ids.extend(check_guilds)
|
|
for guild_id in set(cmd_guild_ids):
|
|
guild_commands = [
|
|
cmd
|
|
for cmd in commands
|
|
if cmd.guild_ids is not None and guild_id in cmd.guild_ids
|
|
]
|
|
app_cmds = await self.register_commands(
|
|
guild_commands,
|
|
guild_id=guild_id,
|
|
method=method,
|
|
force=force,
|
|
delete_existing=delete_existing,
|
|
)
|
|
registered_guild_commands[guild_id] = app_cmds
|
|
|
|
for i in registered_commands:
|
|
cmd = get(
|
|
self.pending_application_commands,
|
|
name=i["name"],
|
|
guild_ids=None,
|
|
type=i.get("type"),
|
|
)
|
|
if cmd:
|
|
cmd.id = i["id"]
|
|
self._application_commands[cmd.id] = cmd
|
|
|
|
if register_guild_commands and registered_guild_commands:
|
|
for guild_id, guild_cmds in registered_guild_commands.items():
|
|
for i in guild_cmds:
|
|
cmd = find(
|
|
lambda cmd: cmd.name == i["name"]
|
|
and cmd.type == i.get("type")
|
|
and cmd.guild_ids is not None
|
|
and (guild_id := i.get("guild_id"))
|
|
and guild_id in cmd.guild_ids,
|
|
self.pending_application_commands,
|
|
)
|
|
if not cmd:
|
|
# command has not been added yet
|
|
continue
|
|
cmd.id = i["id"]
|
|
self._application_commands[cmd.id] = cmd
|
|
|
|
async def process_application_commands(
|
|
self, interaction: Interaction, auto_sync: bool | None = None
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
This function processes the commands that have been registered
|
|
to the bot and other groups. Without this coroutine, none of the
|
|
commands will be triggered.
|
|
|
|
By default, this coroutine is called inside the :func:`.on_interaction`
|
|
event. If you choose to override the :func:`.on_interaction` event, then
|
|
you should invoke this coroutine as well.
|
|
|
|
This function finds a registered command matching the interaction id from
|
|
application commands and invokes it. If no matching command was
|
|
found, it replies to the interaction with a default message.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
interaction: :class:`discord.Interaction`
|
|
The interaction to process
|
|
auto_sync: Optional[:class:`bool`]
|
|
Whether to automatically sync and unregister the command if it is not found in the internal cache. This will
|
|
invoke the :meth:`~.Bot.sync_commands` method on the context of the command, either globally or per-guild,
|
|
based on the type of the command, respectively. Defaults to :attr:`.Bot.auto_sync_commands`.
|
|
"""
|
|
if auto_sync is None:
|
|
auto_sync = self._bot.auto_sync_commands
|
|
# TODO: find out why the isinstance check below doesn't stop the type errors below
|
|
if interaction.type not in (
|
|
InteractionType.application_command,
|
|
InteractionType.auto_complete,
|
|
):
|
|
return
|
|
|
|
command: ApplicationCommand | None = None
|
|
try:
|
|
if interaction.data:
|
|
command = self._application_commands[interaction.data["id"]] # type: ignore
|
|
except KeyError:
|
|
for cmd in self.application_commands + self.pending_application_commands:
|
|
if interaction.data:
|
|
guild_id = interaction.data.get("guild_id")
|
|
if guild_id:
|
|
guild_id = int(guild_id)
|
|
if cmd.name == interaction.data["name"] and ( # type: ignore
|
|
guild_id == cmd.guild_ids
|
|
or (
|
|
isinstance(cmd.guild_ids, list)
|
|
and guild_id in cmd.guild_ids
|
|
)
|
|
):
|
|
command = cmd
|
|
break
|
|
else:
|
|
if auto_sync and interaction.data:
|
|
guild_id = interaction.data.get("guild_id")
|
|
if guild_id is None:
|
|
await self.sync_commands()
|
|
else:
|
|
await self.sync_commands(check_guilds=[guild_id])
|
|
return self._bot.dispatch("unknown_application_command", interaction)
|
|
|
|
if interaction.type is InteractionType.auto_complete:
|
|
return self._bot.dispatch(
|
|
"application_command_auto_complete", interaction, command
|
|
)
|
|
|
|
ctx = await self.get_application_context(interaction)
|
|
if command:
|
|
interaction.command = command
|
|
await self.invoke_application_command(ctx)
|
|
|
|
async def on_application_command_auto_complete(
|
|
self, interaction: Interaction, command: ApplicationCommand
|
|
) -> None:
|
|
async def callback() -> None:
|
|
ctx = await self.get_autocomplete_context(interaction)
|
|
interaction.command = command
|
|
return await command.invoke_autocomplete_callback(ctx)
|
|
|
|
autocomplete_task = self._bot.loop.create_task(callback())
|
|
try:
|
|
await self._bot.wait_for(
|
|
"application_command_auto_complete",
|
|
check=lambda i, c: c == command,
|
|
timeout=3,
|
|
)
|
|
except asyncio.TimeoutError:
|
|
return
|
|
else:
|
|
if not autocomplete_task.done():
|
|
autocomplete_task.cancel()
|
|
|
|
def slash_command(
|
|
self,
|
|
*,
|
|
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
|
|
cog: Cog | None = MISSING,
|
|
contexts: set[InteractionContextType] | None = MISSING,
|
|
cooldown: Cooldown | None = MISSING,
|
|
default_member_permissions: Permissions | None = MISSING,
|
|
description: str | None = MISSING,
|
|
description_localizations: dict[str, str] | None = MISSING,
|
|
guild_ids: list[int] | None = MISSING,
|
|
guild_only: bool | None = MISSING,
|
|
integration_types: set[IntegrationType] | None = MISSING,
|
|
name: str | None = MISSING,
|
|
name_localizations: dict[str, str] | None = MISSING,
|
|
nsfw: bool | None = MISSING,
|
|
options: list[Option] | None = MISSING,
|
|
parent: SlashCommandGroup | None = MISSING,
|
|
**kwargs: Never,
|
|
) -> Callable[..., SlashCommand]:
|
|
"""A shortcut decorator for adding a slash command to the bot.
|
|
This is equivalent to using :meth:`application_command`, providing
|
|
the :class:`SlashCommand` class.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Returns
|
|
-------
|
|
Callable[..., :class:`SlashCommand`]
|
|
A decorator that converts the provided function into a :class:`.SlashCommand`,
|
|
adds it to the bot, and returns it.
|
|
"""
|
|
return self.application_command(
|
|
cls=SlashCommand,
|
|
checks=checks,
|
|
cog=cog,
|
|
contexts=contexts,
|
|
cooldown=cooldown,
|
|
default_member_permissions=default_member_permissions,
|
|
description=description,
|
|
description_localizations=description_localizations,
|
|
guild_ids=guild_ids,
|
|
guild_only=guild_only,
|
|
integration_types=integration_types,
|
|
name=name,
|
|
name_localizations=name_localizations,
|
|
nsfw=nsfw,
|
|
options=options,
|
|
parent=parent,
|
|
**kwargs,
|
|
)
|
|
|
|
def user_command(
|
|
self,
|
|
*,
|
|
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
|
|
cog: Cog | None = MISSING,
|
|
contexts: set[InteractionContextType] | None = MISSING,
|
|
cooldown: Cooldown | None = MISSING,
|
|
default_member_permissions: Permissions | None = MISSING,
|
|
guild_ids: list[int] | None = MISSING,
|
|
guild_only: bool | None = MISSING,
|
|
integration_types: set[IntegrationType] | None = MISSING,
|
|
name: str | None = MISSING,
|
|
name_localizations: dict[str, str] | None = MISSING,
|
|
nsfw: bool | None = MISSING,
|
|
**kwargs: Never,
|
|
) -> Callable[..., UserCommand]:
|
|
"""A shortcut decorator for adding a user command to the bot.
|
|
This is equivalent to using :meth:`application_command`, providing
|
|
the :class:`UserCommand` class.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Returns
|
|
-------
|
|
Callable[..., :class:`UserCommand`]
|
|
A decorator that converts the provided function into a :class:`.UserCommand`,
|
|
adds it to the bot, and returns it.
|
|
"""
|
|
return self.application_command(
|
|
cls=UserCommand,
|
|
checks=checks,
|
|
cog=cog,
|
|
contexts=contexts,
|
|
cooldown=cooldown,
|
|
default_member_permissions=default_member_permissions,
|
|
guild_ids=guild_ids,
|
|
guild_only=guild_only,
|
|
integration_types=integration_types,
|
|
name=name,
|
|
name_localizations=name_localizations,
|
|
nsfw=nsfw,
|
|
**kwargs,
|
|
)
|
|
|
|
def message_command(
|
|
self,
|
|
*,
|
|
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
|
|
cog: Cog | None = MISSING,
|
|
contexts: set[InteractionContextType] | None = MISSING,
|
|
cooldown: Cooldown | None = MISSING,
|
|
default_member_permissions: Permissions | None = MISSING,
|
|
guild_ids: list[int] | None = MISSING,
|
|
guild_only: bool | None = MISSING,
|
|
integration_types: set[IntegrationType] | None = MISSING,
|
|
name: str | None = MISSING,
|
|
name_localizations: dict[str, str] | None = MISSING,
|
|
nsfw: bool | None = MISSING,
|
|
**kwargs: Never,
|
|
) -> Callable[..., MessageCommand]:
|
|
"""A shortcut decorator for adding a message command to the bot.
|
|
This is equivalent to using :meth:`application_command`, providing
|
|
the :class:`MessageCommand` class.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Returns
|
|
-------
|
|
Callable[..., :class:`MessageCommand`]
|
|
A decorator that converts the provided function into a :class:`.MessageCommand`,
|
|
adds it to the bot, and returns it.
|
|
"""
|
|
return self.application_command(
|
|
cls=MessageCommand,
|
|
checks=checks,
|
|
cog=cog,
|
|
contexts=contexts,
|
|
cooldown=cooldown,
|
|
default_member_permissions=default_member_permissions,
|
|
guild_ids=guild_ids,
|
|
guild_only=guild_only,
|
|
integration_types=integration_types,
|
|
name=name,
|
|
name_localizations=name_localizations,
|
|
nsfw=nsfw,
|
|
**kwargs,
|
|
)
|
|
|
|
def application_command(
|
|
self,
|
|
*,
|
|
cls: type[C] = SlashCommand,
|
|
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
|
|
cog: Cog | None = MISSING,
|
|
contexts: set[InteractionContextType] | None = MISSING,
|
|
cooldown: Cooldown | None = MISSING,
|
|
default_member_permissions: Permissions | None = MISSING,
|
|
description: str | None = MISSING,
|
|
description_localizations: dict[str, str] | None = MISSING,
|
|
guild_ids: list[int] | None = MISSING,
|
|
guild_only: bool | None = MISSING,
|
|
integration_types: set[IntegrationType] | None = MISSING,
|
|
name: str | None = MISSING,
|
|
name_localizations: dict[str, str] | None = MISSING,
|
|
nsfw: bool | None = MISSING,
|
|
options: list[Option] | None = MISSING,
|
|
parent: SlashCommandGroup | None = MISSING,
|
|
**kwargs: Any,
|
|
) -> Callable[..., C]:
|
|
"""A shortcut decorator that converts the provided function into
|
|
an application command via :func:`command` and adds it to
|
|
the internal command list via :meth:`~.Bot.add_application_command`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
cls: Type[:class:`ApplicationCommand`]
|
|
The factory class that will be used to create the command.
|
|
By default, this is :class:`.SlashCommand`. Should a custom
|
|
class be provided, it must be a subclass of either
|
|
:class:`SlashCommand`, :class:`MessageCommand` or :class:`UserCommand`.
|
|
|
|
Returns
|
|
-------
|
|
Callable[..., :class:`ApplicationCommand`]
|
|
A decorator that converts the provided function into an :class:`.ApplicationCommand`,
|
|
adds it to the bot, and returns it.
|
|
"""
|
|
|
|
params = {
|
|
"checks": checks,
|
|
"cog": cog,
|
|
"contexts": contexts,
|
|
"cooldown": cooldown,
|
|
"default_member_permissions": default_member_permissions,
|
|
"description": description,
|
|
"description_localizations": description_localizations,
|
|
"guild_ids": guild_ids,
|
|
"guild_only": guild_only,
|
|
"integration_types": integration_types,
|
|
"name": name,
|
|
"name_localizations": name_localizations,
|
|
"nsfw": nsfw,
|
|
"options": options,
|
|
"parent": parent,
|
|
**kwargs,
|
|
}
|
|
kwargs = {k: v for k, v in params.items() if v is not MISSING}
|
|
|
|
def decorator(func) -> C:
|
|
result = command(cls=cls, **kwargs)(func)
|
|
self.add_application_command(result)
|
|
return result
|
|
|
|
return decorator
|
|
|
|
def command(self, **kwargs):
|
|
"""An alias for :meth:`application_command`.
|
|
|
|
.. note::
|
|
|
|
This decorator is overridden by :class:`discord.ext.commands.Bot`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Returns
|
|
-------
|
|
Callable[..., :class:`ApplicationCommand`]
|
|
A decorator that converts the provided function into an :class:`.ApplicationCommand`,
|
|
adds it to the bot, and returns it.
|
|
"""
|
|
return self.application_command(**kwargs)
|
|
|
|
def create_group(
|
|
self,
|
|
name: str,
|
|
description: str | None = None,
|
|
guild_ids: list[int] | None = None,
|
|
**kwargs,
|
|
) -> SlashCommandGroup:
|
|
"""A shortcut method that creates a slash command group with no subcommands and adds it to the internal
|
|
command list via :meth:`add_application_command`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
name: :class:`str`
|
|
The name of the group to create.
|
|
description: Optional[:class:`str`]
|
|
The description of the group to create.
|
|
guild_ids: Optional[List[:class:`int`]]
|
|
A list of the IDs of each guild this group should be added to, making it a guild command.
|
|
This will be a global command if ``None`` is passed.
|
|
kwargs:
|
|
Any additional keyword arguments to pass to :class:`.SlashCommandGroup`.
|
|
|
|
Returns
|
|
-------
|
|
SlashCommandGroup
|
|
The slash command group that was created.
|
|
"""
|
|
description = description or "No description provided."
|
|
group = SlashCommandGroup(name, description, guild_ids, **kwargs)
|
|
self.add_application_command(group)
|
|
return group
|
|
|
|
def group(
|
|
self,
|
|
name: str | None = None,
|
|
description: str | None = None,
|
|
guild_ids: list[int] | None = None,
|
|
) -> Callable[[type[SlashCommandGroup]], SlashCommandGroup]:
|
|
"""A shortcut decorator that initializes the provided subclass of :class:`.SlashCommandGroup`
|
|
and adds it to the internal command list via :meth:`add_application_command`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
name: Optional[:class:`str`]
|
|
The name of the group to create. This will resolve to the name of the decorated class if ``None`` is passed.
|
|
description: Optional[:class:`str`]
|
|
The description of the group to create.
|
|
guild_ids: Optional[List[:class:`int`]]
|
|
A list of the IDs of each guild this group should be added to, making it a guild command.
|
|
This will be a global command if ``None`` is passed.
|
|
|
|
Returns
|
|
-------
|
|
Callable[[Type[SlashCommandGroup]], SlashCommandGroup]
|
|
The slash command group that was created.
|
|
"""
|
|
|
|
def inner(cls: type[SlashCommandGroup]) -> SlashCommandGroup:
|
|
group = cls(
|
|
name or cls.__name__,
|
|
(
|
|
description or inspect.cleandoc(cls.__doc__).splitlines()[0]
|
|
if cls.__doc__ is not None
|
|
else "No description provided"
|
|
),
|
|
guild_ids=guild_ids,
|
|
)
|
|
self.add_application_command(group)
|
|
return group
|
|
|
|
return inner
|
|
|
|
slash_group = group
|
|
|
|
def walk_application_commands(self) -> Generator[ApplicationCommand]:
|
|
"""An iterator that recursively walks through all application commands and subcommands.
|
|
|
|
Yields
|
|
------
|
|
:class:`.ApplicationCommand`
|
|
An application command from the internal list of application commands.
|
|
"""
|
|
for command in self.application_commands:
|
|
if isinstance(command, SlashCommandGroup):
|
|
yield from command.walk_commands()
|
|
yield command
|
|
|
|
async def get_application_context(
|
|
self, interaction: Interaction, cls: Any = ApplicationContext
|
|
) -> ApplicationContext:
|
|
r"""|coro|
|
|
|
|
Returns the invocation context from the interaction.
|
|
|
|
This is a more low-level counter-part for :meth:`.process_application_commands`
|
|
to allow users more fine-grained control over the processing.
|
|
|
|
Parameters
|
|
-----------
|
|
interaction: :class:`discord.Interaction`
|
|
The interaction to get the invocation context from.
|
|
cls
|
|
The factory class that will be used to create the context.
|
|
By default, this is :class:`.ApplicationContext`. Should a custom
|
|
class be provided, it must be similar enough to
|
|
:class:`.ApplicationContext`\'s interface.
|
|
|
|
Returns
|
|
--------
|
|
:class:`.ApplicationContext`
|
|
The invocation context. The type of this can change via the
|
|
``cls`` parameter.
|
|
"""
|
|
return cls(self, interaction)
|
|
|
|
async def get_autocomplete_context(
|
|
self, interaction: Interaction, cls: Any = AutocompleteContext
|
|
) -> AutocompleteContext:
|
|
r"""|coro|
|
|
|
|
Returns the autocomplete context from the interaction.
|
|
|
|
This is a more low-level counter-part for :meth:`.process_application_commands`
|
|
to allow users more fine-grained control over the processing.
|
|
|
|
Parameters
|
|
-----------
|
|
interaction: :class:`discord.Interaction`
|
|
The interaction to get the invocation context from.
|
|
cls
|
|
The factory class that will be used to create the context.
|
|
By default, this is :class:`.AutocompleteContext`. Should a custom
|
|
class be provided, it must be similar enough to
|
|
:class:`.AutocompleteContext`\'s interface.
|
|
|
|
Returns
|
|
--------
|
|
:class:`.AutocompleteContext`
|
|
The autocomplete context. The type of this can change via the
|
|
``cls`` parameter.
|
|
"""
|
|
return cls(self, interaction)
|
|
|
|
async def invoke_application_command(self, ctx: ApplicationContext) -> None:
|
|
"""|coro|
|
|
|
|
Invokes the application command given under the invocation
|
|
context and handles all the internal event dispatch mechanisms.
|
|
|
|
Parameters
|
|
----------
|
|
ctx: :class:`.ApplicationCommand`
|
|
The invocation context to invoke.
|
|
"""
|
|
self._bot.dispatch("application_command", ctx)
|
|
try:
|
|
if await self._bot.can_run(ctx, call_once=True):
|
|
await ctx.command.invoke(ctx)
|
|
else:
|
|
raise CheckFailure("The global check once functions failed.")
|
|
except DiscordException as exc:
|
|
await ctx.command.dispatch_error(ctx, exc)
|
|
else:
|
|
self._bot.dispatch("application_command_completion", ctx)
|
|
|
|
@property
|
|
@abstractmethod
|
|
def _bot(self) -> Bot | AutoShardedBot: ...
|
|
|
|
|
|
class BotBase(ApplicationCommandMixin, CogMixin, ABC):
|
|
_supports_prefixed_commands = False
|
|
|
|
def __init__(self, description=None, *args, **options):
|
|
super().__init__(*args, **options)
|
|
self.__cogs = {} # TYPE: Dict[str, Cog]
|
|
self.__extensions = {} # TYPE: Dict[str, types.ModuleType]
|
|
self._checks = [] # TYPE: List[Check]
|
|
self._check_once = []
|
|
self._before_invoke = None
|
|
self._after_invoke = None
|
|
self.description = inspect.cleandoc(description) if description else ""
|
|
self.owner_id = options.get("owner_id")
|
|
self.owner_ids = options.get("owner_ids", set())
|
|
self.auto_sync_commands = options.get("auto_sync_commands", True)
|
|
|
|
self.debug_guilds = options.pop("debug_guilds", None)
|
|
self.default_command_contexts = options.pop(
|
|
"default_command_contexts",
|
|
{
|
|
InteractionContextType.guild,
|
|
InteractionContextType.bot_dm,
|
|
InteractionContextType.private_channel,
|
|
},
|
|
)
|
|
|
|
self.default_command_integration_types = options.pop(
|
|
"default_command_integration_types",
|
|
{
|
|
IntegrationType.guild_install,
|
|
},
|
|
)
|
|
|
|
if self.owner_id and self.owner_ids:
|
|
raise TypeError("Both owner_id and owner_ids are set.")
|
|
|
|
if self.owner_ids and not isinstance(
|
|
self.owner_ids, collections.abc.Collection
|
|
):
|
|
raise TypeError(
|
|
f"owner_ids must be a collection not {self.owner_ids.__class__!r}"
|
|
)
|
|
if not isinstance(self.default_command_contexts, collections.abc.Collection):
|
|
raise TypeError(
|
|
f"default_command_contexts must be a collection not {self.default_command_contexts.__class__!r}"
|
|
)
|
|
if not isinstance(
|
|
self.default_command_integration_types, collections.abc.Collection
|
|
):
|
|
raise TypeError(
|
|
f"default_command_integration_types must be a collection not {self.default_command_integration_types.__class__!r}"
|
|
)
|
|
self.default_command_contexts = set(self.default_command_contexts)
|
|
self.default_command_integration_types = set(
|
|
self.default_command_integration_types
|
|
)
|
|
|
|
self._checks = []
|
|
self._check_once = []
|
|
self._before_invoke = None
|
|
self._after_invoke = None
|
|
|
|
async def on_connect(self):
|
|
if self.auto_sync_commands:
|
|
await self.sync_commands()
|
|
|
|
async def on_interaction(self, interaction):
|
|
await self.process_application_commands(interaction)
|
|
|
|
async def on_application_command_error(
|
|
self, context: ApplicationContext, exception: DiscordException
|
|
) -> None:
|
|
"""|coro|
|
|
|
|
The default command error handler provided by the bot.
|
|
|
|
By default, this prints to :data:`sys.stderr` however it could be
|
|
overridden to have a different implementation.
|
|
|
|
This only fires if you do not specify any listeners for command error.
|
|
"""
|
|
if self._event_handlers.get("on_application_command_error", None):
|
|
return
|
|
command = context.command
|
|
if command and command.has_error_handler():
|
|
return
|
|
|
|
cog = context.cog
|
|
if cog and cog.has_error_handler():
|
|
return
|
|
|
|
print(f"Ignoring exception in command {context.command}:", file=sys.stderr)
|
|
traceback.print_exception(
|
|
type(exception), exception, exception.__traceback__, file=sys.stderr
|
|
)
|
|
|
|
# global check registration
|
|
# TODO: Remove these from commands.Bot
|
|
|
|
def check(self, func):
|
|
"""A decorator that adds a global check to the bot. A global check is similar to a :func:`.check` that is
|
|
applied on a per-command basis except it is run before any command checks have been verified and applies to
|
|
every command the bot has.
|
|
|
|
.. note::
|
|
|
|
This function can either be a regular function or a coroutine. Similar to a command :func:`.check`, this
|
|
takes a single parameter of type :class:`.Context` and can only raise exceptions inherited from
|
|
:exc:`.ApplicationCommandError`.
|
|
|
|
Example
|
|
-------
|
|
.. code-block:: python3
|
|
|
|
@bot.check
|
|
def check_commands(ctx):
|
|
return ctx.command.qualified_name in allowed_commands
|
|
"""
|
|
# T was used instead of Check to ensure the type matches on return
|
|
self.add_check(func) # type: ignore
|
|
return func
|
|
|
|
def add_check(self, func, *, call_once: bool = False) -> None:
|
|
"""Adds a global check to the bot. This is the non-decorator interface to :meth:`.check` and
|
|
:meth:`.check_once`.
|
|
|
|
Parameters
|
|
----------
|
|
func
|
|
The function that was used as a global check.
|
|
call_once: :class:`bool`
|
|
If the function should only be called once per :meth:`.Bot.invoke` call.
|
|
"""
|
|
|
|
if call_once:
|
|
self._check_once.append(func)
|
|
else:
|
|
self._checks.append(func)
|
|
|
|
def remove_check(self, func, *, call_once: bool = False) -> None:
|
|
"""Removes a global check from the bot.
|
|
This function is idempotent and will not raise an exception
|
|
if the function is not in the global checks.
|
|
|
|
Parameters
|
|
----------
|
|
func
|
|
The function to remove from the global checks.
|
|
call_once: :class:`bool`
|
|
If the function was added with ``call_once=True`` in
|
|
the :meth:`.Bot.add_check` call or using :meth:`.check_once`.
|
|
"""
|
|
checks = self._check_once if call_once else self._checks
|
|
|
|
try:
|
|
checks.remove(func)
|
|
except ValueError:
|
|
pass
|
|
|
|
def check_once(self, func):
|
|
"""A decorator that adds a "call once" global check to the bot. Unlike regular global checks, this one is called
|
|
only once per :meth:`.Bot.invoke` call. Regular global checks are called whenever a command is called or
|
|
:meth:`.Command.can_run` is called. This type of check bypasses that and ensures that it's called only once,
|
|
even inside the default help command.
|
|
|
|
.. note::
|
|
|
|
When using this function the :class:`.Context` sent to a group subcommand may only parse the parent command
|
|
and not the subcommands due to it being invoked once per :meth:`.Bot.invoke` call.
|
|
|
|
.. note::
|
|
|
|
This function can either be a regular function or a coroutine. Similar to a command :func:`.check`,
|
|
this takes a single parameter of type :class:`.Context` and can only raise exceptions inherited from
|
|
:exc:`.ApplicationCommandError`.
|
|
|
|
Example
|
|
-------
|
|
.. code-block:: python3
|
|
|
|
@bot.check_once
|
|
def whitelist(ctx):
|
|
return ctx.message.author.id in my_whitelist
|
|
"""
|
|
self.add_check(func, call_once=True)
|
|
return func
|
|
|
|
async def can_run(
|
|
self, ctx: ApplicationContext, *, call_once: bool = False
|
|
) -> bool:
|
|
data = self._check_once if call_once else self._checks
|
|
|
|
if not data:
|
|
return True
|
|
|
|
# type-checker doesn't distinguish between functions and methods
|
|
return await async_all(f(ctx) for f in data) # type: ignore
|
|
|
|
def before_invoke(self, coro):
|
|
"""A decorator that registers a coroutine as a pre-invoke hook.
|
|
A pre-invoke hook is called directly before the command is
|
|
called. This makes it a useful function to set up database
|
|
connections or any type of set up required.
|
|
This pre-invoke hook takes a sole parameter, a :class:`.Context`.
|
|
|
|
.. note::
|
|
|
|
The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are
|
|
only called if all checks and argument parsing procedures pass
|
|
without error. If any check or argument parsing procedures fail
|
|
then the hooks are not called.
|
|
|
|
Parameters
|
|
----------
|
|
coro: :ref:`coroutine <coroutine>`
|
|
The coroutine to register as the pre-invoke hook.
|
|
|
|
Raises
|
|
------
|
|
TypeError
|
|
The coroutine passed is not actually a coroutine.
|
|
"""
|
|
if not asyncio.iscoroutinefunction(coro):
|
|
raise TypeError("The pre-invoke hook must be a coroutine.")
|
|
|
|
self._before_invoke = coro
|
|
return coro
|
|
|
|
def after_invoke(self, coro):
|
|
r"""A decorator that registers a coroutine as a post-invoke hook.
|
|
A post-invoke hook is called directly after the command is
|
|
called. This makes it a useful function to clean-up database
|
|
connections or any type of clean up required.
|
|
This post-invoke hook takes a sole parameter, a :class:`.Context`.
|
|
|
|
.. note::
|
|
|
|
Similar to :meth:`~.Bot.before_invoke`\, this is not called unless
|
|
checks and argument parsing procedures succeed. This hook is,
|
|
however, **always** called regardless of the internal command
|
|
callback raising an error (i.e. :exc:`.CommandInvokeError`\).
|
|
This makes it ideal for clean-up scenarios.
|
|
|
|
Parameters
|
|
-----------
|
|
coro: :ref:`coroutine <coroutine>`
|
|
The coroutine to register as the post-invoke hook.
|
|
|
|
Raises
|
|
-------
|
|
TypeError
|
|
The coroutine passed is not actually a coroutine.
|
|
|
|
"""
|
|
if not asyncio.iscoroutinefunction(coro):
|
|
raise TypeError("The post-invoke hook must be a coroutine.")
|
|
|
|
self._after_invoke = coro
|
|
return coro
|
|
|
|
async def is_owner(self, user: User | Member) -> bool:
|
|
"""|coro|
|
|
|
|
Checks if a :class:`~discord.User` or :class:`~discord.Member` is the owner of
|
|
this bot.
|
|
|
|
If an :attr:`owner_id` is not set, it is fetched automatically
|
|
through the use of :meth:`~.Bot.application_info`, returning
|
|
the application owner, or all non read-only team members.
|
|
|
|
.. versionchanged:: 1.3
|
|
The function also checks if the application is team-owned if
|
|
:attr:`owner_ids` is not set.
|
|
|
|
Parameters
|
|
----------
|
|
user: Union[:class:`.abc.User`, :class:`.member.Member`]
|
|
The user to check for.
|
|
|
|
Returns
|
|
-------
|
|
:class:`bool`
|
|
Whether the user is the owner.
|
|
"""
|
|
|
|
if self.owner_id:
|
|
return user.id == self.owner_id
|
|
elif self.owner_ids:
|
|
return user.id in self.owner_ids
|
|
else:
|
|
app = await self.application_info() # type: ignore
|
|
if app.team:
|
|
self.owner_ids = ids = {
|
|
m.id for m in app.team.members if m.role is not TeamRole.read_only
|
|
}
|
|
return user.id in ids
|
|
else:
|
|
self.owner_id = owner_id = app.owner.id
|
|
return user.id == owner_id
|
|
|
|
|
|
class Bot(BotBase, Client):
|
|
"""Represents a discord bot.
|
|
|
|
This class is a subclass of :class:`discord.Client` and as a result
|
|
anything that you can do with a :class:`discord.Client` you can do with
|
|
this bot.
|
|
|
|
This class also subclasses ``ApplicationCommandMixin`` to provide the functionality
|
|
to manage commands.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
description: :class:`str`
|
|
The content prefixed into the default help message.
|
|
owner_id: Optional[:class:`int`]
|
|
The user ID that owns the bot. If this is not set and is then queried via
|
|
:meth:`.is_owner` then it is fetched automatically using
|
|
:meth:`~.Bot.application_info`, returning the application owner.
|
|
owner_ids: Optional[Collection[:class:`int`]]
|
|
The user IDs that owns the bot. This is similar to :attr:`owner_id`.
|
|
If this is not set and the application is team based, then it is
|
|
fetched automatically using :meth:`~.Bot.application_info`,
|
|
returning all non read-only team members.
|
|
For performance reasons it is recommended to use a :class:`set`
|
|
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
|
|
|
|
.. versionadded:: 1.3
|
|
debug_guilds: Optional[List[:class:`int`]]
|
|
Guild IDs of guilds to use for testing commands.
|
|
The bot will not create any global commands if debug guild IDs are passed.
|
|
|
|
.. versionadded:: 2.0
|
|
auto_sync_commands: :class:`bool`
|
|
Whether to automatically sync slash commands. This will call :meth:`~.Bot.sync_commands` in :func:`discord.on_connect`, and in
|
|
:attr:`.process_application_commands` if the command is not found. Defaults to ``True``.
|
|
|
|
.. versionadded:: 2.0
|
|
default_command_contexts: Collection[:class:`InteractionContextType`]
|
|
The default context types that the bot will use for commands.
|
|
Defaults to a set containing :attr:`InteractionContextType.guild`, :attr:`InteractionContextType.bot_dm`, and
|
|
:attr:`InteractionContextType.private_channel`.
|
|
|
|
.. versionadded:: 2.6
|
|
default_command_integration_types: Collection[:class:`IntegrationType`]]
|
|
The default integration types that the bot will use for commands.
|
|
Defaults to a set containing :attr:`IntegrationType.guild_install`.
|
|
|
|
.. versionadded:: 2.6
|
|
"""
|
|
|
|
@property
|
|
def _bot(self) -> Bot:
|
|
return self
|
|
|
|
|
|
class AutoShardedBot(BotBase, AutoShardedClient):
|
|
"""This is similar to :class:`.Bot` except that it is inherited from
|
|
:class:`discord.AutoShardedClient` instead.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
|
|
@property
|
|
def _bot(self) -> AutoShardedBot:
|
|
return self
|