From 749d87c92bd21c401c3b021eb838b0a0d13d2f19 Mon Sep 17 00:00:00 2001 From: rattus-ratgirl <220043304+rattus-ratgirl@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:18:22 +0000 Subject: [PATCH 1/2] add [--pretty|-p] flag to `.acl show` --- README.md | 2 +- bot/acl.py | 30 ++++++++++++++++++++++++++++++ plugins/db_manager.py | 32 +++++++++++++++++++++++--------- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c2a503b..1032b31 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 ` -- display the formula for the given ACL in YAML format. + - `acl show [--pretty|-p] ` -- display YAML for the given ACL using Markdown formatting with mention tags. - ``acl set ```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 [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. diff --git a/bot/acl.py b/bot/acl.py index 563555e..03e3e5a 100644 --- a/bot/acl.py +++ b/bot/acl.py @@ -104,6 +104,36 @@ def parse_data(data: ACLData) -> ACLExpr: return NestedACL(data["acl"]) raise ValueError("Invalid ACL data: {!r}".format(data)) + @staticmethod + def format_markdown(data: ACLData, indent: int = 0) -> str: + """ + Return a markdown-formatted string representation of the ACL's data, + converting IDs into Discord mentions where possible. + """ + pad = " " * indent + "- " + + if "role" in data: + return f"{pad}role: <@&{data['role']}>" + elif "user" in data: + return f"{pad}user: <@{data['user']}>" + elif "channel" in data: + return f"{pad}channel: <#{data['channel']}>" + elif "category" in data: + cat = data["category"] + return f"{pad}category: {f'<#{cat}>' if cat else '*(none)*'}" + elif "not" in data: + inner = ACL.format_markdown(data["not"], indent + 1) + return f"{pad}not:\n{inner}" + elif "and" in data: + parts = [ACL.format_markdown(d, indent + 1) for d in data["and"]] + return f"{pad}and:\n" + "\n".join(parts) + elif "or" in data: + parts = [ACL.format_markdown(d, indent + 1) for d in data["or"]] + return f"{pad}or:\n" + "\n".join(parts) + elif "acl" in data: + return f"{pad}acl: `{data['acl']}`" + raise ValueError(f"Invalid ACL data: {data!r}") + def parse(self) -> ACLExpr: return ACL.parse_data(self.data) diff --git a/plugins/db_manager.py b/plugins/db_manager.py index 255d60c..8ebefce 100644 --- a/plugins/db_manager.py +++ b/plugins/db_manager.py @@ -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 @@ -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() @@ -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: @@ -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.format_markdown(acl_obj.data), allowed_mentions=AllowedMentions.none()) + + acl_override = register_action("acl_override") @@ -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: @@ -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: @@ -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: From 07a17b067bf661e2bc88f02b7db65221ed116688 Mon Sep 17 00:00:00 2001 From: rattus_ratgirl <220043304+rattus-ratgirl@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:06:55 +0000 Subject: [PATCH 2/2] refactor: add format_markdown to ACLExpr and inheritors --- bot/acl.py | 71 +++++++++++++++++++++++++------------------ plugins/db_manager.py | 2 +- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/bot/acl.py b/bot/acl.py index 03e3e5a..e5788ab 100644 --- a/bot/acl.py +++ b/bot/acl.py @@ -104,39 +104,12 @@ def parse_data(data: ACLData) -> ACLExpr: return NestedACL(data["acl"]) raise ValueError("Invalid ACL data: {!r}".format(data)) - @staticmethod - def format_markdown(data: ACLData, indent: int = 0) -> str: - """ - Return a markdown-formatted string representation of the ACL's data, - converting IDs into Discord mentions where possible. - """ - pad = " " * indent + "- " - - if "role" in data: - return f"{pad}role: <@&{data['role']}>" - elif "user" in data: - return f"{pad}user: <@{data['user']}>" - elif "channel" in data: - return f"{pad}channel: <#{data['channel']}>" - elif "category" in data: - cat = data["category"] - return f"{pad}category: {f'<#{cat}>' if cat else '*(none)*'}" - elif "not" in data: - inner = ACL.format_markdown(data["not"], indent + 1) - return f"{pad}not:\n{inner}" - elif "and" in data: - parts = [ACL.format_markdown(d, indent + 1) for d in data["and"]] - return f"{pad}and:\n" + "\n".join(parts) - elif "or" in data: - parts = [ACL.format_markdown(d, indent + 1) for d in data["or"]] - return f"{pad}or:\n" + "\n".join(parts) - elif "acl" in data: - return f"{pad}acl: `{data['acl']}`" - raise ValueError(f"Invalid ACL data: {data!r}") - 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: ... @@ -230,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 @@ -248,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 @@ -266,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 @@ -287,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] @@ -308,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 @@ -329,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] @@ -344,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] @@ -359,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 @@ -374,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], diff --git a/plugins/db_manager.py b/plugins/db_manager.py index 8ebefce..ee44ca3 100644 --- a/plugins/db_manager.py +++ b/plugins/db_manager.py @@ -169,7 +169,7 @@ async def acl_show_pretty(ctx: Context, acl: str) -> None: if (acl_obj := await session.get(bot.acl.ACL, acl)) is None: raise UserError(format("No such ACL: {!i}", acl)) - await ctx.send(ACL.format_markdown(acl_obj.data), allowed_mentions=AllowedMentions.none()) + await ctx.send(acl_obj.format_markdown(), allowed_mentions=AllowedMentions.none()) acl_override = register_action("acl_override")