mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-07-06 18:11:59 +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:
parent
2892401992
commit
2e2f51fd5c
@ -16,5 +16,6 @@ from .tree import *
|
|||||||
from .namespace import *
|
from .namespace import *
|
||||||
from .transformers import *
|
from .transformers import *
|
||||||
from .translator import *
|
from .translator import *
|
||||||
|
from .installs import *
|
||||||
from . import checks as checks
|
from . import checks as checks
|
||||||
from .checks import Cooldown as Cooldown
|
from .checks import Cooldown as Cooldown
|
||||||
|
@ -49,6 +49,7 @@ import re
|
|||||||
from copy import copy as shallow_copy
|
from copy import copy as shallow_copy
|
||||||
|
|
||||||
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
|
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
|
||||||
|
from .installs import AppCommandContext, AppInstallationType
|
||||||
from .models import Choice
|
from .models import Choice
|
||||||
from .transformers import annotation_to_parameter, CommandParameter, NoneType
|
from .transformers import annotation_to_parameter, CommandParameter, NoneType
|
||||||
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
|
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
|
||||||
@ -65,6 +66,8 @@ if TYPE_CHECKING:
|
|||||||
from ..abc import Snowflake
|
from ..abc import Snowflake
|
||||||
from .namespace import Namespace
|
from .namespace import Namespace
|
||||||
from .models import ChoiceT
|
from .models import ChoiceT
|
||||||
|
from .tree import CommandTree
|
||||||
|
from .._types import ClientT
|
||||||
|
|
||||||
# Generally, these two libraries are supposed to be separate from each other.
|
# Generally, these two libraries are supposed to be separate from each other.
|
||||||
# However, for type hinting purposes it's unfortunately necessary for one to
|
# However, for type hinting purposes it's unfortunately necessary for one to
|
||||||
@ -87,6 +90,12 @@ __all__ = (
|
|||||||
'autocomplete',
|
'autocomplete',
|
||||||
'guilds',
|
'guilds',
|
||||||
'guild_only',
|
'guild_only',
|
||||||
|
'dm_only',
|
||||||
|
'private_channel_only',
|
||||||
|
'allowed_contexts',
|
||||||
|
'guild_install',
|
||||||
|
'user_install',
|
||||||
|
'allowed_installs',
|
||||||
'default_permissions',
|
'default_permissions',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -618,6 +627,16 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
Whether the command should only be usable in guild contexts.
|
Whether the command should only be usable in guild contexts.
|
||||||
|
|
||||||
Due to a Discord limitation, this does not work on subcommands.
|
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`
|
nsfw: :class:`bool`
|
||||||
Whether the command is NSFW and should only work in NSFW channels.
|
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,
|
nsfw: bool = False,
|
||||||
parent: Optional[Group] = None,
|
parent: Optional[Group] = None,
|
||||||
guild_ids: Optional[List[int]] = None,
|
guild_ids: Optional[List[int]] = None,
|
||||||
|
allowed_contexts: Optional[AppCommandContext] = None,
|
||||||
|
allowed_installs: Optional[AppInstallationType] = None,
|
||||||
auto_locale_strings: bool = True,
|
auto_locale_strings: bool = True,
|
||||||
extras: Dict[Any, Any] = MISSING,
|
extras: Dict[Any, Any] = MISSING,
|
||||||
):
|
):
|
||||||
@ -672,6 +693,13 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
callback, '__discord_app_commands_default_permissions__', None
|
callback, '__discord_app_commands_default_permissions__', None
|
||||||
)
|
)
|
||||||
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
|
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.nsfw: bool = nsfw
|
||||||
self.extras: Dict[Any, Any] = extras or {}
|
self.extras: Dict[Any, Any] = extras or {}
|
||||||
|
|
||||||
@ -718,8 +746,8 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
|
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||||
base = self.to_dict()
|
base = self.to_dict(tree)
|
||||||
name_localizations: Dict[str, str] = {}
|
name_localizations: Dict[str, str] = {}
|
||||||
description_localizations: Dict[str, str] = {}
|
description_localizations: Dict[str, str] = {}
|
||||||
|
|
||||||
@ -745,7 +773,7 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
]
|
]
|
||||||
return base
|
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
|
# 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)
|
# 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
|
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['nsfw'] = self.nsfw
|
||||||
base['dm_permission'] = not self.guild_only
|
base['dm_permission'] = not self.guild_only
|
||||||
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
|
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
|
return base
|
||||||
|
|
||||||
@ -1167,6 +1197,16 @@ class ContextMenu:
|
|||||||
guild_only: :class:`bool`
|
guild_only: :class:`bool`
|
||||||
Whether the command should only be usable in guild contexts.
|
Whether the command should only be usable in guild contexts.
|
||||||
Defaults to ``False``.
|
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`
|
nsfw: :class:`bool`
|
||||||
Whether the command is NSFW and should only work in NSFW channels.
|
Whether the command is NSFW and should only work in NSFW channels.
|
||||||
Defaults to ``False``.
|
Defaults to ``False``.
|
||||||
@ -1189,6 +1229,8 @@ class ContextMenu:
|
|||||||
type: AppCommandType = MISSING,
|
type: AppCommandType = MISSING,
|
||||||
nsfw: bool = False,
|
nsfw: bool = False,
|
||||||
guild_ids: Optional[List[int]] = None,
|
guild_ids: Optional[List[int]] = None,
|
||||||
|
allowed_contexts: Optional[AppCommandContext] = None,
|
||||||
|
allowed_installs: Optional[AppInstallationType] = None,
|
||||||
auto_locale_strings: bool = True,
|
auto_locale_strings: bool = True,
|
||||||
extras: Dict[Any, Any] = MISSING,
|
extras: Dict[Any, Any] = MISSING,
|
||||||
):
|
):
|
||||||
@ -1214,6 +1256,12 @@ class ContextMenu:
|
|||||||
)
|
)
|
||||||
self.nsfw: bool = nsfw
|
self.nsfw: bool = nsfw
|
||||||
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
|
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.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', [])
|
||||||
self.extras: Dict[Any, Any] = extras or {}
|
self.extras: Dict[Any, Any] = extras or {}
|
||||||
|
|
||||||
@ -1231,8 +1279,8 @@ class ContextMenu:
|
|||||||
""":class:`str`: Returns the fully qualified command name."""
|
""":class:`str`: Returns the fully qualified command name."""
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||||
base = self.to_dict()
|
base = self.to_dict(tree)
|
||||||
context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
|
context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
|
||||||
if self._locale_name:
|
if self._locale_name:
|
||||||
name_localizations: Dict[str, str] = {}
|
name_localizations: Dict[str, str] = {}
|
||||||
@ -1244,11 +1292,13 @@ class ContextMenu:
|
|||||||
base['name_localizations'] = name_localizations
|
base['name_localizations'] = name_localizations
|
||||||
return base
|
return base
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type.value,
|
'type': self.type.value,
|
||||||
'dm_permission': not self.guild_only,
|
'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,
|
'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value,
|
||||||
'nsfw': self.nsfw,
|
'nsfw': self.nsfw,
|
||||||
}
|
}
|
||||||
@ -1405,6 +1455,16 @@ class Group:
|
|||||||
Whether the group should only be usable in guild contexts.
|
Whether the group should only be usable in guild contexts.
|
||||||
|
|
||||||
Due to a Discord limitation, this does not work on subcommands.
|
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`
|
nsfw: :class:`bool`
|
||||||
Whether the command is NSFW and should only work in NSFW channels.
|
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_locale_description__: Optional[locale_str] = None
|
||||||
__discord_app_commands_group_nsfw__: bool = False
|
__discord_app_commands_group_nsfw__: bool = False
|
||||||
__discord_app_commands_guild_only__: bool = MISSING
|
__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_default_permissions__: Optional[Permissions] = MISSING
|
||||||
__discord_app_commands_has_module__: bool = False
|
__discord_app_commands_has_module__: bool = False
|
||||||
__discord_app_commands_error_handler__: Optional[
|
__discord_app_commands_error_handler__: Optional[
|
||||||
@ -1492,6 +1554,8 @@ class Group:
|
|||||||
parent: Optional[Group] = None,
|
parent: Optional[Group] = None,
|
||||||
guild_ids: Optional[List[int]] = None,
|
guild_ids: Optional[List[int]] = None,
|
||||||
guild_only: bool = MISSING,
|
guild_only: bool = MISSING,
|
||||||
|
allowed_contexts: Optional[AppCommandContext] = MISSING,
|
||||||
|
allowed_installs: Optional[AppInstallationType] = MISSING,
|
||||||
nsfw: bool = MISSING,
|
nsfw: bool = MISSING,
|
||||||
auto_locale_strings: bool = True,
|
auto_locale_strings: bool = True,
|
||||||
default_permissions: Optional[Permissions] = MISSING,
|
default_permissions: Optional[Permissions] = MISSING,
|
||||||
@ -1540,6 +1604,22 @@ class Group:
|
|||||||
|
|
||||||
self.guild_only: bool = guild_only
|
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:
|
if nsfw is MISSING:
|
||||||
nsfw = cls.__discord_app_commands_group_nsfw__
|
nsfw = cls.__discord_app_commands_group_nsfw__
|
||||||
|
|
||||||
@ -1633,8 +1713,8 @@ class Group:
|
|||||||
|
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
|
||||||
base = self.to_dict()
|
base = self.to_dict(tree)
|
||||||
name_localizations: Dict[str, str] = {}
|
name_localizations: Dict[str, str] = {}
|
||||||
description_localizations: Dict[str, str] = {}
|
description_localizations: Dict[str, str] = {}
|
||||||
|
|
||||||
@ -1654,10 +1734,10 @@ class Group:
|
|||||||
|
|
||||||
base['name_localizations'] = name_localizations
|
base['name_localizations'] = name_localizations
|
||||||
base['description_localizations'] = description_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
|
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
|
# If this has a parent command then it's part of a subcommand group
|
||||||
# Otherwise, it's just a regular command
|
# Otherwise, it's just a regular command
|
||||||
option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value
|
option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value
|
||||||
@ -1665,13 +1745,15 @@ class Group:
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
'type': option_type,
|
'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:
|
if self.parent is None:
|
||||||
base['nsfw'] = self.nsfw
|
base['nsfw'] = self.nsfw
|
||||||
base['dm_permission'] = not self.guild_only
|
base['dm_permission'] = not self.guild_only
|
||||||
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
|
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
|
return base
|
||||||
|
|
||||||
@ -2421,8 +2503,16 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
|
|||||||
def inner(f: T) -> T:
|
def inner(f: T) -> T:
|
||||||
if isinstance(f, (Command, Group, ContextMenu)):
|
if isinstance(f, (Command, Group, ContextMenu)):
|
||||||
f.guild_only = True
|
f.guild_only = True
|
||||||
|
allowed_contexts = f.allowed_contexts or AppCommandContext()
|
||||||
|
f.allowed_contexts = allowed_contexts
|
||||||
else:
|
else:
|
||||||
f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment
|
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
|
return f
|
||||||
|
|
||||||
# Check if called with parentheses or not
|
# 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)
|
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]:
|
def default_permissions(**perms: bool) -> Callable[[T], T]:
|
||||||
r"""A decorator that sets the default permissions needed to execute this command.
|
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 datetime import datetime
|
||||||
|
|
||||||
from .errors import MissingApplicationID
|
from .errors import MissingApplicationID
|
||||||
|
from ..flags import AppCommandContext, AppInstallationType
|
||||||
from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator
|
from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator
|
||||||
from ..permissions import Permissions
|
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 ..mixins import Hashable
|
||||||
from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING
|
from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING
|
||||||
from ..object import Object
|
from ..object import Object
|
||||||
@ -160,6 +168,14 @@ class AppCommand(Hashable):
|
|||||||
The default member permissions that can run this command.
|
The default member permissions that can run this command.
|
||||||
dm_permission: :class:`bool`
|
dm_permission: :class:`bool`
|
||||||
A boolean that indicates whether this command can be run in direct messages.
|
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`]
|
guild_id: Optional[:class:`int`]
|
||||||
The ID of the guild this command is registered in. A value of ``None``
|
The ID of the guild this command is registered in. A value of ``None``
|
||||||
denotes that it is a global command.
|
denotes that it is a global command.
|
||||||
@ -179,6 +195,8 @@ class AppCommand(Hashable):
|
|||||||
'options',
|
'options',
|
||||||
'default_member_permissions',
|
'default_member_permissions',
|
||||||
'dm_permission',
|
'dm_permission',
|
||||||
|
'allowed_contexts',
|
||||||
|
'allowed_installs',
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'_state',
|
'_state',
|
||||||
)
|
)
|
||||||
@ -210,6 +228,19 @@ class AppCommand(Hashable):
|
|||||||
dm_permission = True
|
dm_permission = True
|
||||||
|
|
||||||
self.dm_permission: bool = dm_permission
|
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.nsfw: bool = data.get('nsfw', False)
|
||||||
self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {})
|
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 {})
|
self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {})
|
||||||
@ -223,6 +254,8 @@ class AppCommand(Hashable):
|
|||||||
'description': self.description,
|
'description': self.description,
|
||||||
'name_localizations': {str(k): v for k, v in self.name_localizations.items()},
|
'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()},
|
'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],
|
'options': [opt.to_dict() for opt in self.options],
|
||||||
} # type: ignore # Type checker does not understand this literal.
|
} # type: ignore # Type checker does not understand this literal.
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ class Namespace:
|
|||||||
state = interaction._state
|
state = interaction._state
|
||||||
members = resolved.get('members', {})
|
members = resolved.get('members', {})
|
||||||
guild_id = interaction.guild_id
|
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
|
type = AppCommandOptionType.user.value
|
||||||
for (user_id, user_data) in resolved.get('users', {}).items():
|
for (user_id, user_data) in resolved.get('users', {}).items():
|
||||||
try:
|
try:
|
||||||
@ -220,7 +220,6 @@ class Namespace:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
guild = state._get_guild(guild_id)
|
|
||||||
for (message_id, message_data) in resolved.get('messages', {}).items():
|
for (message_id, message_data) in resolved.get('messages', {}).items():
|
||||||
channel_id = int(message_data['channel_id'])
|
channel_id = int(message_data['channel_id'])
|
||||||
if guild is None:
|
if guild is None:
|
||||||
@ -232,6 +231,7 @@ class Namespace:
|
|||||||
|
|
||||||
# Type checker doesn't understand this due to failure to narrow
|
# Type checker doesn't understand this due to failure to narrow
|
||||||
message = Message(state=state, channel=channel, data=message_data) # type: ignore
|
message = Message(state=state, channel=channel, data=message_data) # type: ignore
|
||||||
|
message.guild = guild
|
||||||
key = ResolveKey(id=message_id, type=-1)
|
key = ResolveKey(id=message_id, type=-1)
|
||||||
completed[key] = message
|
completed[key] = message
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ from .errors import (
|
|||||||
CommandSyncFailure,
|
CommandSyncFailure,
|
||||||
MissingApplicationID,
|
MissingApplicationID,
|
||||||
)
|
)
|
||||||
|
from .installs import AppCommandContext, AppInstallationType
|
||||||
from .translator import Translator, locale_str
|
from .translator import Translator, locale_str
|
||||||
from ..errors import ClientException, HTTPException
|
from ..errors import ClientException, HTTPException
|
||||||
from ..enums import AppCommandType, InteractionType
|
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.
|
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
|
This has the potential to raise more :exc:`~discord.app_commands.CommandSignatureMismatch` errors
|
||||||
than usual. Defaults to ``True``.
|
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.client: ClientT = client
|
||||||
self._http = client.http
|
self._http = client.http
|
||||||
self._state = client._connection
|
self._state = client._connection
|
||||||
@ -133,6 +151,8 @@ class CommandTree(Generic[ClientT]):
|
|||||||
|
|
||||||
self._state._command_tree = self
|
self._state._command_tree = self
|
||||||
self.fallback_to_global: bool = fallback_to_global
|
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._guild_commands: Dict[int, Dict[str, Union[Command, Group]]] = {}
|
||||||
self._global_commands: Dict[str, Union[Command, Group]] = {}
|
self._global_commands: Dict[str, Union[Command, Group]] = {}
|
||||||
# (name, guild_id, command_type): Command
|
# (name, guild_id, command_type): Command
|
||||||
@ -722,7 +742,7 @@ class CommandTree(Generic[ClientT]):
|
|||||||
else:
|
else:
|
||||||
guild_id = None if guild is None else guild.id
|
guild_id = None if guild is None else guild.id
|
||||||
value = type.value
|
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:
|
if g == guild_id and t == value:
|
||||||
yield command
|
yield command
|
||||||
|
|
||||||
@ -1058,9 +1078,9 @@ class CommandTree(Generic[ClientT]):
|
|||||||
|
|
||||||
translator = self.translator
|
translator = self.translator
|
||||||
if 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:
|
else:
|
||||||
payload = [command.to_dict() for command in commands]
|
payload = [command.to_dict(self) for command in commands]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guild is None:
|
if guild is None:
|
||||||
|
@ -2916,22 +2916,21 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable):
|
|||||||
The user you are participating with in the direct message channel.
|
The user you are participating with in the direct message channel.
|
||||||
If this channel is received through the gateway, the recipient information
|
If this channel is received through the gateway, the recipient information
|
||||||
may not be always available.
|
may not be always available.
|
||||||
|
recipients: List[:class:`User`]
|
||||||
|
The users you are participating with in the DM channel.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
me: :class:`ClientUser`
|
me: :class:`ClientUser`
|
||||||
The user presenting yourself.
|
The user presenting yourself.
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
The direct message channel ID.
|
The direct message channel ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('id', 'recipient', 'me', '_state')
|
__slots__ = ('id', 'recipients', 'me', '_state')
|
||||||
|
|
||||||
def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload):
|
def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload):
|
||||||
self._state: ConnectionState = state
|
self._state: ConnectionState = state
|
||||||
self.recipient: Optional[User] = None
|
self.recipients: List[User] = [state.store_user(u) for u in data.get('recipients', [])]
|
||||||
|
|
||||||
recipients = data.get('recipients')
|
|
||||||
if recipients is not None:
|
|
||||||
self.recipient = state.store_user(recipients[0])
|
|
||||||
|
|
||||||
self.me: ClientUser = me
|
self.me: ClientUser = me
|
||||||
self.id: int = int(data['id'])
|
self.id: int = int(data['id'])
|
||||||
|
|
||||||
@ -2951,11 +2950,17 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable):
|
|||||||
self = cls.__new__(cls)
|
self = cls.__new__(cls)
|
||||||
self._state = state
|
self._state = state
|
||||||
self.id = channel_id
|
self.id = channel_id
|
||||||
self.recipient = None
|
self.recipients = []
|
||||||
# state.user won't be None here
|
# state.user won't be None here
|
||||||
self.me = state.user # type: ignore
|
self.me = state.user # type: ignore
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipient(self) -> Optional[User]:
|
||||||
|
if self.recipients:
|
||||||
|
return self.recipients[0]
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> Literal[ChannelType.private]:
|
def type(self) -> Literal[ChannelType.private]:
|
||||||
""":class:`ChannelType`: The channel's Discord type."""
|
""":class:`ChannelType`: The channel's Discord type."""
|
||||||
|
@ -166,6 +166,8 @@ class BotBase(GroupMixin[None]):
|
|||||||
help_command: Optional[HelpCommand] = _default,
|
help_command: Optional[HelpCommand] = _default,
|
||||||
tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree,
|
tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
|
allowed_contexts: app_commands.AppCommandContext = MISSING,
|
||||||
|
allowed_installs: app_commands.AppInstallationType = MISSING,
|
||||||
intents: discord.Intents,
|
intents: discord.Intents,
|
||||||
**options: Any,
|
**options: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -174,6 +176,11 @@ class BotBase(GroupMixin[None]):
|
|||||||
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
||||||
# Self doesn't have the ClientT bound, but since this is a mixin it technically does
|
# Self doesn't have the ClientT bound, but since this is a mixin it technically does
|
||||||
self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore
|
self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore
|
||||||
|
if allowed_contexts is not MISSING:
|
||||||
|
self.__tree.allowed_contexts = allowed_contexts
|
||||||
|
if allowed_installs is not MISSING:
|
||||||
|
self.__tree.allowed_installs = allowed_installs
|
||||||
|
|
||||||
self.__cogs: Dict[str, Cog] = {}
|
self.__cogs: Dict[str, Cog] = {}
|
||||||
self.__extensions: Dict[str, types.ModuleType] = {}
|
self.__extensions: Dict[str, types.ModuleType] = {}
|
||||||
self._checks: List[UserCheck] = []
|
self._checks: List[UserCheck] = []
|
||||||
@ -521,7 +528,6 @@ class BotBase(GroupMixin[None]):
|
|||||||
elif self.owner_ids:
|
elif self.owner_ids:
|
||||||
return user.id in self.owner_ids
|
return user.id in self.owner_ids
|
||||||
else:
|
else:
|
||||||
|
|
||||||
app: discord.AppInfo = await self.application_info() # type: ignore
|
app: discord.AppInfo = await self.application_info() # type: ignore
|
||||||
if app.team:
|
if app.team:
|
||||||
self.owner_ids = ids = {
|
self.owner_ids = ids = {
|
||||||
@ -1489,6 +1495,20 @@ class Bot(BotBase, discord.Client):
|
|||||||
The type of application command tree to use. Defaults to :class:`~discord.app_commands.CommandTree`.
|
The type of application command tree to use. Defaults to :class:`~discord.app_commands.CommandTree`.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
|
allowed_contexts: :class:`~discord.app_commands.AppCommandContext`
|
||||||
|
The default allowed contexts that applies to all application commands
|
||||||
|
in the application command 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 application commands
|
||||||
|
in the application command tree.
|
||||||
|
|
||||||
|
Note that you can override this on a per command basis.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
@ -318,6 +318,8 @@ class Cog(metaclass=CogMeta):
|
|||||||
parent=None,
|
parent=None,
|
||||||
guild_ids=getattr(cls, '__discord_app_commands_default_guilds__', None),
|
guild_ids=getattr(cls, '__discord_app_commands_default_guilds__', None),
|
||||||
guild_only=getattr(cls, '__discord_app_commands_guild_only__', False),
|
guild_only=getattr(cls, '__discord_app_commands_guild_only__', False),
|
||||||
|
allowed_contexts=getattr(cls, '__discord_app_commands_contexts__', None),
|
||||||
|
allowed_installs=getattr(cls, '__discord_app_commands_installation_types__', None),
|
||||||
default_permissions=getattr(cls, '__discord_app_commands_default_permissions__', None),
|
default_permissions=getattr(cls, '__discord_app_commands_default_permissions__', None),
|
||||||
extras=cls.__cog_group_extras__,
|
extras=cls.__cog_group_extras__,
|
||||||
)
|
)
|
||||||
|
@ -472,7 +472,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
if self.channel.type is ChannelType.private:
|
if self.interaction is None and self.channel.type is ChannelType.private:
|
||||||
return Permissions._dm_permissions()
|
return Permissions._dm_permissions()
|
||||||
if not self.interaction:
|
if not self.interaction:
|
||||||
# channel and author will always match relevant types here
|
# channel and author will always match relevant types here
|
||||||
@ -506,7 +506,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
channel = self.channel
|
channel = self.channel
|
||||||
if channel.type == ChannelType.private:
|
if self.interaction is None and channel.type == ChannelType.private:
|
||||||
return Permissions._dm_permissions()
|
return Permissions._dm_permissions()
|
||||||
if not self.interaction:
|
if not self.interaction:
|
||||||
# channel and me will always match relevant types here
|
# channel and me will always match relevant types here
|
||||||
|
@ -653,6 +653,8 @@ class HybridGroup(Group[CogT, P, T]):
|
|||||||
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False)
|
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False)
|
||||||
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
|
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
|
||||||
nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__', False)
|
nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__', False)
|
||||||
|
contexts = getattr(self.callback, '__discord_app_commands_contexts__', MISSING)
|
||||||
|
installs = getattr(self.callback, '__discord_app_commands_installation_types__', MISSING)
|
||||||
self.app_command = app_commands.Group(
|
self.app_command = app_commands.Group(
|
||||||
name=self._locale_name or self.name,
|
name=self._locale_name or self.name,
|
||||||
description=self._locale_description or self.description or self.short_doc or '…',
|
description=self._locale_description or self.description or self.short_doc or '…',
|
||||||
@ -660,6 +662,8 @@ class HybridGroup(Group[CogT, P, T]):
|
|||||||
guild_only=guild_only,
|
guild_only=guild_only,
|
||||||
default_permissions=default_permissions,
|
default_permissions=default_permissions,
|
||||||
nsfw=nsfw,
|
nsfw=nsfw,
|
||||||
|
allowed_installs=installs,
|
||||||
|
allowed_contexts=contexts,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This prevents the group from re-adding the command at __init__
|
# This prevents the group from re-adding the command at __init__
|
||||||
|
176
discord/flags.py
176
discord/flags.py
@ -58,8 +58,10 @@ __all__ = (
|
|||||||
'ChannelFlags',
|
'ChannelFlags',
|
||||||
'AutoModPresets',
|
'AutoModPresets',
|
||||||
'MemberFlags',
|
'MemberFlags',
|
||||||
|
'AppCommandContext',
|
||||||
'AttachmentFlags',
|
'AttachmentFlags',
|
||||||
'RoleFlags',
|
'RoleFlags',
|
||||||
|
'AppInstallationType',
|
||||||
'SKUFlags',
|
'SKUFlags',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1660,8 +1662,24 @@ class ArrayFlags(BaseFlags):
|
|||||||
self.value = reduce(or_, map((1).__lshift__, value), 0) >> 1
|
self.value = reduce(or_, map((1).__lshift__, value), 0) >> 1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def to_array(self) -> List[int]:
|
def to_array(self, *, offset: int = 0) -> List[int]:
|
||||||
return [i + 1 for i in range(self.value.bit_length()) if self.value & (1 << i)]
|
return [i + offset for i in range(self.value.bit_length()) if self.value & (1 << i)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls: Type[Self]) -> Self:
|
||||||
|
"""A factory method that creates an instance of ArrayFlags with everything enabled."""
|
||||||
|
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||||
|
value = (1 << bits) - 1
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def none(cls: Type[Self]) -> Self:
|
||||||
|
"""A factory method that creates an instance of ArrayFlags with everything disabled."""
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self.value = self.DEFAULT_VALUE
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
@fill_with_flags()
|
@fill_with_flags()
|
||||||
@ -1728,6 +1746,9 @@ class AutoModPresets(ArrayFlags):
|
|||||||
rather than using this raw value.
|
rather than using this raw value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def to_array(self) -> List[int]:
|
||||||
|
return super().to_array(offset=1)
|
||||||
|
|
||||||
@flag_value
|
@flag_value
|
||||||
def profanity(self):
|
def profanity(self):
|
||||||
""":class:`bool`: Whether to use the preset profanity filter."""
|
""":class:`bool`: Whether to use the preset profanity filter."""
|
||||||
@ -1743,21 +1764,144 @@ class AutoModPresets(ArrayFlags):
|
|||||||
""":class:`bool`: Whether to use the preset slurs filter."""
|
""":class:`bool`: Whether to use the preset slurs filter."""
|
||||||
return 1 << 2
|
return 1 << 2
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all(cls: Type[Self]) -> Self:
|
|
||||||
"""A factory method that creates a :class:`AutoModPresets` with everything enabled."""
|
|
||||||
bits = max(cls.VALID_FLAGS.values()).bit_length()
|
|
||||||
value = (1 << bits) - 1
|
|
||||||
self = cls.__new__(cls)
|
|
||||||
self.value = value
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@fill_with_flags()
|
||||||
def none(cls: Type[Self]) -> Self:
|
class AppCommandContext(ArrayFlags):
|
||||||
"""A factory method that creates a :class:`AutoModPresets` with everything disabled."""
|
r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context.
|
||||||
self = cls.__new__(cls)
|
|
||||||
self.value = self.DEFAULT_VALUE
|
.. versionadded:: 2.4
|
||||||
return self
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two AppCommandContext flags are equal.
|
||||||
|
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two AppCommandContext flags are not equal.
|
||||||
|
|
||||||
|
.. describe:: x | y, x |= y
|
||||||
|
|
||||||
|
Returns an AppCommandContext instance with all enabled flags from
|
||||||
|
both x and y.
|
||||||
|
|
||||||
|
.. describe:: x & y, x &= y
|
||||||
|
|
||||||
|
Returns an AppCommandContext instance with only flags enabled on
|
||||||
|
both x and y.
|
||||||
|
|
||||||
|
.. describe:: x ^ y, x ^= y
|
||||||
|
|
||||||
|
Returns an AppCommandContext instance with only flags enabled on
|
||||||
|
only one of x or y, not on both.
|
||||||
|
|
||||||
|
.. describe:: ~x
|
||||||
|
|
||||||
|
Returns an AppCommandContext instance with all flags inverted from x
|
||||||
|
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Return the flag's hash.
|
||||||
|
.. describe:: iter(x)
|
||||||
|
|
||||||
|
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||||
|
to be, for example, constructed as a dict or a list of pairs.
|
||||||
|
Note that aliases are not shown.
|
||||||
|
|
||||||
|
.. describe:: bool(b)
|
||||||
|
|
||||||
|
Returns whether any flag is set to ``True``.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
value: :class:`int`
|
||||||
|
The raw value. You should query flags via the properties
|
||||||
|
rather than using this raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_VALUE = 3
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def guild(self):
|
||||||
|
""":class:`bool`: Whether the context allows usage in a guild."""
|
||||||
|
return 1 << 0
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def dm_channel(self):
|
||||||
|
""":class:`bool`: Whether the context allows usage in a DM channel."""
|
||||||
|
return 1 << 1
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def private_channel(self):
|
||||||
|
""":class:`bool`: Whether the context allows usage in a DM or a GDM channel."""
|
||||||
|
return 1 << 2
|
||||||
|
|
||||||
|
|
||||||
|
@fill_with_flags()
|
||||||
|
class AppInstallationType(ArrayFlags):
|
||||||
|
r"""Represents the installation location of an application command.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
|
||||||
|
.. container:: operations
|
||||||
|
|
||||||
|
.. describe:: x == y
|
||||||
|
|
||||||
|
Checks if two AppInstallationType flags are equal.
|
||||||
|
|
||||||
|
.. describe:: x != y
|
||||||
|
|
||||||
|
Checks if two AppInstallationType flags are not equal.
|
||||||
|
|
||||||
|
.. describe:: x | y, x |= y
|
||||||
|
|
||||||
|
Returns an AppInstallationType instance with all enabled flags from
|
||||||
|
both x and y.
|
||||||
|
|
||||||
|
.. describe:: x & y, x &= y
|
||||||
|
|
||||||
|
Returns an AppInstallationType instance with only flags enabled on
|
||||||
|
both x and y.
|
||||||
|
|
||||||
|
.. describe:: x ^ y, x ^= y
|
||||||
|
|
||||||
|
Returns an AppInstallationType instance with only flags enabled on
|
||||||
|
only one of x or y, not on both.
|
||||||
|
|
||||||
|
.. describe:: ~x
|
||||||
|
|
||||||
|
Returns an AppInstallationType instance with all flags inverted from x
|
||||||
|
|
||||||
|
.. describe:: hash(x)
|
||||||
|
|
||||||
|
Return the flag's hash.
|
||||||
|
.. describe:: iter(x)
|
||||||
|
|
||||||
|
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||||
|
to be, for example, constructed as a dict or a list of pairs.
|
||||||
|
Note that aliases are not shown.
|
||||||
|
|
||||||
|
.. describe:: bool(b)
|
||||||
|
|
||||||
|
Returns whether any flag is set to ``True``.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
value: :class:`int`
|
||||||
|
The raw value. You should query flags via the properties
|
||||||
|
rather than using this raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def guild(self):
|
||||||
|
""":class:`bool`: Whether the integration is a guild install."""
|
||||||
|
return 1 << 0
|
||||||
|
|
||||||
|
@flag_value
|
||||||
|
def user(self):
|
||||||
|
""":class:`bool`: Whether the integration is a user install."""
|
||||||
|
return 1 << 1
|
||||||
|
|
||||||
|
|
||||||
@fill_with_flags()
|
@fill_with_flags()
|
||||||
|
@ -455,8 +455,11 @@ class Guild(Hashable):
|
|||||||
return role
|
return role
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_unavailable(cls, *, state: ConnectionState, guild_id: int) -> Guild:
|
def _create_unavailable(cls, *, state: ConnectionState, guild_id: int, data: Optional[Dict[str, Any]]) -> Guild:
|
||||||
return cls(state=state, data={'id': guild_id, 'unavailable': True}) # type: ignore
|
if data is None:
|
||||||
|
data = {'unavailable': True}
|
||||||
|
data.update(id=guild_id)
|
||||||
|
return cls(state=state, data=data) # type: ignore
|
||||||
|
|
||||||
def _from_data(self, guild: GuildPayload) -> None:
|
def _from_data(self, guild: GuildPayload) -> None:
|
||||||
try:
|
try:
|
||||||
|
@ -45,6 +45,7 @@ from .message import Message, Attachment
|
|||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
from .http import handle_message_parameters
|
from .http import handle_message_parameters
|
||||||
from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params
|
from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params
|
||||||
|
from .app_commands.installs import AppCommandContext
|
||||||
from .app_commands.namespace import Namespace
|
from .app_commands.namespace import Namespace
|
||||||
from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation
|
from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation
|
||||||
from .channel import _threaded_channel_factory
|
from .channel import _threaded_channel_factory
|
||||||
@ -64,6 +65,7 @@ if TYPE_CHECKING:
|
|||||||
from .types.webhook import (
|
from .types.webhook import (
|
||||||
Webhook as WebhookPayload,
|
Webhook as WebhookPayload,
|
||||||
)
|
)
|
||||||
|
from .types.snowflake import Snowflake
|
||||||
from .guild import Guild
|
from .guild import Guild
|
||||||
from .state import ConnectionState
|
from .state import ConnectionState
|
||||||
from .file import File
|
from .file import File
|
||||||
@ -139,6 +141,10 @@ class Interaction(Generic[ClientT]):
|
|||||||
command_failed: :class:`bool`
|
command_failed: :class:`bool`
|
||||||
Whether the command associated with this interaction failed to execute.
|
Whether the command associated with this interaction failed to execute.
|
||||||
This includes checks and execution.
|
This includes checks and execution.
|
||||||
|
context: :class:`.AppCommandContext`
|
||||||
|
The context of the interaction.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__: Tuple[str, ...] = (
|
__slots__: Tuple[str, ...] = (
|
||||||
@ -157,6 +163,8 @@ class Interaction(Generic[ClientT]):
|
|||||||
'command_failed',
|
'command_failed',
|
||||||
'entitlement_sku_ids',
|
'entitlement_sku_ids',
|
||||||
'entitlements',
|
'entitlements',
|
||||||
|
"context",
|
||||||
|
'_integration_owners',
|
||||||
'_permissions',
|
'_permissions',
|
||||||
'_app_permissions',
|
'_app_permissions',
|
||||||
'_state',
|
'_state',
|
||||||
@ -194,6 +202,14 @@ class Interaction(Generic[ClientT]):
|
|||||||
self.application_id: int = int(data['application_id'])
|
self.application_id: int = int(data['application_id'])
|
||||||
self.entitlement_sku_ids: List[int] = [int(x) for x in data.get('entitlement_skus', []) or []]
|
self.entitlement_sku_ids: List[int] = [int(x) for x in data.get('entitlement_skus', []) or []]
|
||||||
self.entitlements: List[Entitlement] = [Entitlement(self._state, x) for x in data.get('entitlements', [])]
|
self.entitlements: List[Entitlement] = [Entitlement(self._state, x) for x in data.get('entitlements', [])]
|
||||||
|
# This is not entirely useful currently, unsure how to expose it in a way that it is.
|
||||||
|
self._integration_owners: Dict[int, Snowflake] = {
|
||||||
|
int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items()
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.context = AppCommandContext._from_value([data['context']])
|
||||||
|
except KeyError:
|
||||||
|
self.context = AppCommandContext()
|
||||||
|
|
||||||
self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
|
self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
|
||||||
self.guild_locale: Optional[Locale]
|
self.guild_locale: Optional[Locale]
|
||||||
@ -204,7 +220,10 @@ class Interaction(Generic[ClientT]):
|
|||||||
|
|
||||||
guild = None
|
guild = None
|
||||||
if self.guild_id:
|
if self.guild_id:
|
||||||
guild = self._state._get_or_create_unavailable_guild(self.guild_id)
|
# The data type is a TypedDict but it doesn't narrow to Dict[str, Any] properly
|
||||||
|
guild = self._state._get_or_create_unavailable_guild(self.guild_id, data=data.get('guild')) # type: ignore
|
||||||
|
if guild.me is None and self._client.user is not None:
|
||||||
|
guild._add_member(Member._from_client_user(user=self._client.user, guild=guild, state=self._state))
|
||||||
|
|
||||||
raw_channel = data.get('channel', {})
|
raw_channel = data.get('channel', {})
|
||||||
channel_id = utils._get_as_snowflake(raw_channel, 'id')
|
channel_id = utils._get_as_snowflake(raw_channel, 'id')
|
||||||
@ -371,6 +390,22 @@ class Interaction(Generic[ClientT]):
|
|||||||
""":class:`bool`: Returns ``True`` if the interaction is expired."""
|
""":class:`bool`: Returns ``True`` if the interaction is expired."""
|
||||||
return utils.utcnow() >= self.expires_at
|
return utils.utcnow() >= self.expires_at
|
||||||
|
|
||||||
|
def is_guild_integration(self) -> bool:
|
||||||
|
""":class:`bool`: Returns ``True`` if the interaction is a guild integration.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
"""
|
||||||
|
if self.guild_id:
|
||||||
|
return self.guild_id == self._integration_owners.get(0)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_user_integration(self) -> bool:
|
||||||
|
""":class:`bool`: Returns ``True`` if the interaction is a user integration.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
"""
|
||||||
|
return self.user.id == self._integration_owners.get(1)
|
||||||
|
|
||||||
async def original_response(self) -> InteractionMessage:
|
async def original_response(self) -> InteractionMessage:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ import discord.abc
|
|||||||
from . import utils
|
from . import utils
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
from .utils import MISSING
|
from .utils import MISSING
|
||||||
from .user import BaseUser, User, _UserTag
|
from .user import BaseUser, ClientUser, User, _UserTag
|
||||||
from .activity import create_activity, ActivityTypes
|
from .activity import create_activity, ActivityTypes
|
||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
from .enums import Status, try_enum
|
from .enums import Status, try_enum
|
||||||
@ -392,6 +392,15 @@ class Member(discord.abc.Messageable, _UserTag):
|
|||||||
data['user'] = author._to_minimal_user_json() # type: ignore
|
data['user'] = author._to_minimal_user_json() # type: ignore
|
||||||
return cls(data=data, guild=message.guild, state=message._state) # type: ignore
|
return cls(data=data, guild=message.guild, state=message._state) # type: ignore
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_client_user(cls, *, user: ClientUser, guild: Guild, state: ConnectionState) -> Self:
|
||||||
|
data = {
|
||||||
|
'roles': [],
|
||||||
|
'user': user._to_minimal_user_json(),
|
||||||
|
'flags': 0,
|
||||||
|
}
|
||||||
|
return cls(data=data, guild=guild, state=state) # type: ignore
|
||||||
|
|
||||||
def _update_from_message(self, data: MemberPayload) -> None:
|
def _update_from_message(self, data: MemberPayload) -> None:
|
||||||
self.joined_at = utils.parse_time(data.get('joined_at'))
|
self.joined_at = utils.parse_time(data.get('joined_at'))
|
||||||
self.premium_since = utils.parse_time(data.get('premium_since'))
|
self.premium_since = utils.parse_time(data.get('premium_since'))
|
||||||
|
@ -429,8 +429,8 @@ class ConnectionState(Generic[ClientT]):
|
|||||||
# the keys of self._guilds are ints
|
# the keys of self._guilds are ints
|
||||||
return self._guilds.get(guild_id) # type: ignore
|
return self._guilds.get(guild_id) # type: ignore
|
||||||
|
|
||||||
def _get_or_create_unavailable_guild(self, guild_id: int) -> Guild:
|
def _get_or_create_unavailable_guild(self, guild_id: int, *, data: Optional[Dict[str, Any]] = None) -> Guild:
|
||||||
return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id)
|
return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id, data=data)
|
||||||
|
|
||||||
def _add_guild(self, guild: Guild) -> None:
|
def _add_guild(self, guild: Guild) -> None:
|
||||||
self._guilds[guild.id] = guild
|
self._guilds[guild.id] = guild
|
||||||
@ -1592,7 +1592,8 @@ class ConnectionState(Generic[ClientT]):
|
|||||||
|
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
if isinstance(channel, DMChannel):
|
if isinstance(channel, DMChannel):
|
||||||
channel.recipient = raw.user
|
if raw.user is not None and raw.user not in channel.recipients:
|
||||||
|
channel.recipients.append(raw.user)
|
||||||
elif guild is not None:
|
elif guild is not None:
|
||||||
raw.user = guild.get_member(raw.user_id)
|
raw.user = guild.get_member(raw.user_id)
|
||||||
|
|
||||||
|
@ -29,9 +29,11 @@ from typing_extensions import NotRequired, Required
|
|||||||
|
|
||||||
from .channel import ChannelType
|
from .channel import ChannelType
|
||||||
from .snowflake import Snowflake
|
from .snowflake import Snowflake
|
||||||
|
from .interactions import InteractionContextType
|
||||||
|
|
||||||
ApplicationCommandType = Literal[1, 2, 3]
|
ApplicationCommandType = Literal[1, 2, 3]
|
||||||
ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||||
|
ApplicationIntegrationType = Literal[0, 1]
|
||||||
|
|
||||||
|
|
||||||
class _BaseApplicationCommandOption(TypedDict):
|
class _BaseApplicationCommandOption(TypedDict):
|
||||||
@ -141,6 +143,8 @@ class _BaseApplicationCommand(TypedDict):
|
|||||||
id: Snowflake
|
id: Snowflake
|
||||||
application_id: Snowflake
|
application_id: Snowflake
|
||||||
name: str
|
name: str
|
||||||
|
contexts: List[InteractionContextType]
|
||||||
|
integration_types: List[ApplicationIntegrationType]
|
||||||
dm_permission: NotRequired[Optional[bool]]
|
dm_permission: NotRequired[Optional[bool]]
|
||||||
default_member_permissions: NotRequired[Optional[str]]
|
default_member_permissions: NotRequired[Optional[str]]
|
||||||
nsfw: NotRequired[bool]
|
nsfw: NotRequired[bool]
|
||||||
|
@ -35,12 +35,15 @@ from .message import Attachment
|
|||||||
from .role import Role
|
from .role import Role
|
||||||
from .snowflake import Snowflake
|
from .snowflake import Snowflake
|
||||||
from .user import User
|
from .user import User
|
||||||
|
from .guild import GuildFeature
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
|
|
||||||
InteractionType = Literal[1, 2, 3, 4, 5]
|
InteractionType = Literal[1, 2, 3, 4, 5]
|
||||||
|
InteractionContextType = Literal[0, 1, 2]
|
||||||
|
InteractionInstallationType = Literal[0, 1]
|
||||||
|
|
||||||
|
|
||||||
class _BasePartialChannel(TypedDict):
|
class _BasePartialChannel(TypedDict):
|
||||||
@ -68,6 +71,12 @@ class ResolvedData(TypedDict, total=False):
|
|||||||
attachments: Dict[str, Attachment]
|
attachments: Dict[str, Attachment]
|
||||||
|
|
||||||
|
|
||||||
|
class PartialInteractionGuild(TypedDict):
|
||||||
|
id: Snowflake
|
||||||
|
locale: str
|
||||||
|
features: List[GuildFeature]
|
||||||
|
|
||||||
|
|
||||||
class _BaseApplicationCommandInteractionDataOption(TypedDict):
|
class _BaseApplicationCommandInteractionDataOption(TypedDict):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
@ -204,6 +213,7 @@ class _BaseInteraction(TypedDict):
|
|||||||
token: str
|
token: str
|
||||||
version: Literal[1]
|
version: Literal[1]
|
||||||
guild_id: NotRequired[Snowflake]
|
guild_id: NotRequired[Snowflake]
|
||||||
|
guild: NotRequired[PartialInteractionGuild]
|
||||||
channel_id: NotRequired[Snowflake]
|
channel_id: NotRequired[Snowflake]
|
||||||
channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel]
|
channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel]
|
||||||
app_permissions: NotRequired[str]
|
app_permissions: NotRequired[str]
|
||||||
@ -211,6 +221,8 @@ class _BaseInteraction(TypedDict):
|
|||||||
guild_locale: NotRequired[str]
|
guild_locale: NotRequired[str]
|
||||||
entitlement_sku_ids: NotRequired[List[Snowflake]]
|
entitlement_sku_ids: NotRequired[List[Snowflake]]
|
||||||
entitlements: NotRequired[List[Entitlement]]
|
entitlements: NotRequired[List[Entitlement]]
|
||||||
|
authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake]
|
||||||
|
context: NotRequired[InteractionContextType]
|
||||||
|
|
||||||
|
|
||||||
class PingInteraction(_BaseInteraction):
|
class PingInteraction(_BaseInteraction):
|
||||||
|
@ -129,6 +129,22 @@ AppCommandPermissions
|
|||||||
.. autoclass:: discord.app_commands.AppCommandPermissions()
|
.. autoclass:: discord.app_commands.AppCommandPermissions()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
AppCommandContext
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. attributetable:: discord.app_commands.AppCommandContext
|
||||||
|
|
||||||
|
.. autoclass:: discord.app_commands.AppCommandContext
|
||||||
|
:members:
|
||||||
|
|
||||||
|
AppInstallationType
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. attributetable:: discord.app_commands.AppInstallationType
|
||||||
|
|
||||||
|
.. autoclass:: discord.app_commands.AppInstallationType
|
||||||
|
:members:
|
||||||
|
|
||||||
GuildAppCommandPermissions
|
GuildAppCommandPermissions
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -642,6 +658,24 @@ Decorators
|
|||||||
.. autofunction:: discord.app_commands.guild_only
|
.. autofunction:: discord.app_commands.guild_only
|
||||||
:decorator:
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.dm_only
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.private_channel_only
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.allowed_contexts
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.user_install
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.guild_install
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
.. autofunction:: discord.app_commands.allowed_installs
|
||||||
|
:decorator:
|
||||||
|
|
||||||
.. autofunction:: discord.app_commands.default_permissions
|
.. autofunction:: discord.app_commands.default_permissions
|
||||||
:decorator:
|
:decorator:
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user