Skip to content

Commit df85b81

Browse files
NeloBlivionpre-commit-ci[bot]BobDotCom
authored
feat: Add support for nsfw commands (#1775)
* Add @is_nsfw decorator * Add @is_nsfw bridge decorator * Update bot.py * Add nsfw attribute and documentation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * App Directory warning * Update docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update bridge docs * Add changelog entry Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: BobDotCom <[email protected]>
1 parent 4fb327c commit df85b81

File tree

7 files changed

+89
-4
lines changed

7 files changed

+89
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ These changes are available on the `master` branch, but have not yet been releas
1818
- New select types `user`, `role`, `mentionable`, and `channel` - Along with their
1919
respective types and shortcut decorators.
2020
([#1702](https://github.com/Pycord-Development/pycord/pull/1702))
21+
- Added support for age-restricted (NSFW) commands.
22+
([#1775](https://github.com/Pycord-Development/pycord/pull/1775))
2123

2224
### Fixed
2325

discord/bot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool:
275275
as_dict = cmd.to_dict()
276276
to_check = {
277277
"dm_permission": None,
278+
"nsfw": None,
278279
"default_member_permissions": None,
279280
"name": None,
280281
"description": None,

discord/commands/core.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def __init__(self, func: Callable, **kwargs) -> None:
222222
self.guild_only: bool | None = getattr(
223223
func, "__guild_only__", kwargs.get("guild_only", None)
224224
)
225+
self.nsfw: bool | None = getattr(func, "__nsfw__", kwargs.get("nsfw", None))
225226

226227
def __repr__(self) -> str:
227228
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
@@ -629,6 +630,9 @@ class SlashCommand(ApplicationCommand):
629630
Returns a string that allows you to mention the slash command.
630631
guild_only: :class:`bool`
631632
Whether the command should only be usable inside a guild.
633+
nsfw: :class:`bool`
634+
Whether the command should be restricted to 18+ channels and users.
635+
Apps intending to be listed in the App Directory cannot have NSFW commands.
632636
default_member_permissions: :class:`~discord.Permissions`
633637
The default permissions a member needs to be able to run the command.
634638
cog: Optional[:class:`Cog`]
@@ -849,6 +853,9 @@ def to_dict(self) -> dict:
849853
if self.guild_only is not None:
850854
as_dict["dm_permission"] = not self.guild_only
851855

856+
if self.nsfw is not None:
857+
as_dict["nsfw"] = self.nsfw
858+
852859
if self.default_member_permissions is not None:
853860
as_dict[
854861
"default_member_permissions"
@@ -1060,6 +1067,9 @@ class SlashCommandGroup(ApplicationCommand):
10601067
isn't one.
10611068
guild_only: :class:`bool`
10621069
Whether the command should only be usable inside a guild.
1070+
nsfw: :class:`bool`
1071+
Whether the command should be restricted to 18+ channels and users.
1072+
Apps intending to be listed in the App Directory cannot have NSFW commands.
10631073
default_member_permissions: :class:`~discord.Permissions`
10641074
The default permissions a member needs to be able to run the command.
10651075
checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]]
@@ -1132,6 +1142,7 @@ def __init__(
11321142
"default_member_permissions", None
11331143
)
11341144
self.guild_only: bool | None = kwargs.get("guild_only", None)
1145+
self.nsfw: bool | None = kwargs.get("nsfw", None)
11351146

11361147
self.name_localizations: dict[str, str] | None = kwargs.get(
11371148
"name_localizations", None
@@ -1161,6 +1172,9 @@ def to_dict(self) -> dict:
11611172
if self.guild_only is not None:
11621173
as_dict["dm_permission"] = not self.guild_only
11631174

1175+
if self.nsfw is not None:
1176+
as_dict["nsfw"] = self.nsfw
1177+
11641178
if self.default_member_permissions is not None:
11651179
as_dict[
11661180
"default_member_permissions"
@@ -1208,6 +1222,9 @@ def create_subgroup(
12081222
This will be a global command if ``None`` is passed.
12091223
guild_only: :class:`bool`
12101224
Whether the command should only be usable inside a guild.
1225+
nsfw: :class:`bool`
1226+
Whether the command should be restricted to 18+ channels and users.
1227+
Apps intending to be listed in the App Directory cannot have NSFW commands.
12111228
default_member_permissions: :class:`~discord.Permissions`
12121229
The default permissions a member needs to be able to run the command.
12131230
checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]]
@@ -1377,6 +1394,9 @@ class ContextMenuCommand(ApplicationCommand):
13771394
The ids of the guilds where this command will be registered.
13781395
guild_only: :class:`bool`
13791396
Whether the command should only be usable inside a guild.
1397+
nsfw: :class:`bool`
1398+
Whether the command should be restricted to 18+ channels and users.
1399+
Apps intending to be listed in the App Directory cannot have NSFW commands.
13801400
default_member_permissions: :class:`~discord.Permissions`
13811401
The default permissions a member needs to be able to run the command.
13821402
cog: Optional[:class:`Cog`]
@@ -1476,6 +1496,9 @@ def to_dict(self) -> dict[str, str | int]:
14761496
if self.guild_only is not None:
14771497
as_dict["dm_permission"] = not self.guild_only
14781498

1499+
if self.nsfw is not None:
1500+
as_dict["nsfw"] = self.nsfw
1501+
14791502
if self.default_member_permissions is not None:
14801503
as_dict[
14811504
"default_member_permissions"

discord/commands/permissions.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@
2828
from ..permissions import Permissions
2929
from .core import ApplicationCommand
3030

31-
__all__ = (
32-
"default_permissions",
33-
"guild_only",
34-
)
31+
__all__ = ("default_permissions", "guild_only", "is_nsfw")
3532

3633

3734
def default_permissions(**perms: bool) -> Callable:
@@ -108,3 +105,34 @@ def inner(command: Callable):
108105
return command
109106

110107
return inner
108+
109+
110+
def is_nsfw() -> Callable:
111+
"""A decorator that limits the usage of a slash command to 18+ channels and users.
112+
In guilds, the command will only be able to be used in channels marked as NSFW.
113+
In DMs, users must have opted into age-restricted commands via privacy settings.
114+
115+
Note that apps intending to be listed in the App Directory cannot have NSFW commands.
116+
117+
Example
118+
-------
119+
120+
.. code-block:: python3
121+
122+
from discord import is_nsfw
123+
124+
@bot.slash_command()
125+
@is_nsfw()
126+
async def test(ctx):
127+
await ctx.respond("This command is age restricted.")
128+
"""
129+
130+
def inner(command: Callable):
131+
if isinstance(command, ApplicationCommand):
132+
command.nsfw = True
133+
else:
134+
command.__nsfw__ = True
135+
136+
return command
137+
138+
return inner

discord/ext/bridge/core.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"map_to",
6969
"guild_only",
7070
"has_permissions",
71+
"is_nsfw",
7172
)
7273

7374

@@ -437,6 +438,30 @@ def predicate(func: Callable | ApplicationCommand):
437438
return predicate
438439

439440

441+
def is_nsfw():
442+
"""Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check`
443+
that locks the command to only run in nsfw contexts, and also registers the command as nsfw client-side (on discord).
444+
445+
Basically a utility function that wraps both :func:`discord.ext.commands.is_nsfw` and :func:`discord.commands.is_nsfw`.
446+
447+
.. warning::
448+
449+
In DMs, the prefixed-based command will always run as the user's privacy settings cannot be checked directly.
450+
"""
451+
452+
def predicate(func: Callable | ApplicationCommand):
453+
if isinstance(func, ApplicationCommand):
454+
func.nsfw = True
455+
else:
456+
func.__nsfw__ = True
457+
458+
from ..commands import is_nsfw
459+
460+
return is_nsfw()(func)
461+
462+
return predicate
463+
464+
440465
def has_permissions(**perms: dict[str, bool]):
441466
r"""Intended to work with :class:`.SlashCommand` and :class:`BridgeCommand`, adds a
442467
:func:`~ext.commands.check` that locks the command to be run by people with certain

docs/api/application_commands.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Command Permission Decorators
1313
.. autofunction:: discord.commands.guild_only
1414
:decorator:
1515

16+
.. autofunction:: discord.commands.is_nsfw
17+
:decorator:
18+
1619

1720
Commands
1821
--------

docs/ext/bridge/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ Decorators
7373
.. automethod:: discord.ext.bridge.guild_only()
7474
:decorator:
7575

76+
.. automethod:: discord.ext.bridge.is_nsfw()
77+
:decorator:
78+
7679
.. automethod:: discord.ext.bridge.has_permissions()
7780
:decorator:
7881

0 commit comments

Comments
 (0)