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:
Danny
2024-05-04 23:25:01 -04:00
committed by GitHub
parent 2892401992
commit 2e2f51fd5c
19 changed files with 920 additions and 52 deletions

View File

@@ -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

View File

@@ -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.

View 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

View File

@@ -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.

View File

@@ -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

View File

@@ -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: