mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-10-20 23:42:58 +00:00
First pass at supporting user apps
Co-authored-by: red <red@kalab.sk> Co-authored-by: Vioshim <63890837+Vioshim@users.noreply.github.com>
This commit is contained in:
@@ -16,5 +16,6 @@ from .tree import *
|
||||
from .namespace import *
|
||||
from .transformers import *
|
||||
from .translator import *
|
||||
from .installs import *
|
||||
from . import checks as checks
|
||||
from .checks import Cooldown as Cooldown
|
||||
|
@@ -49,6 +49,7 @@ import re
|
||||
from copy import copy as shallow_copy
|
||||
|
||||
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
|
||||
from .installs import AppCommandContext, AppInstallationType
|
||||
from .models import Choice
|
||||
from .transformers import annotation_to_parameter, CommandParameter, NoneType
|
||||
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
|
||||
@@ -65,6 +66,8 @@ if TYPE_CHECKING:
|
||||
from ..abc import Snowflake
|
||||
from .namespace import Namespace
|
||||
from .models import ChoiceT
|
||||
from .tree import CommandTree
|
||||
from .._types import ClientT
|
||||
|
||||
# Generally, these two libraries are supposed to be separate from each other.
|
||||
# However, for type hinting purposes it's unfortunately necessary for one to
|
||||
@@ -87,6 +90,12 @@ __all__ = (
|
||||
'autocomplete',
|
||||
'guilds',
|
||||
'guild_only',
|
||||
'dm_only',
|
||||
'private_channel_only',
|
||||
'allowed_contexts',
|
||||
'guild_install',
|
||||
'user_install',
|
||||
'allowed_installs',
|
||||
'default_permissions',
|
||||
)
|
||||
|
||||
@@ -618,6 +627,16 @@ class Command(Generic[GroupT, P, T]):
|
||||
Whether the command should only be usable in guild contexts.
|
||||
|
||||
Due to a Discord limitation, this does not work on subcommands.
|
||||
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
|
||||
The contexts that the command is allowed to be used in.
|
||||
Overrides ``guild_only`` if this is set.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
|
||||
The installation contexts that the command is allowed to be installed
|
||||
on.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
nsfw: :class:`bool`
|
||||
Whether the command is NSFW and should only work in NSFW channels.
|
||||
|
||||
@@ -638,6 +657,8 @@ class Command(Generic[GroupT, P, T]):
|
||||
nsfw: bool = False,
|
||||
parent: Optional[Group] = None,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
allowed_contexts: Optional[AppCommandContext] = None,
|
||||
allowed_installs: Optional[AppInstallationType] = None,
|
||||
auto_locale_strings: bool = True,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
):
|
||||
@@ -672,6 +693,13 @@ class Command(Generic[GroupT, P, T]):
|
||||
callback, '__discord_app_commands_default_permissions__', None
|
||||
)
|
||||
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
|
||||
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr(
|
||||
callback, '__discord_app_commands_contexts__', None
|
||||
)
|
||||
self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr(
|
||||
callback, '__discord_app_commands_installation_types__', None
|
||||
)
|
||||
|
||||
self.nsfw: bool = nsfw
|
||||
self.extras: Dict[Any, Any] = extras or {}
|
||||
|
||||
@@ -718,8 +746,8 @@ class Command(Generic[GroupT, P, T]):
|
||||
|
||||
return copy
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict(tree)
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
|
||||
@@ -745,7 +773,7 @@ class Command(Generic[GroupT, P, T]):
|
||||
]
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
|
||||
# If we have a parent then our type is a subcommand
|
||||
# Otherwise, the type falls back to the specific command type (e.g. slash command or context menu)
|
||||
option_type = AppCommandType.chat_input.value if self.parent is None else AppCommandOptionType.subcommand.value
|
||||
@@ -760,6 +788,8 @@ class Command(Generic[GroupT, P, T]):
|
||||
base['nsfw'] = self.nsfw
|
||||
base['dm_permission'] = not self.guild_only
|
||||
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
|
||||
base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts)
|
||||
base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs)
|
||||
|
||||
return base
|
||||
|
||||
@@ -1167,6 +1197,16 @@ class ContextMenu:
|
||||
guild_only: :class:`bool`
|
||||
Whether the command should only be usable in guild contexts.
|
||||
Defaults to ``False``.
|
||||
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
|
||||
The contexts that this context menu is allowed to be used in.
|
||||
Overrides ``guild_only`` if set.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
|
||||
The installation contexts that the command is allowed to be installed
|
||||
on.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
nsfw: :class:`bool`
|
||||
Whether the command is NSFW and should only work in NSFW channels.
|
||||
Defaults to ``False``.
|
||||
@@ -1189,6 +1229,8 @@ class ContextMenu:
|
||||
type: AppCommandType = MISSING,
|
||||
nsfw: bool = False,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
allowed_contexts: Optional[AppCommandContext] = None,
|
||||
allowed_installs: Optional[AppInstallationType] = None,
|
||||
auto_locale_strings: bool = True,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
):
|
||||
@@ -1214,6 +1256,12 @@ class ContextMenu:
|
||||
)
|
||||
self.nsfw: bool = nsfw
|
||||
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
|
||||
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr(
|
||||
callback, '__discord_app_commands_contexts__', None
|
||||
)
|
||||
self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr(
|
||||
callback, '__discord_app_commands_installation_types__', None
|
||||
)
|
||||
self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', [])
|
||||
self.extras: Dict[Any, Any] = extras or {}
|
||||
|
||||
@@ -1231,8 +1279,8 @@ class ContextMenu:
|
||||
""":class:`str`: Returns the fully qualified command name."""
|
||||
return self.name
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict(tree)
|
||||
context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
|
||||
if self._locale_name:
|
||||
name_localizations: Dict[str, str] = {}
|
||||
@@ -1244,11 +1292,13 @@ class ContextMenu:
|
||||
base['name_localizations'] = name_localizations
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type.value,
|
||||
'dm_permission': not self.guild_only,
|
||||
'contexts': tree.allowed_contexts._merge_to_array(self.allowed_contexts),
|
||||
'integration_types': tree.allowed_installs._merge_to_array(self.allowed_installs),
|
||||
'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value,
|
||||
'nsfw': self.nsfw,
|
||||
}
|
||||
@@ -1405,6 +1455,16 @@ class Group:
|
||||
Whether the group should only be usable in guild contexts.
|
||||
|
||||
Due to a Discord limitation, this does not work on subcommands.
|
||||
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
|
||||
The contexts that this group is allowed to be used in. Overrides
|
||||
guild_only if set.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
|
||||
The installation contexts that the command is allowed to be installed
|
||||
on.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
nsfw: :class:`bool`
|
||||
Whether the command is NSFW and should only work in NSFW channels.
|
||||
|
||||
@@ -1424,6 +1484,8 @@ class Group:
|
||||
__discord_app_commands_group_locale_description__: Optional[locale_str] = None
|
||||
__discord_app_commands_group_nsfw__: bool = False
|
||||
__discord_app_commands_guild_only__: bool = MISSING
|
||||
__discord_app_commands_contexts__: Optional[AppCommandContext] = MISSING
|
||||
__discord_app_commands_installation_types__: Optional[AppInstallationType] = MISSING
|
||||
__discord_app_commands_default_permissions__: Optional[Permissions] = MISSING
|
||||
__discord_app_commands_has_module__: bool = False
|
||||
__discord_app_commands_error_handler__: Optional[
|
||||
@@ -1492,6 +1554,8 @@ class Group:
|
||||
parent: Optional[Group] = None,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
guild_only: bool = MISSING,
|
||||
allowed_contexts: Optional[AppCommandContext] = MISSING,
|
||||
allowed_installs: Optional[AppInstallationType] = MISSING,
|
||||
nsfw: bool = MISSING,
|
||||
auto_locale_strings: bool = True,
|
||||
default_permissions: Optional[Permissions] = MISSING,
|
||||
@@ -1540,6 +1604,22 @@ class Group:
|
||||
|
||||
self.guild_only: bool = guild_only
|
||||
|
||||
if allowed_contexts is MISSING:
|
||||
if cls.__discord_app_commands_contexts__ is MISSING:
|
||||
allowed_contexts = None
|
||||
else:
|
||||
allowed_contexts = cls.__discord_app_commands_contexts__
|
||||
|
||||
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts
|
||||
|
||||
if allowed_installs is MISSING:
|
||||
if cls.__discord_app_commands_installation_types__ is MISSING:
|
||||
allowed_installs = None
|
||||
else:
|
||||
allowed_installs = cls.__discord_app_commands_installation_types__
|
||||
|
||||
self.allowed_installs: Optional[AppInstallationType] = allowed_installs
|
||||
|
||||
if nsfw is MISSING:
|
||||
nsfw = cls.__discord_app_commands_group_nsfw__
|
||||
|
||||
@@ -1633,8 +1713,8 @@ class Group:
|
||||
|
||||
return copy
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict(tree)
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
|
||||
@@ -1654,10 +1734,10 @@ class Group:
|
||||
|
||||
base['name_localizations'] = name_localizations
|
||||
base['description_localizations'] = description_localizations
|
||||
base['options'] = [await child.get_translated_payload(translator) for child in self._children.values()]
|
||||
base['options'] = [await child.get_translated_payload(tree, translator) for child in self._children.values()]
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
|
||||
# If this has a parent command then it's part of a subcommand group
|
||||
# Otherwise, it's just a regular command
|
||||
option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value
|
||||
@@ -1665,13 +1745,15 @@ class Group:
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'type': option_type,
|
||||
'options': [child.to_dict() for child in self._children.values()],
|
||||
'options': [child.to_dict(tree) for child in self._children.values()],
|
||||
}
|
||||
|
||||
if self.parent is None:
|
||||
base['nsfw'] = self.nsfw
|
||||
base['dm_permission'] = not self.guild_only
|
||||
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
|
||||
base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts)
|
||||
base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs)
|
||||
|
||||
return base
|
||||
|
||||
@@ -2421,8 +2503,16 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
f.guild_only = True
|
||||
allowed_contexts = f.allowed_contexts or AppCommandContext()
|
||||
f.allowed_contexts = allowed_contexts
|
||||
else:
|
||||
f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
|
||||
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_contexts.guild = True
|
||||
|
||||
return f
|
||||
|
||||
# Check if called with parentheses or not
|
||||
@@ -2433,6 +2523,250 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
return inner(func)
|
||||
|
||||
|
||||
def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command can only be used in the context of DMs and group DMs.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
Therefore, there is no error handler called when a command is used within a guild.
|
||||
|
||||
This decorator can be called with or without parentheses.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.private_channel_only()
|
||||
async def my_private_channel_only_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am only available in DMs and GDMs!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
f.guild_only = False
|
||||
allowed_contexts = f.allowed_contexts or AppCommandContext()
|
||||
f.allowed_contexts = allowed_contexts
|
||||
else:
|
||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
|
||||
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_contexts.private_channel = True
|
||||
|
||||
return f
|
||||
|
||||
# Check if called with parentheses or not
|
||||
if func is None:
|
||||
# Called with parentheses
|
||||
return inner
|
||||
else:
|
||||
return inner(func)
|
||||
|
||||
|
||||
def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command can only be used in the context of bot DMs.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
Therefore, there is no error handler called when a command is used within a guild or group DM.
|
||||
|
||||
This decorator can be called with or without parentheses.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.dm_only()
|
||||
async def my_dm_only_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am only available in DMs!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
f.guild_only = False
|
||||
allowed_contexts = f.allowed_contexts or AppCommandContext()
|
||||
f.allowed_contexts = allowed_contexts
|
||||
else:
|
||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
|
||||
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_contexts.dm_channel = True
|
||||
return f
|
||||
|
||||
# Check if called with parentheses or not
|
||||
if func is None:
|
||||
# Called with parentheses
|
||||
return inner
|
||||
else:
|
||||
return inner(func)
|
||||
|
||||
|
||||
def allowed_contexts(
|
||||
guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING
|
||||
) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command can only be used in certain contexts.
|
||||
Valid contexts are guilds, DMs and private channels.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.allowed_contexts(guilds=True, dms=False, private_channels=True)
|
||||
async def my_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am only available in guilds and private channels!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
f.guild_only = False
|
||||
allowed_contexts = f.allowed_contexts or AppCommandContext()
|
||||
f.allowed_contexts = allowed_contexts
|
||||
else:
|
||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
|
||||
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
|
||||
|
||||
if guilds is not MISSING:
|
||||
allowed_contexts.guild = guilds
|
||||
|
||||
if dms is not MISSING:
|
||||
allowed_contexts.dm_channel = dms
|
||||
|
||||
if private_channels is not MISSING:
|
||||
allowed_contexts.private_channel = private_channels
|
||||
|
||||
return f
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command should be installed in guilds.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.guild_install()
|
||||
async def my_guild_install_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am installed in guilds by default!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
allowed_installs = f.allowed_installs or AppInstallationType()
|
||||
f.allowed_installs = allowed_installs
|
||||
else:
|
||||
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
|
||||
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_installs.guild = True
|
||||
|
||||
return f
|
||||
|
||||
# Check if called with parentheses or not
|
||||
if func is None:
|
||||
# Called with parentheses
|
||||
return inner
|
||||
else:
|
||||
return inner(func)
|
||||
|
||||
|
||||
def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command should be installed for users.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.user_install()
|
||||
async def my_user_install_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am installed in users by default!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
allowed_installs = f.allowed_installs or AppInstallationType()
|
||||
f.allowed_installs = allowed_installs
|
||||
else:
|
||||
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
|
||||
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
|
||||
|
||||
allowed_installs.user = True
|
||||
|
||||
return f
|
||||
|
||||
# Check if called with parentheses or not
|
||||
if func is None:
|
||||
# Called with parentheses
|
||||
return inner
|
||||
else:
|
||||
return inner(func)
|
||||
|
||||
|
||||
def allowed_installs(
|
||||
guilds: bool = MISSING,
|
||||
users: bool = MISSING,
|
||||
) -> Union[T, Callable[[T], T]]:
|
||||
"""A decorator that indicates this command should be installed in certain contexts.
|
||||
Valid contexts are guilds and users.
|
||||
|
||||
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
|
||||
|
||||
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@app_commands.command()
|
||||
@app_commands.allowed_installs(guilds=False, users=True)
|
||||
async def my_command(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message('I am installed in users by default!')
|
||||
"""
|
||||
|
||||
def inner(f: T) -> T:
|
||||
if isinstance(f, (Command, Group, ContextMenu)):
|
||||
allowed_installs = f.allowed_installs or AppInstallationType()
|
||||
f.allowed_installs = allowed_installs
|
||||
else:
|
||||
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
|
||||
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
|
||||
|
||||
if guilds is not MISSING:
|
||||
allowed_installs.guild = guilds
|
||||
|
||||
if users is not MISSING:
|
||||
allowed_installs.user = users
|
||||
|
||||
return f
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def default_permissions(**perms: bool) -> Callable[[T], T]:
|
||||
r"""A decorator that sets the default permissions needed to execute this command.
|
||||
|
||||
|
207
discord/app_commands/installs.py
Normal file
207
discord/app_commands/installs.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
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
|
||||
from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence
|
||||
|
||||
__all__ = (
|
||||
'AppInstallationType',
|
||||
'AppCommandContext',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from ..types.interactions import InteractionContextType, InteractionInstallationType
|
||||
|
||||
|
||||
class AppInstallationType:
|
||||
r"""Represents the installation location of an application command.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
guild: Optional[:class:`bool`]
|
||||
Whether the integration is a guild install.
|
||||
user: Optional[:class:`bool`]
|
||||
Whether the integration is a user install.
|
||||
"""
|
||||
|
||||
__slots__ = ('_guild', '_user')
|
||||
|
||||
GUILD: ClassVar[int] = 0
|
||||
USER: ClassVar[int] = 1
|
||||
|
||||
def __init__(self, *, guild: Optional[bool] = None, user: Optional[bool] = None):
|
||||
self._guild: Optional[bool] = guild
|
||||
self._user: Optional[bool] = user
|
||||
|
||||
@property
|
||||
def guild(self) -> bool:
|
||||
""":class:`bool`: Whether the integration is a guild install."""
|
||||
return bool(self._guild)
|
||||
|
||||
@guild.setter
|
||||
def guild(self, value: bool) -> None:
|
||||
self._guild = bool(value)
|
||||
|
||||
@property
|
||||
def user(self) -> bool:
|
||||
""":class:`bool`: Whether the integration is a user install."""
|
||||
return bool(self._user)
|
||||
|
||||
@user.setter
|
||||
def user(self, value: bool) -> None:
|
||||
self._user = bool(value)
|
||||
|
||||
def merge(self, other: AppInstallationType) -> AppInstallationType:
|
||||
# Merging is similar to AllowedMentions where `self` is the base
|
||||
# and the `other` is the override preference
|
||||
guild = self.guild if other.guild is None else other.guild
|
||||
user = self.user if other.user is None else other.user
|
||||
return AppInstallationType(guild=guild, user=user)
|
||||
|
||||
def _is_unset(self) -> bool:
|
||||
return all(x is None for x in (self._guild, self._user))
|
||||
|
||||
def _merge_to_array(self, other: Optional[AppInstallationType]) -> Optional[List[InteractionInstallationType]]:
|
||||
result = self.merge(other) if other is not None else self
|
||||
if result._is_unset():
|
||||
return None
|
||||
return result.to_array()
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls, value: Sequence[InteractionInstallationType]) -> Self:
|
||||
self = cls()
|
||||
for x in value:
|
||||
if x == cls.GUILD:
|
||||
self._guild = True
|
||||
elif x == cls.USER:
|
||||
self._user = True
|
||||
return self
|
||||
|
||||
def to_array(self) -> List[InteractionInstallationType]:
|
||||
values = []
|
||||
if self._guild:
|
||||
values.append(self.GUILD)
|
||||
if self._user:
|
||||
values.append(self.USER)
|
||||
return values
|
||||
|
||||
|
||||
class AppCommandContext:
|
||||
r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
guild: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a guild.
|
||||
dm_channel: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a DM channel.
|
||||
private_channel: Optional[:class:`bool`]
|
||||
Whether the context allows usage in a DM or a GDM channel.
|
||||
"""
|
||||
|
||||
GUILD: ClassVar[int] = 0
|
||||
DM_CHANNEL: ClassVar[int] = 1
|
||||
PRIVATE_CHANNEL: ClassVar[int] = 2
|
||||
|
||||
__slots__ = ('_guild', '_dm_channel', '_private_channel')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
guild: Optional[bool] = None,
|
||||
dm_channel: Optional[bool] = None,
|
||||
private_channel: Optional[bool] = None,
|
||||
):
|
||||
self._guild: Optional[bool] = guild
|
||||
self._dm_channel: Optional[bool] = dm_channel
|
||||
self._private_channel: Optional[bool] = private_channel
|
||||
|
||||
@property
|
||||
def guild(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a guild."""
|
||||
return bool(self._guild)
|
||||
|
||||
@guild.setter
|
||||
def guild(self, value: bool) -> None:
|
||||
self._guild = bool(value)
|
||||
|
||||
@property
|
||||
def dm_channel(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a DM channel."""
|
||||
return bool(self._dm_channel)
|
||||
|
||||
@dm_channel.setter
|
||||
def dm_channel(self, value: bool) -> None:
|
||||
self._dm_channel = bool(value)
|
||||
|
||||
@property
|
||||
def private_channel(self) -> bool:
|
||||
""":class:`bool`: Whether the context allows usage in a DM or a GDM channel."""
|
||||
return bool(self._private_channel)
|
||||
|
||||
@private_channel.setter
|
||||
def private_channel(self, value: bool) -> None:
|
||||
self._private_channel = bool(value)
|
||||
|
||||
def merge(self, other: AppCommandContext) -> AppCommandContext:
|
||||
guild = self.guild if other.guild is None else other.guild
|
||||
dm_channel = self.dm_channel if other.dm_channel is None else other.dm_channel
|
||||
private_channel = self.private_channel if other.private_channel is None else other.private_channel
|
||||
return AppCommandContext(guild=guild, dm_channel=dm_channel, private_channel=private_channel)
|
||||
|
||||
def _is_unset(self) -> bool:
|
||||
return all(x is None for x in (self._guild, self._dm_channel, self._private_channel))
|
||||
|
||||
def _merge_to_array(self, other: Optional[AppCommandContext]) -> Optional[List[InteractionContextType]]:
|
||||
result = self.merge(other) if other is not None else self
|
||||
if result._is_unset():
|
||||
return None
|
||||
return result.to_array()
|
||||
|
||||
@classmethod
|
||||
def _from_value(cls, value: Sequence[InteractionContextType]) -> Self:
|
||||
self = cls()
|
||||
for x in value:
|
||||
if x == cls.GUILD:
|
||||
self._guild = True
|
||||
elif x == cls.DM_CHANNEL:
|
||||
self._dm_channel = True
|
||||
elif x == cls.PRIVATE_CHANNEL:
|
||||
self._private_channel = True
|
||||
return self
|
||||
|
||||
def to_array(self) -> List[InteractionContextType]:
|
||||
values = []
|
||||
if self._guild:
|
||||
values.append(self.GUILD)
|
||||
if self._dm_channel:
|
||||
values.append(self.DM_CHANNEL)
|
||||
if self._private_channel:
|
||||
values.append(self.PRIVATE_CHANNEL)
|
||||
return values
|
@@ -26,9 +26,17 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from .errors import MissingApplicationID
|
||||
from ..flags import AppCommandContext, AppInstallationType
|
||||
from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator
|
||||
from ..permissions import Permissions
|
||||
from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum
|
||||
from ..enums import (
|
||||
AppCommandOptionType,
|
||||
AppCommandType,
|
||||
AppCommandPermissionType,
|
||||
ChannelType,
|
||||
Locale,
|
||||
try_enum,
|
||||
)
|
||||
from ..mixins import Hashable
|
||||
from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING
|
||||
from ..object import Object
|
||||
@@ -160,6 +168,14 @@ class AppCommand(Hashable):
|
||||
The default member permissions that can run this command.
|
||||
dm_permission: :class:`bool`
|
||||
A boolean that indicates whether this command can be run in direct messages.
|
||||
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
|
||||
The contexts that this command is allowed to be used in. Overrides the ``dm_permission`` attribute.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
|
||||
The installation contexts that this command is allowed to be installed in.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
guild_id: Optional[:class:`int`]
|
||||
The ID of the guild this command is registered in. A value of ``None``
|
||||
denotes that it is a global command.
|
||||
@@ -179,6 +195,8 @@ class AppCommand(Hashable):
|
||||
'options',
|
||||
'default_member_permissions',
|
||||
'dm_permission',
|
||||
'allowed_contexts',
|
||||
'allowed_installs',
|
||||
'nsfw',
|
||||
'_state',
|
||||
)
|
||||
@@ -210,6 +228,19 @@ class AppCommand(Hashable):
|
||||
dm_permission = True
|
||||
|
||||
self.dm_permission: bool = dm_permission
|
||||
|
||||
allowed_contexts = data.get('contexts')
|
||||
if allowed_contexts is None:
|
||||
self.allowed_contexts: Optional[AppCommandContext] = None
|
||||
else:
|
||||
self.allowed_contexts = AppCommandContext._from_value(allowed_contexts)
|
||||
|
||||
allowed_installs = data.get('integration_types')
|
||||
if allowed_installs is None:
|
||||
self.allowed_installs: Optional[AppInstallationType] = None
|
||||
else:
|
||||
self.allowed_installs = AppInstallationType._from_value(allowed_installs)
|
||||
|
||||
self.nsfw: bool = data.get('nsfw', False)
|
||||
self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {})
|
||||
self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {})
|
||||
@@ -223,6 +254,8 @@ class AppCommand(Hashable):
|
||||
'description': self.description,
|
||||
'name_localizations': {str(k): v for k, v in self.name_localizations.items()},
|
||||
'description_localizations': {str(k): v for k, v in self.description_localizations.items()},
|
||||
'contexts': self.allowed_contexts.to_array() if self.allowed_contexts is not None else None,
|
||||
'integration_types': self.allowed_installs.to_array() if self.allowed_installs is not None else None,
|
||||
'options': [opt.to_dict() for opt in self.options],
|
||||
} # type: ignore # Type checker does not understand this literal.
|
||||
|
||||
|
@@ -179,7 +179,7 @@ class Namespace:
|
||||
state = interaction._state
|
||||
members = resolved.get('members', {})
|
||||
guild_id = interaction.guild_id
|
||||
guild = state._get_or_create_unavailable_guild(guild_id) if guild_id is not None else None
|
||||
guild = interaction.guild
|
||||
type = AppCommandOptionType.user.value
|
||||
for (user_id, user_data) in resolved.get('users', {}).items():
|
||||
try:
|
||||
@@ -220,7 +220,6 @@ class Namespace:
|
||||
}
|
||||
)
|
||||
|
||||
guild = state._get_guild(guild_id)
|
||||
for (message_id, message_data) in resolved.get('messages', {}).items():
|
||||
channel_id = int(message_data['channel_id'])
|
||||
if guild is None:
|
||||
@@ -232,6 +231,7 @@ class Namespace:
|
||||
|
||||
# Type checker doesn't understand this due to failure to narrow
|
||||
message = Message(state=state, channel=channel, data=message_data) # type: ignore
|
||||
message.guild = guild
|
||||
key = ResolveKey(id=message_id, type=-1)
|
||||
completed[key] = message
|
||||
|
||||
|
@@ -58,6 +58,7 @@ from .errors import (
|
||||
CommandSyncFailure,
|
||||
MissingApplicationID,
|
||||
)
|
||||
from .installs import AppCommandContext, AppInstallationType
|
||||
from .translator import Translator, locale_str
|
||||
from ..errors import ClientException, HTTPException
|
||||
from ..enums import AppCommandType, InteractionType
|
||||
@@ -121,9 +122,26 @@ class CommandTree(Generic[ClientT]):
|
||||
to find the guild-specific ``/ping`` command it will fall back to the global ``/ping`` command.
|
||||
This has the potential to raise more :exc:`~discord.app_commands.CommandSignatureMismatch` errors
|
||||
than usual. Defaults to ``True``.
|
||||
allowed_contexts: :class:`~discord.app_commands.AppCommandContext`
|
||||
The default allowed contexts that applies to all commands in this tree.
|
||||
Note that you can override this on a per command basis.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
allowed_installs: :class:`~discord.app_commands.AppInstallationType`
|
||||
The default allowed install locations that apply to all commands in this tree.
|
||||
Note that you can override this on a per command basis.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
"""
|
||||
|
||||
def __init__(self, client: ClientT, *, fallback_to_global: bool = True):
|
||||
def __init__(
|
||||
self,
|
||||
client: ClientT,
|
||||
*,
|
||||
fallback_to_global: bool = True,
|
||||
allowed_contexts: AppCommandContext = MISSING,
|
||||
allowed_installs: AppInstallationType = MISSING,
|
||||
):
|
||||
self.client: ClientT = client
|
||||
self._http = client.http
|
||||
self._state = client._connection
|
||||
@@ -133,6 +151,8 @@ class CommandTree(Generic[ClientT]):
|
||||
|
||||
self._state._command_tree = self
|
||||
self.fallback_to_global: bool = fallback_to_global
|
||||
self.allowed_contexts = AppCommandContext() if allowed_contexts is MISSING else allowed_contexts
|
||||
self.allowed_installs = AppInstallationType() if allowed_installs is MISSING else allowed_installs
|
||||
self._guild_commands: Dict[int, Dict[str, Union[Command, Group]]] = {}
|
||||
self._global_commands: Dict[str, Union[Command, Group]] = {}
|
||||
# (name, guild_id, command_type): Command
|
||||
@@ -722,7 +742,7 @@ class CommandTree(Generic[ClientT]):
|
||||
else:
|
||||
guild_id = None if guild is None else guild.id
|
||||
value = type.value
|
||||
for ((_, g, t), command) in self._context_menus.items():
|
||||
for (_, g, t), command in self._context_menus.items():
|
||||
if g == guild_id and t == value:
|
||||
yield command
|
||||
|
||||
@@ -1058,9 +1078,9 @@ class CommandTree(Generic[ClientT]):
|
||||
|
||||
translator = self.translator
|
||||
if translator:
|
||||
payload = [await command.get_translated_payload(translator) for command in commands]
|
||||
payload = [await command.get_translated_payload(self, translator) for command in commands]
|
||||
else:
|
||||
payload = [command.to_dict() for command in commands]
|
||||
payload = [command.to_dict(self) for command in commands]
|
||||
|
||||
try:
|
||||
if guild is None:
|
||||
|
Reference in New Issue
Block a user