Skip to content

Commit

Permalink
Merge pull request Pycord-Development#280 from TheGamerX20/slash_perms
Browse files Browse the repository at this point in the history
Add Slash Command Permissions support.
  • Loading branch information
BobDotCom authored Oct 21, 2021
2 parents 5ef9963 + 21cb441 commit 997d364
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 11 deletions.
96 changes: 85 additions & 11 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ async def register_commands(self) -> None:
"""
commands = []

# Global Command Permissions
global_permissions: List = []

registered_commands = await self.http.get_global_commands(self.user.id)
for command in [cmd for cmd in self.pending_application_commands if cmd.guild_ids is None]:
as_dict = command.to_dict()
Expand All @@ -165,6 +168,20 @@ async def register_commands(self) -> None:
as_dict["id"] = matches[0]["id"]
commands.append(as_dict)

cmds = await self.http.bulk_upsert_global_commands(self.user.id, commands)

for i in cmds:
cmd = get(
self.pending_application_commands,
name=i["name"],
description=i["description"],
type=i["type"],
)
self.application_commands[i["id"]] = cmd

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

update_guild_commands = {}
async for guild in self.fetch_guilds(limit=None):
update_guild_commands[guild.id] = []
Expand All @@ -178,6 +195,9 @@ async def register_commands(self) -> None:
try:
cmds = await self.http.bulk_upsert_guild_commands(self.user.id, guild_id,
update_guild_commands[guild_id])

# Permissions for this Guild
guild_permissions: List = []
except Forbidden:
if not update_guild_commands[guild_id]:
continue
Expand All @@ -186,19 +206,73 @@ async def register_commands(self) -> None:
raise
else:
for i in cmds:
cmd = get(self.pending_application_commands, name=i["name"], description=i["description"], type=i['type'])
cmd = get(self.pending_application_commands, name=i["name"], description=i["description"],
type=i['type'])
self.application_commands[i["id"]] = cmd

cmds = await self.http.bulk_upsert_global_commands(self.user.id, commands)

for i in cmds:
cmd = get(
self.pending_application_commands,
name=i["name"],
description=i["description"],
type=i["type"],
)
self.application_commands[i["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 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 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 and isinstance(permission['id'], str):
role = get(self.get_guild(guild_id).roles, name=permission['id'])

# If not missing
if not role is None:
new_cmd_perm["permissions"].append(
{"id": role.id, "type": 1, "permission": permission['permission']})
else:
print("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:
print(
"Command '{name}' has more than 10 permission overrides in guild ({guild_id}).\nwill only use the first 10 permission overrides.".format(
name=self.application_commands[new_cmd_perm['id']].name, guild_id=guild_id))
new_cmd_perm['permissions'] = new_cmd_perm['permissions'][:10]

# Append to guild_cmd_perms
guild_cmd_perms.append(new_cmd_perm)

# Upsert
try:
await self.http.bulk_upsert_command_permissions(self.user.id, guild_id, guild_cmd_perms)
except Forbidden:
print(f"Failed to add command permissions to guild {guild_id}", file=sys.stderr)
raise

async def process_application_commands(self, interaction: Interaction) -> None:
"""|coro|
Expand Down
109 changes: 109 additions & 0 deletions discord/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
"slash_command",
"user_command",
"message_command",
"has_role",
"has_any_role",
"is_user",
"is_owner",
"permission",
)

def wrap_callback(coro):
Expand Down Expand Up @@ -332,6 +337,10 @@ 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: Optional[List[Permission]] = getattr(func, "__app_cmd_perms__", None)


def parse_options(self, params) -> List[Option]:
Expand Down Expand Up @@ -401,6 +410,7 @@ 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.is_subcommand:
as_dict["type"] = SlashCommandOptionType.sub_command.value
Expand Down Expand Up @@ -667,6 +677,9 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:

self.validate_parameters()

# Context Commands don't have permissions
self.permissions = None

def validate_parameters(self):
params = self._get_signature_parameters()
if list(params.items())[0][0] == "self":
Expand Down Expand Up @@ -946,3 +959,99 @@ def validate_chat_input_description(description: Any):
raise ValidationError(
"Description of a chat input command must be less than 100 characters and non empty."
)

# Slash Command Permissions
class Permission:
def __init__(self, id: Union[int, str], type: int, permission: bool = True, guild_id: int = None):
self.id = id
self.type = type
self.permission = permission
self.guild_id = guild_id

def to_dict(self) -> Dict[int, int, bool]:
return {"id": self.id, "type": self.type, "permission": self.permission}

def permission(role_id: int = None, user_id: int = None, permission: bool = True, guild_id: int = None):
def decorator(func: Callable):
if not role_id is None:
app_cmd_perm = Permission(role_id, 1, permission, guild_id)
elif not user_id is None:
app_cmd_perm = Permission(user_id, 2, permission, guild_id)
else:
raise ValueError("role_id or user_id must be specified!")

# Create __app_cmd_perms__
if not hasattr(func, '__app_cmd_perms__'):
func.__app_cmd_perms__ = []

# Append
func.__app_cmd_perms__.append(app_cmd_perm)

return func

return decorator

def has_role(item: Union[int, str], guild_id: int = None):
def decorator(func: Callable):
# Create __app_cmd_perms__
if not hasattr(func, '__app_cmd_perms__'):
func.__app_cmd_perms__ = []

# Permissions (Will Convert ID later in register_commands if needed)
app_cmd_perm = Permission(item, 1, True, guild_id) #{"id": item, "type": 1, "permission": True}

# Append
func.__app_cmd_perms__.append(app_cmd_perm)

return func

return decorator

def has_any_role(*items: Union[int, str], guild_id: int = None):
def decorator(func: Callable):
# Create __app_cmd_perms__
if not hasattr(func, '__app_cmd_perms__'):
func.__app_cmd_perms__ = []

# Permissions (Will Convert ID later in register_commands if needed)
for item in items:
app_cmd_perm = Permission(item, 1, True, guild_id) #{"id": item, "type": 1, "permission": True}

# Append
func.__app_cmd_perms__.append(app_cmd_perm)

return func

return decorator

def is_user(user: int, guild_id: int = None):
def decorator(func: Callable):
# Create __app_cmd_perms__
if not hasattr(func, '__app_cmd_perms__'):
func.__app_cmd_perms__ = []

# Permissions (Will Convert ID later in register_commands if needed)
app_cmd_perm = Permission(user, 2, True, guild_id) #{"id": user, "type": 2, "permission": True}

# Append
func.__app_cmd_perms__.append(app_cmd_perm)

return func

return decorator

def is_owner(guild_id: int = None):
def decorator(func: Callable):
# Create __app_cmd_perms__
if not hasattr(func, '__app_cmd_perms__'):
func.__app_cmd_perms__ = []

# Permissions (Will Convert ID later in register_commands if needed)
app_cmd_perm = Permission("owner", 2, True, guild_id) #{"id": "owner", "type": 2, "permission": True}

# Append
func.__app_cmd_perms__.append(app_cmd_perm)

return func

return decorator
14 changes: 14 additions & 0 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,20 @@ def bulk_upsert_guild_commands(
)
return self.request(r, json=payload)

def bulk_upsert_command_permissions(
self,
application_id: Snowflake,
guild_id: Snowflake,
payload: List[interactions.EditApplicationCommand],
) -> Response[List[interactions.ApplicationCommand]]:
r = Route(
'PUT',
'/applications/{application_id}/guilds/{guild_id}/commands/permissions',
application_id=application_id,
guild_id=guild_id,
)
return self.request(r, json=payload)

# Interaction responses

def _edit_webhook_helper(
Expand Down
75 changes: 75 additions & 0 deletions examples/app_commands/slash_perms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import discord

# Imports Commands from discord.commands (for Slash Permissions)
from discord.commands import commands

bot = discord.Bot()

# Note: If you want you can use commands.Bot instead of discord.Bot
# Use discord.Bot if you don't want prefixed message commands

# With discord.Bot you can use @bot.command as an alias
# of @bot.slash_command but this is overridden by commands.Bot

# by default, default_permission is set to True, you can use
# default_permission=False to disable the command for everyone.
# You can add up to 10 permissions per Command for a guild.
# You can either use the following decorators:
# --------------------------------------------
# @commands.permission(role_id/user_id, permission)
# @commands.has_role("ROLE_NAME") <-- can use either a name or id
# @commands.has_any_role("ROLE_NAME", "ROLE_NAME_2") <-- can use either a name or id
# @commands.is_user(USER_ID) <-- id only
# @commands.is_owner()
# Note: you can supply "guild_id" to limit it to 1 guild.
# Ex: @commands.has_role("Admin", guild_id=GUILD_ID)
# --------------------------------------------
# or supply permissions directly in @bot.slash_command
# @bot.slash_command(default_permission=False,
# permissions=[commands.Permission(id=ID, type=TYPE, permission=True, guild_id=GUILD_ID)])

# Note: Please replace token, GUILD_ID, USER_ID and ROLE_NAME.

# Guild Slash Command Example with User Permissions
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False)
@commands.is_user(USER_ID)
async def user(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# Guild Slash Command Example with Owner Permissions
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False)
@commands.is_owner()
async def owner(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# Guild Slash Command Example with Role Permissions
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False)
@commands.has_role("ROLE_NAME")
async def role(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# Guild Slash Command Example with Any Specified Role Permissions
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False)
@commands.has_any_role("ROLE_NAME", "ROLE_NAME2")
async def multirole(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# Guild Slash Command Example with Permission Decorator
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False)
@commands.permission(user_id=USER_ID, permission=True)
async def permission_decorator(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# Guild Slash Command Example with Permissions Kwarg
@bot.slash_command(guild_ids=[GUILD_ID], default_permission=False, permissions=[commands.Permission(id=USER_ID, type=2, permission=True)])
async def permission_kwarg(ctx):
"""Say hello to the author""" # the command description can be supplied as the docstring
await ctx.respond(f"Hello {ctx.author}!")

# To learn how to add descriptions, choices to options check slash_options.py
bot.run("token")

0 comments on commit 997d364

Please sign in to comment.