Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Mathematics Server Discord Bot

This is the open source repository for the utility bot that manages various kinds of things on the Mathematics Discord server. With that purpose in mind, see `CONTRIBUTING.md` if you want to contribute to the bot. If you'd like to run this bot on your own server, that's fine too, but don't expect support.
Expand Down Expand Up @@ -40,6 +39,7 @@ Commands:
- `acl` -- edit Access Control Lists: permission settings for commands and other miscellaneous actions. An ACL is a formula involving users, roles, channels, categories, and boolean connectives. A command or an action can be mapped to an ACL, which will restrict who can use the command/action and where.
- `acl list` -- list ACL formulas.
- `acl show <acl>` -- display the formula for the given ACL in YAML format.
- `acl show [--pretty|-p] <acl>` -- display YAML for the given ACL using Markdown formatting with mention tags.
- ``acl set <acl> ```formula``` `` -- set the formula for the given ACL. The formula must be a code-block containing YAML.
- `acl commands` -- show all commands that are assigned to ACLs.
- `acl command <command> [acl]` -- assign the given command (fully qualified name) to the given ACL, restricting its usage to the users/channels specified in that ACL. If the ACL is omitted the command can never be used.
Expand Down
41 changes: 41 additions & 0 deletions bot/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ def parse_data(data: ACLData) -> ACLExpr:
def parse(self) -> ACLExpr:
return ACL.parse_data(self.data)

def format_markdown(self) -> str:
return self.parse().format_markdown()

if TYPE_CHECKING:

def __init__(self, *, name: str, data: ACLData, meta: Optional[str] = ...) -> None: ...
Expand Down Expand Up @@ -200,6 +203,14 @@ def evaluate(
def serialize(self) -> ACLData:
raise NotImplemented

@abstractmethod
def format_markdown(self, indent: int = 0) -> str:
raise NotImplemented

@staticmethod
def _pad(indent: int) -> str:
return " " * indent + "- "


class RoleACL(ACLExpr):
role: int
Expand All @@ -218,6 +229,9 @@ def evaluate(
def serialize(self) -> ACLData:
return {"role": self.role}

def format_markdown(self, indent: int = 0) -> str:
return f"{self._pad(indent)}role: <@&{self.role}>"


class UserACL(ACLExpr):
user: int
Expand All @@ -236,6 +250,9 @@ def evaluate(
def serialize(self) -> ACLData:
return {"user": self.user}

def format_markdown(self, indent: int = 0) -> str:
return f"{self._pad(indent)}user: <@{self.user}>"


class ChannelACL(ACLExpr):
channel: int
Expand All @@ -257,6 +274,10 @@ def evaluate(
def serialize(self) -> ACLData:
return {"channel": self.channel}

def format_markdown(self, indent: int = 0) -> str:
pad = self._pad(indent)
return f"{pad}channel: <#{self.channel}>"


class CategoryACL(ACLExpr):
category: Optional[int]
Expand All @@ -278,6 +299,11 @@ def evaluate(
def serialize(self) -> ACLData:
return {"category": self.category}

def format_markdown(self, indent: int = 0) -> str:
pad = self._pad(indent)
category = f"<#{self.category}>" if self.category else "*(none)*"
return f"{pad}category: {category}"


class NotACL(ACLExpr):
acl: ACLExpr
Expand All @@ -299,6 +325,10 @@ def evaluate(
def serialize(self) -> ACLData:
return {"not": self.acl.serialize()}

def format_markdown(self, indent: int = 0) -> str:
inner = self.acl.format_markdown(indent + 1)
return f"{self._pad(indent)}not:\n{inner}"


class AndACL(ACLExpr):
acls: List[ACLExpr]
Expand All @@ -314,6 +344,10 @@ def evaluate(
def serialize(self) -> ACLData:
return {"and": [acl.serialize() for acl in self.acls]}

def format_markdown(self, indent: int = 0) -> str:
parts = [acl.format_markdown(indent + 1) for acl in self.acls]
return f"{self._pad(indent)}and:\n" + "\n".join(parts)


class OrACL(ACLExpr):
acls: List[ACLExpr]
Expand All @@ -329,6 +363,10 @@ def evaluate(
def serialize(self) -> ACLData:
return {"or": [acl.serialize() for acl in self.acls]}

def format_markdown(self, indent: int = 0) -> str:
parts = [acl.format_markdown(indent + 1) for acl in self.acls]
return f"{self._pad(indent)}or:\n" + "\n".join(parts)


class NestedACL(ACLExpr):
acl: str
Expand All @@ -344,6 +382,9 @@ def evaluate(
def serialize(self) -> ACLData:
return {"acl": self.acl}

def format_markdown(self, indent: int = 0) -> str:
return f"{self._pad(indent)}acl: `{self.acl}`"


def evaluate_acl(
acl: Optional[str],
Expand Down
32 changes: 23 additions & 9 deletions plugins/db_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Dict, List, Optional, Set, Union, cast

import asyncpg
from discord import AllowedMentions
from discord.ext.commands import Greedy, command, group
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -95,7 +96,7 @@ def output_len(output: List[str]) -> int:
if not has_tx:
return

if await get_reaction(reply, ctx.author, {"\u21A9": False, "\u2705": True}, timeout=60):
if await get_reaction(reply, ctx.author, {"\u21a9": False, "\u2705": True}, timeout=60):
await tx.commit()
else:
await tx.rollback()
Expand Down Expand Up @@ -147,9 +148,9 @@ async def acl_list(ctx: Context) -> None:
await ctx.send(output)


@acl_command.command("show")
@acl_command.group("show", invoke_without_command=True)
@privileged
async def acl_show(ctx: Context, acl: str) -> None:
async def acl_show_group(ctx: Context, acl: Optional[str] = None) -> None:
"""Show the formula for the given ACL."""
async with AsyncSession(util.db.engine) as session:
if (data := await session.get(bot.acl.ACL, acl)) is None:
Expand All @@ -158,6 +159,19 @@ async def acl_show(ctx: Context, acl: str) -> None:
await ctx.send(format("{!b:yaml}", yaml.dump(data.data)))


@acl_show_group.command("--pretty", aliases=["-p"])
@privileged
async def acl_show_pretty(ctx: Context, acl: str) -> None:
"""
Show the given ACL using Markdown formatting and mention tags.
"""
async with AsyncSession(util.db.engine, expire_on_commit=False) as session:
if (acl_obj := await session.get(bot.acl.ACL, acl)) is None:
raise UserError(format("No such ACL: {!i}", acl))

await ctx.send(acl_obj.format_markdown(), allowed_mentions=AllowedMentions.none())


acl_override = register_action("acl_override")


Expand Down Expand Up @@ -255,9 +269,9 @@ async def acl_command_cmd(ctx: Context, command: str, acl: Optional[str]) -> Non
else:
reason = format("you do not match the meta-ACL {!i} of the new ACL", meta)
prompt = await ctx.send(
"\u26A0 You will not be able to edit this command anymore, as {}, continue?".format(reason)
"\u26a0 You will not be able to edit this command anymore, as {}, continue?".format(reason)
)
if await get_reaction(prompt, ctx.author, {"\u274C": False, "\u2705": True}, timeout=60) != True:
if await get_reaction(prompt, ctx.author, {"\u274c": False, "\u2705": True}, timeout=60) != True:
return

async with AsyncSession(util.db.engine) as session:
Expand Down Expand Up @@ -323,9 +337,9 @@ async def acl_action(ctx: Context, action: str, acl: Optional[str]) -> None:
else:
reason = format("you do not match the meta-ACL {!i} of the new ACL", meta)
prompt = await ctx.send(
"\u26A0 You will not be able to edit this action anymore, as {}, continue?".format(reason)
"\u26a0 You will not be able to edit this action anymore, as {}, continue?".format(reason)
)
if await get_reaction(prompt, ctx.author, {"\u274C": False, "\u2705": True}, timeout=60) != True:
if await get_reaction(prompt, ctx.author, {"\u274c": False, "\u2705": True}, timeout=60) != True:
return

async with AsyncSession(util.db.engine) as session:
Expand Down Expand Up @@ -386,8 +400,8 @@ async def acl_meta(ctx: Context, acl: str, meta: Optional[str]) -> None:
reason = "the meta is to be removed and you do not match the `acl_override` action"
else:
reason = format("you do not match the new meta-ACL {!i}", meta)
prompt = await ctx.send("\u26A0 You will not be able to edit this ACL anymore, as {}, continue?".format(reason))
if await get_reaction(prompt, ctx.author, {"\u274C": False, "\u2705": True}, timeout=60) != True:
prompt = await ctx.send("\u26a0 You will not be able to edit this ACL anymore, as {}, continue?".format(reason))
if await get_reaction(prompt, ctx.author, {"\u274c": False, "\u2705": True}, timeout=60) != True:
return

async with AsyncSession(util.db.engine, expire_on_commit=False) as session:
Expand Down