-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support app command contexts #9406
Closed
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
8ec73fa
@dm_only and @private_only app command decorators
RedKinda 09b4241
apply suggestions, docs
RedKinda 7cb1b4e
make AppCommandContext an ArrayFlags
RedKinda 646e7da
rename allow_contexts attrs to restrict_contexts
RedKinda 8224f2d
rename private_only to private_channel_only
RedKinda 08a12f8
add offset args in ArrayFlags.to_array()
RedKinda 4803960
remove useless override
RedKinda aaccdfa
apply suggestions
RedKinda b095d78
rename restrict_contexts to allowed_contexts
RedKinda 4c18f20
fix docstrings (?)
RedKinda 46c0bef
add AppCommandContext to flags.py __all__
RedKinda 730145a
update docs for AppCommandContext
RedKinda f838b78
update description
RedKinda 520cf5b
Merge branch 'Rapptz:master' into dev/app_command_contexts
RedKinda df156e2
add allow_contexts decorator
RedKinda File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -48,7 +48,9 @@ | |||||
import re | ||||||
from copy import copy as shallow_copy | ||||||
|
||||||
|
||||||
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale | ||||||
from ..flags import AppCommandContext | ||||||
from .models import Choice | ||||||
from .transformers import annotation_to_parameter, CommandParameter, NoneType | ||||||
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered | ||||||
|
@@ -87,6 +89,9 @@ | |||||
'autocomplete', | ||||||
'guilds', | ||||||
'guild_only', | ||||||
'dm_only', | ||||||
'private_channel_only', | ||||||
'allow_contexts', | ||||||
'default_permissions', | ||||||
) | ||||||
|
||||||
|
@@ -618,6 +623,9 @@ 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.flags.AppCommandContext`] | ||||||
The contexts that the command is allowed to be used in. | ||||||
Overrides ``guild_only`` if this is set. | ||||||
nsfw: :class:`bool` | ||||||
Whether the command is NSFW and should only work in NSFW channels. | ||||||
|
||||||
|
@@ -638,6 +646,7 @@ def __init__( | |||||
nsfw: bool = False, | ||||||
parent: Optional[Group] = None, | ||||||
guild_ids: Optional[List[int]] = None, | ||||||
allowed_contexts: Optional[AppCommandContext] = None, | ||||||
auto_locale_strings: bool = True, | ||||||
extras: Dict[Any, Any] = MISSING, | ||||||
): | ||||||
|
@@ -672,6 +681,9 @@ def __init__( | |||||
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.nsfw: bool = nsfw | ||||||
self.extras: Dict[Any, Any] = extras or {} | ||||||
|
||||||
|
@@ -760,6 +772,7 @@ def to_dict(self) -> Dict[str, Any]: | |||||
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'] = self.allowed_contexts.to_array() if self.allowed_contexts is not None else None | ||||||
|
||||||
return base | ||||||
|
||||||
|
@@ -1167,6 +1180,9 @@ class ContextMenu: | |||||
guild_only: :class:`bool` | ||||||
Whether the command should only be usable in guild contexts. | ||||||
Defaults to ``False``. | ||||||
allowed_contexts: Optional[:class:`~discord.flags.AppCommandContext`] | ||||||
The contexts that this context menu is allowed to be used in. | ||||||
Overrides ``guild_only`` if set. | ||||||
nsfw: :class:`bool` | ||||||
Whether the command is NSFW and should only work in NSFW channels. | ||||||
Defaults to ``False``. | ||||||
|
@@ -1189,6 +1205,7 @@ def __init__( | |||||
type: AppCommandType = MISSING, | ||||||
nsfw: bool = False, | ||||||
guild_ids: Optional[List[int]] = None, | ||||||
allowed_contexts: Optional[AppCommandContext] = MISSING, | ||||||
auto_locale_strings: bool = True, | ||||||
extras: Dict[Any, Any] = MISSING, | ||||||
): | ||||||
|
@@ -1214,6 +1231,9 @@ def __init__( | |||||
) | ||||||
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.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) | ||||||
self.extras: Dict[Any, Any] = extras or {} | ||||||
|
||||||
|
@@ -1249,6 +1269,7 @@ def to_dict(self) -> Dict[str, Any]: | |||||
'name': self.name, | ||||||
'type': self.type.value, | ||||||
'dm_permission': not self.guild_only, | ||||||
'contexts': self.allowed_contexts.to_array() if self.allowed_contexts is not None else None, | ||||||
'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value, | ||||||
'nsfw': self.nsfw, | ||||||
} | ||||||
|
@@ -1405,6 +1426,9 @@ class shortened to 100 characters. | |||||
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.flags.AppCommandContext`] | ||||||
The contexts that this group is allowed to be used in. Overrides | ||||||
guild_only if set. | ||||||
nsfw: :class:`bool` | ||||||
Whether the command is NSFW and should only work in NSFW channels. | ||||||
|
||||||
|
@@ -1424,6 +1448,7 @@ class shortened to 100 characters. | |||||
__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_default_permissions__: Optional[Permissions] = MISSING | ||||||
__discord_app_commands_has_module__: bool = False | ||||||
__discord_app_commands_error_handler__: Optional[ | ||||||
|
@@ -1492,6 +1517,7 @@ def __init__( | |||||
parent: Optional[Group] = None, | ||||||
guild_ids: Optional[List[int]] = None, | ||||||
guild_only: bool = MISSING, | ||||||
allowed_contexts: Optional[AppCommandContext] = MISSING, | ||||||
nsfw: bool = MISSING, | ||||||
auto_locale_strings: bool = True, | ||||||
default_permissions: Optional[Permissions] = MISSING, | ||||||
|
@@ -1540,6 +1566,14 @@ def __init__( | |||||
|
||||||
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 nsfw is MISSING: | ||||||
nsfw = cls.__discord_app_commands_group_nsfw__ | ||||||
|
||||||
|
@@ -1669,6 +1703,7 @@ def to_dict(self) -> Dict[str, Any]: | |||||
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'] = self.allowed_contexts.to_array() if self.allowed_contexts is not None else None | ||||||
|
||||||
return base | ||||||
|
||||||
|
@@ -2418,8 +2453,16 @@ async def my_guild_only_command(interaction: discord.Interaction) -> None: | |||||
def inner(f: T) -> T: | ||||||
if isinstance(f, (Command, Group, ContextMenu)): | ||||||
f.guild_only = True | ||||||
allowed_contexts = f.allowed_contexts or AppCommandContext.none() | ||||||
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.none() | ||||||
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 | ||||||
|
@@ -2430,6 +2473,135 @@ def inner(f: 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.none() | ||||||
f.allowed_contexts = allowed_contexts | ||||||
else: | ||||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext.none() | ||||||
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.none() | ||||||
f.allowed_contexts = allowed_contexts | ||||||
else: | ||||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext.none() | ||||||
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) | ||||||
|
||||||
|
||||||
# wrapper over previous 3 commands | ||||||
def allow_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.allow_contexts(guilds=False, dms=False, private_channels=True) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Didn't you want to set guilds to True in the example as that's what you say in the response message just below |
||||||
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.none() | ||||||
f.allowed_contexts = allowed_contexts | ||||||
else: | ||||||
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext.none() | ||||||
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 default_permissions(**perms: bool) -> Callable[[T], T]: | ||||||
r"""A decorator that sets the default permissions needed to execute this command. | ||||||
|
||||||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should either drop the ability to optionally use parens or add the necessary overloads here otherwise you'll get type errors.