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

Add Slash Command Permissions support. #280

Merged
merged 6 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -333,6 +338,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 @@ -402,6 +411,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 @@ -669,6 +679,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 @@ -1016,3 +1029,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")