mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-20 09:56:05 -05:00
Permissions redesign (#2149)
API changes: - Cogs must now inherit from `commands.Cog` (see #2151 for discussion and more details) - All functions which are not decorators in the `redbot.core.checks` module are now deprecated in favour of their counterparts in `redbot.core.utils.mod`. This is to make this module more consistent and end the confusing naming convention. - `redbot.core.checks.check_overrides` function is now gone, overrideable checks can now be created with the `@commands.permissions_check` decorator - Command, Group, Cog and Context have some new attributes and methods, but they are for internal use so shouldn't concern cog creators (unless they're making a permissions cog!). - `__permissions_check_before` and `__permissions_check_after` have been replaced: A cog method named `__permissions_hook` will be evaluated as permissions hooks in the same way `__permissions_check_before` previously was. Permissions hooks can also be added/removed/verified through the new `*_permissions_hook()` methods on the bot object, and they will be verified even when permissions is unloaded. - New utility method `redbot.core.utils.chat_formatting.humanize_list` - New dependency [`schema`](https://github.com/keleshev/schema) User-facing changes: - When a `@bot_has_permissions` check fails, the bot will respond saying what permissions were actually missing. - All YAML-related `[p]permissions` subcommands now reside under the `[p]permissions acl` sub-group (tbh I still think the whole cog has too many top-level commands) - The YAML schema for these commands has been changed - A rule cannot be set as allow and deny at the same time (previously this would just default to allow) Documentation: - New documentation for `redbot.core.commands.requires` and `redbot.core.checks` modules - Renewed documentation for the permissions cog - `sphinx.ext.doctest` is now enabled Note: standard discord.py checks will still behave exactly the same way, in fact they are checked before `Requires` is looked at, so they are not overrideable. Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
from .permissions import Permissions
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Permissions(bot))
|
||||
async def setup(bot):
|
||||
cog = Permissions(bot)
|
||||
await cog.initialize()
|
||||
# It's important that these listeners are added prior to load, so
|
||||
# the permissions commands themselves have rules added.
|
||||
# Automatic listeners being added in add_cog happen in arbitrary
|
||||
# order, so we want to circumvent that.
|
||||
bot.add_listener(cog.cog_added, "on_cog_add")
|
||||
bot.add_listener(cog.command_added, "on_command_add")
|
||||
bot.add_cog(cog)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
from typing import NamedTuple, Union, Optional
|
||||
from redbot.core import commands
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class CogOrCommand(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||
ret = ctx.bot.get_cog(arg)
|
||||
if ret:
|
||||
return "cogs", ret.__class__.__name__
|
||||
ret = ctx.bot.get_command(arg)
|
||||
if ret:
|
||||
return "commands", ret.qualified_name
|
||||
class CogOrCommand(NamedTuple):
|
||||
type: str
|
||||
name: str
|
||||
obj: Union[commands.Command, commands.Cog]
|
||||
|
||||
# noinspection PyArgumentList
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, arg: str) -> "CogOrCommand":
|
||||
cog = ctx.bot.get_cog(arg)
|
||||
if cog:
|
||||
return cls(type="COG", name=cog.__class__.__name__, obj=cog)
|
||||
cmd = ctx.bot.get_command(arg)
|
||||
if cmd:
|
||||
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
|
||||
|
||||
raise commands.BadArgument(
|
||||
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
||||
@@ -17,28 +23,34 @@ class CogOrCommand(commands.Converter):
|
||||
)
|
||||
|
||||
|
||||
class RuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
class RuleType:
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, arg: str) -> bool:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
return True
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
return False
|
||||
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class ClearableRuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
class ClearableRuleType:
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, arg: str) -> Optional[bool]:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
return True
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
return False
|
||||
if arg.lower() in ("clear", "reset"):
|
||||
return "clear"
|
||||
return None
|
||||
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
||||
"".format(arg=arg)
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
|
||||
"remove the rule".format(arg=arg)
|
||||
)
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
from redbot.core import commands
|
||||
from redbot.core.config import Config
|
||||
from .resolvers import entries_from_ctx, resolve_lists
|
||||
|
||||
# This has optimizations in it that may not hold True if other parts of the permission
|
||||
# model are changed from the state they are in currently.
|
||||
# (commit hash ~ 3bcf375204c22271ad3ed1fc059b598b751aa03f)
|
||||
#
|
||||
# This is primarily to help with the performance of the help formatter
|
||||
|
||||
# This is less efficient if only checking one command,
|
||||
# but is much faster for checking all of them.
|
||||
|
||||
|
||||
async def mass_resolve(*, ctx: commands.Context, config: Config):
|
||||
"""
|
||||
Get's all the permission cog interactions for all loaded commands
|
||||
in the given context.
|
||||
"""
|
||||
|
||||
owner_settings = await config.owner_models()
|
||||
guild_owner_settings = await config.guild(ctx.guild).owner_models() if ctx.guild else None
|
||||
|
||||
ret = {"allowed": [], "denied": [], "default": []}
|
||||
|
||||
for cogname, cog in ctx.bot.cogs.items():
|
||||
|
||||
cog_setting = resolve_cog_or_command(
|
||||
objname=cogname, models=owner_settings, ctx=ctx, typ="cogs"
|
||||
)
|
||||
if cog_setting is None and guild_owner_settings:
|
||||
cog_setting = resolve_cog_or_command(
|
||||
objname=cogname, models=guild_owner_settings, ctx=ctx, typ="cogs"
|
||||
)
|
||||
|
||||
for command in [c for c in ctx.bot.all_commands.values() if c.instance is cog]:
|
||||
resolution = recursively_resolve(
|
||||
com_or_group=command,
|
||||
o_models=owner_settings,
|
||||
g_models=guild_owner_settings,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
for com, resolved in resolution:
|
||||
if resolved is None:
|
||||
resolved = cog_setting
|
||||
if resolved is True:
|
||||
ret["allowed"].append(com)
|
||||
elif resolved is False:
|
||||
ret["denied"].append(com)
|
||||
else:
|
||||
ret["default"].append(com)
|
||||
|
||||
ret = {k: set(v) for k, v in ret.items()}
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def recursively_resolve(*, com_or_group, o_models, g_models, ctx, override=False):
|
||||
ret = []
|
||||
if override:
|
||||
current = False
|
||||
else:
|
||||
current = resolve_cog_or_command(
|
||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||
)
|
||||
if current is None and g_models:
|
||||
current = resolve_cog_or_command(
|
||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||
)
|
||||
ret.append((com_or_group, current))
|
||||
if isinstance(com_or_group, commands.Group):
|
||||
for com in com_or_group.commands:
|
||||
ret.extend(
|
||||
recursively_resolve(
|
||||
com_or_group=com,
|
||||
o_models=o_models,
|
||||
g_models=g_models,
|
||||
ctx=ctx,
|
||||
override=(current is False),
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def resolve_cog_or_command(*, typ, ctx, objname, models: dict) -> bool:
|
||||
"""
|
||||
Resolves models in order.
|
||||
"""
|
||||
|
||||
resolved = None
|
||||
|
||||
if objname in models.get(typ, {}):
|
||||
blacklist = models[typ][objname].get("deny", [])
|
||||
whitelist = models[typ][objname].get("allow", [])
|
||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
resolved = models[typ][objname].get("default", None)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
import types
|
||||
import contextlib
|
||||
import asyncio
|
||||
import logging
|
||||
from redbot.core import commands
|
||||
|
||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
||||
|
||||
|
||||
def entries_from_ctx(ctx: commands.Context) -> tuple:
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
# entries now contains the following (in order) (if applicable)
|
||||
# author.id
|
||||
# author.voice.voice_channel.id
|
||||
# channel.id
|
||||
# role.id for each role (highest to lowest)
|
||||
# (implicitly) guild.id because
|
||||
# the @everyone role shares an id with the guild
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
||||
"""
|
||||
Returns the value from a check if it is valid
|
||||
"""
|
||||
|
||||
val = None
|
||||
# let's not spam the console with improperly made 3rd party checks
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(check):
|
||||
val = await check(ctx, level=level)
|
||||
else:
|
||||
val = check(ctx, level=level)
|
||||
except Exception as e:
|
||||
# but still provide a way to view it (run with debug flag)
|
||||
log.debug(str(e))
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
|
||||
"""
|
||||
Resolves models in order.
|
||||
"""
|
||||
|
||||
cmd_name = ctx.command.qualified_name
|
||||
cog_name = ctx.cog.__class__.__name__
|
||||
|
||||
resolved = None
|
||||
|
||||
to_iter = (("commands", cmd_name), ("cogs", cog_name))
|
||||
|
||||
for model_name, ctx_attr in to_iter:
|
||||
if ctx_attr in models.get(model_name, {}):
|
||||
blacklist = models[model_name][ctx_attr].get("deny", [])
|
||||
whitelist = models[model_name][ctx_attr].get("allow", [])
|
||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
resolved = models[model_name][ctx_attr].get("default", None)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
|
||||
"""
|
||||
resolves specific lists
|
||||
"""
|
||||
for entry in entries_from_ctx(ctx):
|
||||
if entry in whitelist:
|
||||
return True
|
||||
if entry in blacklist:
|
||||
return False
|
||||
return None
|
||||
@@ -1,19 +0,0 @@
|
||||
cogs:
|
||||
Admin:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
deny:
|
||||
- 96733288462286848
|
||||
Audio:
|
||||
allow:
|
||||
- 133049272517001216
|
||||
default: deny
|
||||
commands:
|
||||
cleanup bot:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
default: deny
|
||||
ping:
|
||||
deny:
|
||||
- 96733288462286848
|
||||
default: allow
|
||||
@@ -1,67 +0,0 @@
|
||||
import io
|
||||
import yaml
|
||||
import pathlib
|
||||
import discord
|
||||
|
||||
|
||||
def yaml_template() -> dict:
|
||||
template_fp = pathlib.Path(__file__).parent / "template.yaml"
|
||||
|
||||
with template_fp.open() as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
async def yamlset_acl(ctx, *, config, update):
|
||||
_fp = io.BytesIO()
|
||||
await ctx.message.attachments[0].save(_fp)
|
||||
|
||||
try:
|
||||
data = yaml.safe_load(_fp)
|
||||
except yaml.YAMLError:
|
||||
_fp.close()
|
||||
del _fp
|
||||
raise
|
||||
|
||||
old_data = await config()
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k == "default":
|
||||
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
|
||||
|
||||
if not update:
|
||||
continue
|
||||
try:
|
||||
if isinstance(old_data[outer][ok][k], list):
|
||||
data[outer][ok][k].extend(old_data[outer][ok][k])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
await config.set(data)
|
||||
|
||||
|
||||
async def yamlget_acl(ctx, *, config):
|
||||
data = await config()
|
||||
removals = []
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k != "default":
|
||||
continue
|
||||
if v is True:
|
||||
data[outer][ok][k] = "allow"
|
||||
elif v is False:
|
||||
data[outer][ok][k] = "deny"
|
||||
else:
|
||||
removals.append((outer, ok, k))
|
||||
|
||||
for tup in removals:
|
||||
o, i, k = tup
|
||||
data[o][i].pop(k, None)
|
||||
|
||||
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
|
||||
_fp.seek(0)
|
||||
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
|
||||
_fp.close()
|
||||
Reference in New Issue
Block a user