Browse Source

Add support for NSFW application commands

Of course, this somehow doesn't work with subcommands
pull/8062/head
Rapptz 3 years ago
parent
commit
573b2121b7
  1. 49
      discord/app_commands/commands.py
  2. 4
      discord/app_commands/models.py
  3. 13
      discord/app_commands/tree.py
  4. 1
      discord/types/command.py

49
discord/app_commands/commands.py

@ -484,6 +484,11 @@ 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.
Defaults to ``False``. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands. Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`] parent: Optional[:class:`Group`]
The parent application command. ``None`` if there isn't one. The parent application command. ``None`` if there isn't one.
@ -495,6 +500,7 @@ class Command(Generic[GroupT, P, T]):
name: str, name: str,
description: str, description: str,
callback: CommandCallback[GroupT, P, T], callback: CommandCallback[GroupT, P, T],
nsfw: bool = False,
parent: Optional[Group] = None, parent: Optional[Group] = None,
guild_ids: Optional[List[int]] = None, guild_ids: Optional[List[int]] = None,
): ):
@ -523,6 +529,7 @@ 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.nsfw: bool = nsfw
if self._guild_ids is not None and self.parent is not None: if self._guild_ids is not None and self.parent is not None:
raise ValueError('child commands cannot have default guilds set, consider setting them in the parent instead') raise ValueError('child commands cannot have default guilds set, consider setting them in the parent instead')
@ -553,6 +560,7 @@ class Command(Generic[GroupT, P, T]):
copy.description = self.description copy.description = self.description
copy.default_permissions = self.default_permissions copy.default_permissions = self.default_permissions
copy.guild_only = self.guild_only copy.guild_only = self.guild_only
copy.nsfw = self.nsfw
copy._attr = self._attr copy._attr = self._attr
copy._callback = self._callback copy._callback = self._callback
copy.on_error = self.on_error copy.on_error = self.on_error
@ -578,6 +586,7 @@ class Command(Generic[GroupT, P, T]):
} }
if self.parent is None: if self.parent is None:
base['nsfw'] = self.nsfw
base['dm_permission'] = not self.guild_only base['dm_permission'] = not self.guild_only
base['default_member_permissions'] = self.default_permissions and self.default_permissions.value base['default_member_permissions'] = self.default_permissions and self.default_permissions.value
@ -922,6 +931,9 @@ 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``.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
checks checks
A list of predicates that take a :class:`~discord.Interaction` parameter A list of predicates that take a :class:`~discord.Interaction` parameter
to indicate whether the command callback should be executed. If an exception to indicate whether the command callback should be executed. If an exception
@ -936,6 +948,7 @@ class ContextMenu:
name: str, name: str,
callback: ContextMenuCallback, callback: ContextMenuCallback,
type: AppCommandType = MISSING, type: AppCommandType = MISSING,
nsfw: bool = False,
guild_ids: Optional[List[int]] = None, guild_ids: Optional[List[int]] = None,
): ):
self.name: str = validate_context_menu_name(name) self.name: str = validate_context_menu_name(name)
@ -956,6 +969,7 @@ class ContextMenu:
self.default_permissions: Optional[Permissions] = getattr( self.default_permissions: Optional[Permissions] = getattr(
callback, '__discord_app_commands_default_permissions__', None callback, '__discord_app_commands_default_permissions__', None
) )
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.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', [])
@ -975,6 +989,7 @@ class ContextMenu:
'type': self.type.value, 'type': self.type.value,
'dm_permission': not self.guild_only, 'dm_permission': not self.guild_only,
'default_member_permissions': self.default_permissions and self.default_permissions.value, 'default_member_permissions': self.default_permissions and self.default_permissions.value,
'nsfw': self.nsfw,
} }
async def _check_can_run(self, interaction: Interaction) -> bool: async def _check_can_run(self, interaction: Interaction) -> bool:
@ -1094,6 +1109,11 @@ class Group:
Whether the group should only be usable in guild contexts. Whether the group should only be usable in guild contexts.
Defaults to ``False``. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands. Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`] parent: Optional[:class:`Group`]
The parent group. ``None`` if there isn't one. The parent group. ``None`` if there isn't one.
@ -1103,6 +1123,7 @@ class Group:
__discord_app_commands_skip_init_binding__: bool = False __discord_app_commands_skip_init_binding__: bool = False
__discord_app_commands_group_name__: str = MISSING __discord_app_commands_group_name__: str = MISSING
__discord_app_commands_group_description__: str = MISSING __discord_app_commands_group_description__: str = MISSING
__discord_app_commands_group_nsfw__: bool = False
__discord_app_commands_guild_only__: bool = MISSING __discord_app_commands_guild_only__: bool = 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
@ -1113,6 +1134,7 @@ class Group:
name: str = MISSING, name: str = MISSING,
description: str = MISSING, description: str = MISSING,
guild_only: bool = MISSING, guild_only: bool = MISSING,
nsfw: bool = False,
default_permissions: Optional[Permissions] = MISSING, default_permissions: Optional[Permissions] = MISSING,
) -> None: ) -> None:
if not cls.__discord_app_commands_group_children__: if not cls.__discord_app_commands_group_children__:
@ -1152,6 +1174,7 @@ class Group:
if cls.__module__ != __name__: if cls.__module__ != __name__:
cls.__discord_app_commands_has_module__ = True cls.__discord_app_commands_has_module__ = True
cls.__discord_app_commands_group_nsfw__ = nsfw
def __init__( def __init__(
self, self,
@ -1161,6 +1184,7 @@ 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,
nsfw: bool = MISSING,
default_permissions: Optional[Permissions] = MISSING, default_permissions: Optional[Permissions] = MISSING,
): ):
cls = self.__class__ cls = self.__class__
@ -1186,6 +1210,11 @@ class Group:
self.guild_only: bool = guild_only self.guild_only: bool = guild_only
if nsfw is MISSING:
nsfw = cls.__discord_app_commands_group_nsfw__
self.nsfw: bool = nsfw
if not self.description: if not self.description:
raise TypeError('groups must have a description') raise TypeError('groups must have a description')
@ -1246,6 +1275,7 @@ class Group:
copy.module = self.module copy.module = self.module
copy.default_permissions = self.default_permissions copy.default_permissions = self.default_permissions
copy.guild_only = self.guild_only copy.guild_only = self.guild_only
copy.nsfw = self.nsfw
copy._attr = self._attr copy._attr = self._attr
copy._owner_cls = self._owner_cls copy._owner_cls = self._owner_cls
copy._children = {} copy._children = {}
@ -1280,6 +1310,7 @@ class Group:
} }
if self.parent is None: if self.parent is None:
base['nsfw'] = self.nsfw
base['dm_permission'] = not self.guild_only base['dm_permission'] = not self.guild_only
base['default_member_permissions'] = self.default_permissions and self.default_permissions.value base['default_member_permissions'] = self.default_permissions and self.default_permissions.value
@ -1483,6 +1514,7 @@ class Group:
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING, description: str = MISSING,
nsfw: bool = False,
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]: ) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
"""Creates an application command under this group. """Creates an application command under this group.
@ -1495,6 +1527,8 @@ class Group:
The description of the application command. This shows up in the UI to describe The description of the application command. This shows up in the UI to describe
the application command. If not given, it defaults to the first line of the docstring the application command. If not given, it defaults to the first line of the docstring
of the callback shortened to 100 characters. of the callback shortened to 100 characters.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
""" """
def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]: def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]:
@ -1513,6 +1547,7 @@ class Group:
name=name if name is not MISSING else func.__name__, name=name if name is not MISSING else func.__name__,
description=desc, description=desc,
callback=func, callback=func,
nsfw=nsfw,
parent=self, parent=self,
) )
self.add_command(command) self.add_command(command)
@ -1525,6 +1560,7 @@ def command(
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING, description: str = MISSING,
nsfw: bool = False,
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]: ) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
"""Creates an application command from a regular function. """Creates an application command from a regular function.
@ -1537,6 +1573,10 @@ def command(
The description of the application command. This shows up in the UI to describe The description of the application command. This shows up in the UI to describe
the application command. If not given, it defaults to the first line of the docstring the application command. If not given, it defaults to the first line of the docstring
of the callback shortened to 100 characters. of the callback shortened to 100 characters.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
""" """
def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]: def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]:
@ -1556,12 +1596,13 @@ def command(
description=desc, description=desc,
callback=func, callback=func,
parent=None, parent=None,
nsfw=nsfw,
) )
return decorator return decorator
def context_menu(*, name: str = MISSING) -> Callable[[ContextMenuCallback], ContextMenu]: def context_menu(*, name: str = MISSING, nsfw: bool = False) -> Callable[[ContextMenuCallback], ContextMenu]:
"""Creates an application command context menu from a regular function. """Creates an application command context menu from a regular function.
This function must have a signature of :class:`~discord.Interaction` as its first parameter This function must have a signature of :class:`~discord.Interaction` as its first parameter
@ -1587,6 +1628,10 @@ def context_menu(*, name: str = MISSING) -> Callable[[ContextMenuCallback], Cont
The name of the context menu command. If not given, it defaults to a title-case The name of the context menu command. If not given, it defaults to a title-case
version of the callback name. Note that unlike regular slash commands this can version of the callback name. Note that unlike regular slash commands this can
have spaces and upper case characters in the name. have spaces and upper case characters in the name.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
""" """
def decorator(func: ContextMenuCallback) -> ContextMenu: def decorator(func: ContextMenuCallback) -> ContextMenu:
@ -1594,7 +1639,7 @@ def context_menu(*, name: str = MISSING) -> Callable[[ContextMenuCallback], Cont
raise TypeError('context menu function must be a coroutine function') raise TypeError('context menu function must be a coroutine function')
actual_name = func.__name__.title() if name is MISSING else name actual_name = func.__name__.title() if name is MISSING else name
return ContextMenu(name=actual_name, callback=func) return ContextMenu(name=actual_name, nsfw=nsfw, callback=func)
return decorator return decorator

4
discord/app_commands/models.py

@ -141,6 +141,8 @@ class AppCommand(Hashable):
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.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
""" """
__slots__ = ( __slots__ = (
@ -153,6 +155,7 @@ class AppCommand(Hashable):
'options', 'options',
'default_member_permissions', 'default_member_permissions',
'dm_permission', 'dm_permission',
'nsfw',
'_state', '_state',
) )
@ -183,6 +186,7 @@ class AppCommand(Hashable):
dm_permission = True dm_permission = True
self.dm_permission: bool = dm_permission self.dm_permission: bool = dm_permission
self.nsfw: bool = data.get('nsfw', False)
def to_dict(self) -> ApplicationCommandPayload: def to_dict(self) -> ApplicationCommandPayload:
return { return {

13
discord/app_commands/tree.py

@ -790,6 +790,7 @@ class CommandTree(Generic[ClientT]):
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING, description: str = MISSING,
nsfw: bool = False,
guild: Optional[Snowflake] = MISSING, guild: Optional[Snowflake] = MISSING,
guilds: Sequence[Snowflake] = MISSING, guilds: Sequence[Snowflake] = MISSING,
) -> Callable[[CommandCallback[Group, P, T]], Command[Group, P, T]]: ) -> Callable[[CommandCallback[Group, P, T]], Command[Group, P, T]]:
@ -804,6 +805,10 @@ class CommandTree(Generic[ClientT]):
The description of the application command. This shows up in the UI to describe The description of the application command. This shows up in the UI to describe
the application command. If not given, it defaults to the first line of the docstring the application command. If not given, it defaults to the first line of the docstring
of the callback shortened to 100 characters. of the callback shortened to 100 characters.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
guild: Optional[:class:`~discord.abc.Snowflake`] guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given or ``None`` then it The guild to add the command to. If not given or ``None`` then it
becomes a global command instead. becomes a global command instead.
@ -829,6 +834,7 @@ class CommandTree(Generic[ClientT]):
name=name if name is not MISSING else func.__name__, name=name if name is not MISSING else func.__name__,
description=desc, description=desc,
callback=func, callback=func,
nsfw=nsfw,
parent=None, parent=None,
) )
self.add_command(command, guild=guild, guilds=guilds) self.add_command(command, guild=guild, guilds=guilds)
@ -840,6 +846,7 @@ class CommandTree(Generic[ClientT]):
self, self,
*, *,
name: str = MISSING, name: str = MISSING,
nsfw: bool = False,
guild: Optional[Snowflake] = MISSING, guild: Optional[Snowflake] = MISSING,
guilds: Sequence[Snowflake] = MISSING, guilds: Sequence[Snowflake] = MISSING,
) -> Callable[[ContextMenuCallback], ContextMenu]: ) -> Callable[[ContextMenuCallback], ContextMenu]:
@ -868,6 +875,10 @@ class CommandTree(Generic[ClientT]):
The name of the context menu command. If not given, it defaults to a title-case The name of the context menu command. If not given, it defaults to a title-case
version of the callback name. Note that unlike regular slash commands this can version of the callback name. Note that unlike regular slash commands this can
have spaces and upper case characters in the name. have spaces and upper case characters in the name.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
guild: Optional[:class:`~discord.abc.Snowflake`] guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to add the command to. If not given or ``None`` then it The guild to add the command to. If not given or ``None`` then it
becomes a global command instead. becomes a global command instead.
@ -882,7 +893,7 @@ class CommandTree(Generic[ClientT]):
raise TypeError('context menu function must be a coroutine function') raise TypeError('context menu function must be a coroutine function')
actual_name = func.__name__.title() if name is MISSING else name actual_name = func.__name__.title() if name is MISSING else name
context_menu = ContextMenu(name=actual_name, callback=func) context_menu = ContextMenu(name=actual_name, nsfw=nsfw, callback=func)
self.add_command(context_menu, guild=guild, guilds=guilds) self.add_command(context_menu, guild=guild, guilds=guilds)
return context_menu return context_menu

1
discord/types/command.py

@ -136,6 +136,7 @@ class _BaseApplicationCommand(TypedDict):
name: str name: str
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]
version: Snowflake version: Snowflake

Loading…
Cancel
Save