Skip to content
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

Implement application command permissions v2 #1183

Merged
merged 45 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f0748e6
Remove v1 from application commands
Dorukyum Jan 15, 2022
4488210
Remove permissions v1 from documentation
Dorukyum Jan 15, 2022
291203c
Add v2 attributes to commands
Dorukyum Jan 15, 2022
6410974
Fix permissions for groups
Dorukyum Jan 15, 2022
0cc48ac
Use v2 in as_dict methods
Dorukyum Jan 15, 2022
abb059a
Add new permission decorator
Dorukyum Jan 15, 2022
7ca7fbf
Add missing return
Dorukyum Jan 16, 2022
feed5fe
Allow both kwargs and decorators for permissions
Dorukyum Jan 16, 2022
c07242c
Add guild_only decorator
Dorukyum Jan 16, 2022
5844e94
Allow decorating a command
Dorukyum Jan 16, 2022
c0200a0
Merge branch 'master' of https://github.com/Pycord-Development/pycord…
Dorukyum Feb 5, 2022
3853bcc
Merge branch 'master' into permissions-v2
Dorukyum Mar 7, 2022
362a282
Remove permissions v1 from docs
Dorukyum Mar 7, 2022
71c67d3
Merge branch 'master' into permissions-v2
BobDotCom Mar 19, 2022
5a19868
Merge pull request #1129 from Dorukyum/permissions-v2
BobDotCom Mar 19, 2022
8fd19d9
Fix to_check in get_desynced_commands
BobDotCom Mar 19, 2022
ea7b3af
Merge branch 'master' into permissions-v2
BobDotCom Mar 22, 2022
4b319a8
Merge branch 'master' into permissions-v2
Dorukyum Mar 24, 2022
65ae1b4
Merge branch 'master' into permissions-v2
BobDotCom Apr 2, 2022
6ac81a1
Merge branch 'master' into permissions-v2
BobDotCom Apr 3, 2022
a836b62
Merge branch 'master' into permissions-v2
BobDotCom Apr 4, 2022
483a9aa
Merge branch 'master' into permissions-v2
BobDotCom Apr 5, 2022
4b58e83
Merge branch 'master' into permissions-v2
krittick Apr 27, 2022
258eeab
merge master into permissions-v2
krittick Apr 27, 2022
a6fbe73
Merge branch 'master' into permissions-v2
krittick Apr 28, 2022
e209f21
Merge branch 'master' into permissions-v2
BobDotCom Apr 28, 2022
5e11a6a
Rename has_permissions to default_permissions
BobDotCom Apr 28, 2022
b4879ee
Document command permissions
BobDotCom Apr 28, 2022
238ea15
Fix command permissions doc note
BobDotCom Apr 28, 2022
c40ed9d
Remove note
BobDotCom Apr 28, 2022
bf1c3f2
Rename dm_permission to guild_only for parity
BobDotCom Apr 28, 2022
16e9f6c
Fix imports
BobDotCom Apr 28, 2022
9847a42
Fix kwarg name
BobDotCom Apr 28, 2022
b085c6d
Merge branch 'master' into permissions-v2
BobDotCom Apr 28, 2022
70fe0fe
Move permission attributes to parent class
Dorukyum Apr 30, 2022
1eb9aab
Fix guild_only decorator
Dorukyum Apr 30, 2022
28fb2f6
Merge branch 'master' into permissions-v2
Lulalaby May 2, 2022
bcfd8ce
[Perms v2] Additional implementations (#1303)
plun1331 May 3, 2022
f8f0e6b
Merge branch 'master' into permissions-v2
Lulalaby May 5, 2022
81801b6
Update discord/commands/core.py
Lulalaby May 5, 2022
8ae253f
Merge branch 'master' into permissions-v2
Lulalaby May 5, 2022
e3f8203
Reduce need for Permissions TypeVar
TurnrDev May 5, 2022
5249f90
Update api.rst
plun1331 May 5, 2022
dfb9e2b
Merge branch 'master' into permissions-v2
krittick May 5, 2022
30582bc
Merge branch 'master' into permissions-v2
krittick May 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions discord/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ class AuditLogChanges:
"location_type",
_enum_transformer(enums.ScheduledEventLocationType),
),
"command_id": ("command_id", _transform_snowflake),
}

def __init__(
Expand Down
114 changes: 2 additions & 112 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ def _check_command(cmd: ApplicationCommand, match: Dict) -> bool:
else:
as_dict = cmd.to_dict()
to_check = {
"default_permission": None,
"dm_permission": None,
"default_member_permissions": None,
"name": None,
"description": None,
"name_localizations": None,
Expand Down Expand Up @@ -572,7 +573,6 @@ async def sync_commands(
register_guild_commands: bool = True,
check_guilds: Optional[List[int]] = [],
delete_exiting: bool = True,
_register_permissions: bool = True, # TODO: Remove for perms v2
) -> None:
"""|coro|

Expand Down Expand Up @@ -645,12 +645,6 @@ async def sync_commands(
guild_commands, guild_id=guild_id, method=method, force=force, delete_existing=delete_exiting
)

# TODO: 2.1: Remove this and favor permissions v2
# Global Command Permissions

if not _register_permissions:
return

global_permissions: List = []
krittick marked this conversation as resolved.
Show resolved Hide resolved

for i in registered_commands:
Expand All @@ -664,12 +658,7 @@ async def sync_commands(
cmd.id = i["id"]
self._application_commands[cmd.id] = cmd

# Permissions (Roles will be converted to IDs just before Upsert for Global Commands)
global_permissions.append({"id": i["id"], "permissions": cmd.permissions})

for guild_id, commands in registered_guild_commands.items():
guild_permissions: List = []

for i in commands:
cmd = find(
lambda cmd: cmd.name == i["name"]
Expand All @@ -684,105 +673,6 @@ async def sync_commands(
cmd.id = i["id"]
self._application_commands[cmd.id] = cmd

# Permissions
permissions = [
perm.to_dict()
for perm in cmd.permissions
if perm.guild_id is None
or (perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in cmd.guild_ids)
]
guild_permissions.append({"id": i["id"], "permissions": permissions})

for global_command in global_permissions:
permissions = [
perm.to_dict()
for perm in global_command["permissions"]
if perm.guild_id is None
or (perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in cmd.guild_ids)
]
guild_permissions.append({"id": global_command["id"], "permissions": permissions})

# Collect & Upsert Permissions for Each Guild
# Command Permissions for this Guild
guild_cmd_perms: List = []

# Loop through Commands Permissions available for this Guild
for item in guild_permissions:
new_cmd_perm = {"id": item["id"], "permissions": []}

# Replace Role / Owner Names with IDs
for permission in item["permissions"]:
if isinstance(permission["id"], str):
# Replace Role Names
if permission["type"] == 1:
role = get(
self._bot.get_guild(guild_id).roles,
name=permission["id"],
)

# If not missing
if role is not None:
new_cmd_perm["permissions"].append(
{
"id": role.id,
"type": 1,
"permission": permission["permission"],
}
)
else:
raise RuntimeError(
"No Role ID found in Guild ({guild_id}) for Role ({role})".format(
guild_id=guild_id, role=permission["id"]
)
)
# Add owner IDs
elif permission["type"] == 2 and permission["id"] == "owner":
app = await self.application_info() # type: ignore
if app.team:
for m in app.team.members:
new_cmd_perm["permissions"].append(
{
"id": m.id,
"type": 2,
"permission": permission["permission"],
}
)
else:
new_cmd_perm["permissions"].append(
{
"id": app.owner.id,
"type": 2,
"permission": permission["permission"],
}
)
# Add the rest
else:
new_cmd_perm["permissions"].append(permission)

# Make sure we don't have over 10 overwrites
if len(new_cmd_perm["permissions"]) > 10:
raise RuntimeError(
"Command '{name}' has more than 10 permission overrides in guild ({guild_id}).".format(
name=self._application_commands[new_cmd_perm["id"]].name,
guild_id=guild_id,
)
)
# Append to guild_cmd_perms
guild_cmd_perms.append(new_cmd_perm)

# Upsert
try:
await self._bot.http.bulk_upsert_command_permissions(self._bot.user.id, guild_id, guild_cmd_perms)
except Forbidden:
raise RuntimeError(
f"Failed to add command permissions to guild {guild_id}",
file=sys.stderr,
)
except HTTPException:
_log.warning(
"Command Permissions V2 not yet implemented, permissions will not be set for your commands."
)

async def process_application_commands(self, interaction: Interaction, auto_sync: bool = None) -> None:
"""|coro|

Expand Down
108 changes: 54 additions & 54 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
from ..utils import async_all, find, get_or_fetch, utcnow
from .context import ApplicationContext, AutocompleteContext
from .options import Option, OptionChoice
from .permissions import CommandPermission

__all__ = (
"_BaseCommand",
Expand All @@ -88,6 +87,7 @@
from typing_extensions import Concatenate, ParamSpec

from ..cog import Cog
from .. import Permissions

T = TypeVar("T")
CogT = TypeVar("CogT", bound="Cog")
Expand Down Expand Up @@ -201,6 +201,12 @@ def __init__(self, func: Callable, **kwargs) -> None:
self.guild_ids: Optional[List[int]] = kwargs.get("guild_ids", None)
self.parent = kwargs.get("parent")

# Permissions
self.default_member_permissions: Optional["Permissions"] = getattr(
func, "__default_member_permissions__", kwargs.get("default_member_permissions", None)
)
self.guild_only: Optional[bool] = getattr(func, "__guild_only__", kwargs.get("guild_only", None))

def __repr__(self) -> str:
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"

Expand Down Expand Up @@ -573,16 +579,11 @@ class SlashCommand(ApplicationCommand):
parent: Optional[:class:`SlashCommandGroup`]
The parent group that this command belongs to. ``None`` if there
isn't one.
default_permission: :class:`bool`
Whether the command is enabled by default when it is added to a guild.
permissions: List[:class:`CommandPermission`]
The permissions for this command.

.. note::

If this is not empty then default_permissions will be set to False.

cog: Optional[:class:`.Cog`]
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
default_member_permissions: :class:`~discord.Permissions`
The default permissions a member needs to be able to run the command.
cog: Optional[:class:`Cog`]
The cog that this command belongs to. ``None`` if there isn't one.
checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]]
A list of predicates that verifies if the command could be executed
Expand Down Expand Up @@ -647,14 +648,6 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
self._before_invoke = None
self._after_invoke = None

# Permissions
self.default_permission = kwargs.get("default_permission", True)
self.permissions: List[CommandPermission] = getattr(func, "__app_cmd_perms__", []) + kwargs.get(
"permissions", []
)
if self.permissions and self.default_permission:
self.default_permission = False

def _parse_options(self, params) -> List[Option]:
if list(params.items())[0][0] == "self":
temp = list(params.items())
Expand Down Expand Up @@ -761,7 +754,6 @@ def to_dict(self) -> Dict:
"name": self.name,
"description": self.description,
"options": [o.to_dict() for o in self.options],
"default_permission": self.default_permission,
}
if self.name_localizations is not None:
as_dict["name_localizations"] = self.name_localizations
Expand All @@ -770,6 +762,12 @@ def to_dict(self) -> Dict:
if self.is_subcommand:
as_dict["type"] = SlashCommandOptionType.sub_command.value

if self.guild_only is not None:
as_dict["guild_only"] = self.guild_only

if self.default_member_permissions is not None:
as_dict["default_member_permissions"] = self.default_member_permissions.value

return as_dict

async def _invoke(self, ctx: ApplicationContext) -> None:
Expand Down Expand Up @@ -917,6 +915,10 @@ class SlashCommandGroup(ApplicationCommand):
parent: Optional[:class:`SlashCommandGroup`]
The parent group that this group belongs to. ``None`` if there
isn't one.
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
default_member_permissions: :class:`~discord.Permissions`
The default permissions a member needs to be able to run the command.
subcommands: List[Union[:class:`SlashCommand`, :class:`SlashCommandGroup`]]
The list of all subcommands under this group.
cog: Optional[:class:`.Cog`]
Expand Down Expand Up @@ -979,10 +981,9 @@ def __init__(
self.id = None

# Permissions
self.default_permission = kwargs.get("default_permission", True)
self.permissions: List[CommandPermission] = kwargs.get("permissions", [])
if self.permissions and self.default_permission:
self.default_permission = False
self.default_member_permissions: Optional["Permissions"] = kwargs.get("default_member_permissions", None)
self.guild_only: Optional[bool] = kwargs.get("guild_only", None)

self.name_localizations: Optional[Dict[str, str]] = kwargs.get("name_localizations", None)
self.description_localizations: Optional[Dict[str, str]] = kwargs.get("description_localizations", None)

Expand All @@ -995,7 +996,6 @@ def to_dict(self) -> Dict:
"name": self.name,
"description": self.description,
"options": [c.to_dict() for c in self.subcommands],
"default_permission": self.default_permission,
}
if self.name_localizations is not None:
as_dict["name_localizations"] = self.name_localizations
Expand All @@ -1005,6 +1005,12 @@ def to_dict(self) -> Dict:
if self.parent is not None:
as_dict["type"] = self.input_type.value

if self.guild_only is not None:
as_dict["guild_only"] = self.guild_only

if self.default_member_permissions is not None:
as_dict["default_member_permissions"] = self.default_member_permissions.value

return as_dict

def command(self, **kwargs) -> Callable[[Callable], SlashCommand]:
Expand Down Expand Up @@ -1183,15 +1189,11 @@ class ContextMenuCommand(ApplicationCommand):
The coroutine that is executed when the command is called.
guild_ids: Optional[List[:class:`int`]]
The ids of the guilds where this command will be registered.
default_permission: :class:`bool`
Whether the command is enabled by default when it is added to a guild.
permissions: List[:class:`.CommandPermission`]
The permissions for this command.

.. note::
If this is not empty then default_permissions will be set to ``False``.

cog: Optional[:class:`.Cog`]
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
default_member_permissions: :class:`~discord.Permissions`
The default permissions a member needs to be able to run the command.
cog: Optional[:class:`Cog`]
The cog that this command belongs to. ``None`` if there isn't one.
checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]]
A list of predicates that verifies if the command could be executed
Expand Down Expand Up @@ -1236,13 +1238,6 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:

self.validate_parameters()

self.default_permission = kwargs.get("default_permission", True)
self.permissions: List[CommandPermission] = getattr(func, "__app_cmd_perms__", []) + kwargs.get(
"permissions", []
)
if self.permissions and self.default_permission:
self.default_permission = False

# Context Menu commands can't have parents
self.parent = None

Expand Down Expand Up @@ -1283,9 +1278,14 @@ def to_dict(self) -> Dict[str, Union[str, int]]:
"name": self.name,
"description": self.description,
"type": self.type,
"default_permission": self.default_permission,
}

if self.guild_only is not None:
as_dict["guild_only"] = self.guild_only

if self.default_member_permissions is not None:
as_dict["default_member_permissions"] = self.default_member_permissions.value

if self.name_localizations is not None:
as_dict["name_localizations"] = self.name_localizations

Expand Down Expand Up @@ -1619,42 +1619,42 @@ def validate_chat_input_name(name: Any, locale: Optional[str] = None):
# Must meet the regex ^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$
if locale is not None and locale not in valid_locales:
raise ValidationError(
f"Locale '{locale}' is not a valid locale, "
f"see {docs}/reference#locales for list of supported locales."
f"Locale '{locale}' is not a valid locale, " f"see {docs}/reference#locales for list of supported locales."
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved
)
error = None
if not isinstance(name, str):
if not isinstance(name, str) or not re.match(r"^[\w-]{1,32}$", name):
error = TypeError(f"Command names and options must be of type str. Received \"{name}\"")
elif not re.match(r"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$", name):
error = ValidationError(
r"Command names and options must follow the regex \"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$\". For more information, see "
f"{docs}/interactions/application-commands#application-command-object-application-command-naming. "
f"Received \"{name}\""
f'Received "{name}"'
)
elif not 1 <= len(name) <= 32:
error = ValidationError(f"Command names and options must be 1-32 characters long. Received \"{name}\"")
error = ValidationError(f'Command names and options must be 1-32 characters long. Received "{name}"')
elif not name.lower() == name: # Can't use islower() as it fails if none of the chars can be lower. See #512.
error = ValidationError(f"Command names and options must be lowercase. Received \"{name}\"")
error = ValidationError(f'Command names and options must be lowercase. Received "{name}"')

if error:
if locale:
error.args = (error.args[0]+f" in locale {locale}",)
error.args = (f"{error.args[0]} in locale {locale}",)
raise error


def validate_chat_input_description(description: Any, locale: Optional[str] = None):
if locale is not None and locale not in valid_locales:
raise ValidationError(
f"Locale '{locale}' is not a valid locale, "
f"see {docs}/reference#locales for list of supported locales."
f"Locale '{locale}' is not a valid locale, " f"see {docs}/reference#locales for list of supported locales."
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved
)
error = None
if not isinstance(description, str):
error = TypeError(f"Command and option description must be of type str. Received \"{description}\"")
error = TypeError(f'Command and option description must be of type str. Received "{description}"')
elif not 1 <= len(description) <= 100:
error = ValidationError(f"Command and option description must be 1-100 characters long. Received \"{description}\"")
error = ValidationError(
f'Command and option description must be 1-100 characters long. Received "{description}"'
)

if error:
if locale:
error.args = (error.args[0]+f" in locale {locale}",)
error.args = (f"{error.args[0]} in locale {locale}",)
raise error
Loading