Skip to content

Commit

Permalink
Merge pull request #40: Implement Command Permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
breqdev authored Aug 11, 2021
2 parents 1e2f782 + 512f515 commit 666bb7a
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 30 deletions.
60 changes: 60 additions & 0 deletions docs/permissions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Permissions
===========

Discord allows defining permissions for each slash command in a specific guild
or at the global level. Flask-Discord-Interactions provides a Permission class
and two major ways of setting the permissions of a command.

Permission class
----------------

The :class:`.Permission` class accepts either a role ID or a user ID.

.. autoclass:: flask_discord_interactions.Permission
:members:


Slash Command constructor
-------------------------

You can define permissions when defining a Slash Command. These will be
registered immediately after your Slash Command is registered.

You can set the ``default_permission``, then use the ``permissions`` parameter
to specify any overwrites.

.. code-block:: python
@discord.command(default_permission=False, permissions=[
Permission(role="786840072891662336")
])
def command_with_perms(ctx):
"You need a certain role to access this command"
return "You have permissions!"
Context object
--------------

You can also use the :meth:`.Context.overwrite_permissions` method to overwrite
the permissions for a command. By default, this is the command currently
invoked. However, you can specify a command name.

.. code-block:: python
@discord.command(default_permission=False)
def locked_command(ctx):
"Secret command that has to be unlocked"
return "You have unlocked the secret command!"
@discord.command()
def unlock_command(ctx):
"Unlock the secret command"
ctx.overwrite_permissions(
[Permission(user=ctx.author.id)], "locked_command")
return "Unlocked!"
54 changes: 54 additions & 0 deletions examples/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import sys

from flask import Flask

sys.path.insert(1, ".")

from flask_discord_interactions import DiscordInteractions, Permission


app = Flask(__name__)
discord = DiscordInteractions(app)

app.config["DISCORD_CLIENT_ID"] = os.environ["DISCORD_CLIENT_ID"]
app.config["DISCORD_PUBLIC_KEY"] = os.environ["DISCORD_PUBLIC_KEY"]
app.config["DISCORD_CLIENT_SECRET"] = os.environ["DISCORD_CLIENT_SECRET"]

discord.update_slash_commands()


@discord.command(default_permission=False, permissions=[
Permission(role="786840072891662336")
])
def command_with_perms(ctx):
"You need a certain role to access this command"

return "You have permissions!"


@discord.command(default_permission=False)
def locked_command(ctx):
"Secret command that has to be unlocked"

return "You have unlocked the secret command!"


@discord.command()
def unlock_command(ctx):
"Unlock the secret command"

ctx.overwrite_permissions(
[Permission(user=ctx.author.id)], "locked_command")

return "Unlocked!"


discord.set_route("/interactions")


discord.update_slash_commands(guild_id=os.environ["TESTING_GUILD"])


if __name__ == '__main__':
app.run()
1 change: 1 addition & 0 deletions flask_discord_interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
AsyncContext,
CommandOptionType,
ChannelType,
Permission,
Member,
User,
Role,
Expand Down
15 changes: 13 additions & 2 deletions flask_discord_interactions/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,21 @@ class SlashCommand:
annotations. Do not use with ``options``. If omitted, and if
``options`` is not provided, option descriptions default to
"No description".
default_permission
Whether the command is enabled by default. Default is True.
permissions
List of permission overwrites.
"""

def __init__(self, command, name, description, options, annotations):
def __init__(self, command, name, description, options, annotations,
default_permission=True, permissions=None):
self.command = command
self.name = name
self.description = description
self.options = options
self.annotations = annotations or {}
self.default_permission = default_permission
self.permissions = permissions or []

if self.name is None:
self.name = command.__name__
Expand Down Expand Up @@ -181,9 +188,13 @@ def dump(self):
return {
"name": self.name,
"description": self.description,
"options": self.options
"options": self.options,
"default_permission": self.default_permission
}

def dump_permissions(self):
return [permission.dump() for permission in self.permissions]


class SlashCommandSubgroup(SlashCommand):
"""
Expand Down
113 changes: 104 additions & 9 deletions flask_discord_interactions/context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List
from typing import Any, List
import inspect
import itertools

Expand Down Expand Up @@ -36,6 +36,36 @@ class ChannelType:
GUILD_STORE = 6


class Permission:
"""
An object representing a single permission overwrite.
``Permission(role='1234')`` allows users with role ID 1234 to use the
command
``Permission(user='5678')`` allows user ID 5678 to use the command
``Permission(role='9012', allow=False)`` denies users with role ID 9012
from using the command
"""


def __init__(self, role=None, user=None, allow=True):
if bool(role) == bool(user):
raise ValueError("specify only one of role or user")

self.type = 1 if role else 2
self.id = role or user
self.permission = allow

def dump(self):
return {
"type": self.type,
"id": self.id,
"permission": self.permission
}


class ContextObject:
@classmethod
def from_dict(cls, data):
Expand Down Expand Up @@ -269,9 +299,8 @@ class Context(ContextObject):
channels: List[Channel] = None
roles: List[Role] = None

client_id: str = ""
auth_headers: dict = None
base_url: str = ""
app: Any = None
discord: Any = None

custom_id: str = None
primary_id: str = None
Expand All @@ -283,9 +312,8 @@ def from_data(cls, discord=None, app=None, data={}):
data = {}

result = cls(
client_id = app.config["DISCORD_CLIENT_ID"] if app else "",
auth_headers = discord.auth_headers(app) if discord else {},
base_url = app.config["DISCORD_BASE_URL"] if app else "",
app = app,
discord = discord,
author = Member.from_dict(data.get("member", {})),
id = data.get("id"),
token = data.get("token"),
Expand All @@ -303,6 +331,10 @@ def from_data(cls, discord=None, app=None, data={}):
result.parse_resolved()
return result

@property
def auth_headers(self):
return self.discord.auth_headers(self.app)

def parse_custom_id(self):
"""
Parse the custom ID of the incoming interaction data.
Expand Down Expand Up @@ -434,8 +466,8 @@ def followup_url(self, message=None):
"@original", refers to the original message.
"""

url = (f"{self.base_url}/webhooks/"
f"{self.client_id}/{self.token}")
url = (f"{self.app.config['DISCORD_BASE_URL']}/webhooks/"
f"{self.app.config['DISCORD_CLIENT_ID']}/{self.token}")
if message is not None:
url += f"/messages/{message}"

Expand Down Expand Up @@ -498,6 +530,43 @@ def send(self, response):
response.raise_for_status()
return response.json()["id"]

def get_command(self, command_name=None):
"Get the ID of a command by name."
if command_name is None:
return self.command_id
else:
try:
return self.app.discord_commands[command_name].id
except KeyError:
raise ValueError(f"Unknown command: {command_name}")

def overwrite_permissions(self, permissions, command=None):
"""
Overwrite the permission overwrites for this command.
Parameters
----------
permissions
The new list of permission overwrites.
command
The name of the command to overwrite permissions for. If omitted,
overwrites for the invoking command.
"""

url = (
f"{self.app.config['DISCORD_BASE_URL']}/"
f"applications/{self.app.config['DISCORD_CLIENT_ID']}/"
f"guilds/{self.guild_id}/"
f"commands/{self.get_command(command)}/permissions"
)

data = [permission.dump() for permission in permissions]

response = requests.put(url, headers=self.auth_headers, json={
"permissions": data
})
response.raise_for_status()


@dataclass
class AsyncContext(Context):
Expand Down Expand Up @@ -571,5 +640,31 @@ async def send(self, response):
) as response:
return (await response.json())["id"]

async def overwrite_permissions(self, permissions, command=None):
"""
Overwrite the permission overwrites for this command.
Parameters
----------
permissions
The new list of permission overwrites.
command
The name of the command to overwrite permissions for. If omitted,
overwrites for the invoking command.
"""

url = (
f"{self.app.config['DISCORD_BASE_URL']}/"
f"applications/{self.app.config['DISCORD_CLIENT_ID']}/"
f"guilds/{self.guild_id}/"
f"commands/{self.get_command(command)}/permissions"
)

data = [permission.dump() for permission in permissions]

await self.session.put(url, headers=self.auth_headers, json={
"permissions": data
})

async def close(self):
await self.session.close()
Loading

0 comments on commit 666bb7a

Please sign in to comment.