mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-22 02:37:57 -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:
668
redbot/core/commands/requires.py
Normal file
668
redbot/core/commands/requires.py
Normal file
@@ -0,0 +1,668 @@
|
||||
"""
|
||||
commands.requires
|
||||
=================
|
||||
This module manages the logic of resolving command permissions and
|
||||
requirements. This includes rules which override those requirements,
|
||||
as well as custom checks which can be overriden, and some special
|
||||
checks like bot permissions checks.
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
from typing import (
|
||||
Union,
|
||||
Optional,
|
||||
List,
|
||||
Callable,
|
||||
Awaitable,
|
||||
Dict,
|
||||
Any,
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import discord
|
||||
|
||||
from .converter import GuildConverter
|
||||
from .errors import BotMissingPermissions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command
|
||||
from .context import Context
|
||||
|
||||
_CommandOrCoro = TypeVar("_CommandOrCoro", Callable[..., Awaitable[Any]], Command)
|
||||
|
||||
__all__ = [
|
||||
"CheckPredicate",
|
||||
"DM_PERMS",
|
||||
"GlobalPermissionModel",
|
||||
"GuildPermissionModel",
|
||||
"PermissionModel",
|
||||
"PrivilegeLevel",
|
||||
"PermState",
|
||||
"Requires",
|
||||
"permissions_check",
|
||||
"bot_has_permissions",
|
||||
"has_permissions",
|
||||
"is_owner",
|
||||
"guildowner",
|
||||
"guildowner_or_permissions",
|
||||
"admin",
|
||||
"admin_or_permissions",
|
||||
"mod",
|
||||
"mod_or_permissions",
|
||||
]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
GlobalPermissionModel = Union[
|
||||
discord.User,
|
||||
discord.VoiceChannel,
|
||||
discord.TextChannel,
|
||||
discord.CategoryChannel,
|
||||
discord.Role,
|
||||
GuildConverter, # Unfortunately this will have to do for now
|
||||
]
|
||||
GuildPermissionModel = Union[
|
||||
discord.Member,
|
||||
discord.VoiceChannel,
|
||||
discord.TextChannel,
|
||||
discord.CategoryChannel,
|
||||
discord.Role,
|
||||
GuildConverter,
|
||||
]
|
||||
PermissionModel = Union[GlobalPermissionModel, GuildPermissionModel]
|
||||
CheckPredicate = Callable[["Context"], Union[Optional[bool], Awaitable[Optional[bool]]]]
|
||||
|
||||
# Here we are trying to model DM permissions as closely as possible. The only
|
||||
# discrepancy I've found is that users can pin messages, but they cannot delete them.
|
||||
# This means manage_messages is only half True, so it's left as False.
|
||||
# This is also the same as the permissions returned when `permissions_for` is used in DM.
|
||||
DM_PERMS = discord.Permissions.none()
|
||||
DM_PERMS.update(
|
||||
add_reactions=True,
|
||||
attach_files=True,
|
||||
embed_links=True,
|
||||
external_emojis=True,
|
||||
mention_everyone=True,
|
||||
read_message_history=True,
|
||||
read_messages=True,
|
||||
send_messages=True,
|
||||
)
|
||||
|
||||
|
||||
class PrivilegeLevel(enum.IntEnum):
|
||||
"""Enumeration for special privileges."""
|
||||
|
||||
NONE = enum.auto()
|
||||
"""No special privilege level."""
|
||||
|
||||
MOD = enum.auto()
|
||||
"""User has the mod role."""
|
||||
|
||||
ADMIN = enum.auto()
|
||||
"""User has the admin role."""
|
||||
|
||||
GUILD_OWNER = enum.auto()
|
||||
"""User is the guild level."""
|
||||
|
||||
BOT_OWNER = enum.auto()
|
||||
"""User is a bot owner."""
|
||||
|
||||
@classmethod
|
||||
async def from_ctx(cls, ctx: "Context") -> "PrivilegeLevel":
|
||||
"""Get a command author's PrivilegeLevel based on context."""
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return cls.BOT_OWNER
|
||||
elif ctx.guild is None:
|
||||
return cls.NONE
|
||||
elif ctx.author == ctx.guild.owner:
|
||||
return cls.GUILD_OWNER
|
||||
|
||||
# The following is simply an optimised way to check if the user has the
|
||||
# admin or mod role.
|
||||
guild_settings = ctx.bot.db.guild(ctx.guild)
|
||||
admin_role_id = await guild_settings.admin_role()
|
||||
mod_role_id = await guild_settings.mod_role()
|
||||
is_mod = False
|
||||
for role in ctx.author.roles:
|
||||
if role.id == admin_role_id:
|
||||
return cls.ADMIN
|
||||
elif role.id == mod_role_id:
|
||||
is_mod = True
|
||||
if is_mod:
|
||||
return cls.MOD
|
||||
|
||||
return cls.NONE
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}.{self.name}>"
|
||||
|
||||
|
||||
class PermState(enum.Enum):
|
||||
"""Enumeration for permission states used by rules."""
|
||||
|
||||
ACTIVE_ALLOW = enum.auto()
|
||||
"""This command has been actively allowed, default user checks
|
||||
should be ignored.
|
||||
"""
|
||||
|
||||
NORMAL = enum.auto()
|
||||
"""No overrides have been set for this command, make determination
|
||||
from default user checks.
|
||||
"""
|
||||
|
||||
PASSIVE_ALLOW = enum.auto()
|
||||
"""There exists a subcommand in the `ACTIVE_ALLOW` state, continue
|
||||
down the subcommand tree until we either find it or realise we're
|
||||
on the wrong branch.
|
||||
"""
|
||||
|
||||
CAUTIOUS_ALLOW = enum.auto()
|
||||
"""This command has been actively denied, but there exists a
|
||||
subcommand in the `ACTIVE_ALLOW` state. This occurs when
|
||||
`PASSIVE_ALLOW` and `ACTIVE_DENY` are combined.
|
||||
"""
|
||||
|
||||
ACTIVE_DENY = enum.auto()
|
||||
"""This command has been actively denied, terminate the command
|
||||
chain.
|
||||
"""
|
||||
|
||||
def transition_to(
|
||||
self, next_state: "PermState"
|
||||
) -> Tuple[Optional[bool], Union["PermState", Dict[bool, "PermState"]]]:
|
||||
return self.TRANSITIONS[self][next_state]
|
||||
|
||||
@classmethod
|
||||
def from_bool(cls, value: Optional[bool]) -> "PermState":
|
||||
"""Get a PermState from a bool or ``NoneType``."""
|
||||
if value is True:
|
||||
return cls.ACTIVE_ALLOW
|
||||
elif value is False:
|
||||
return cls.ACTIVE_DENY
|
||||
else:
|
||||
return cls.NORMAL
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}.{self.name}>"
|
||||
|
||||
|
||||
# Here we're defining how we transition between states.
|
||||
# The dict is in the form:
|
||||
# previous state -> this state -> Tuple[override, next state]
|
||||
# "override" is a bool describing whether or not the command should be
|
||||
# invoked. It can be None, in which case the default permission checks
|
||||
# will be used instead.
|
||||
# There is also one case where the "next state" is dependent on the
|
||||
# result of the default permission checks - the transition from NORMAL
|
||||
# to PASSIVE_ALLOW. In this case "next state" is a dict mapping the
|
||||
# permission check results to the actual next state.
|
||||
PermState.TRANSITIONS = {
|
||||
PermState.ACTIVE_ALLOW: {
|
||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.NORMAL: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.PASSIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||
},
|
||||
PermState.NORMAL: {
|
||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.NORMAL: (None, PermState.NORMAL),
|
||||
PermState.PASSIVE_ALLOW: (True, {True: PermState.NORMAL, False: PermState.PASSIVE_ALLOW}),
|
||||
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||
},
|
||||
PermState.PASSIVE_ALLOW: {
|
||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.NORMAL: (False, PermState.NORMAL),
|
||||
PermState.PASSIVE_ALLOW: (True, PermState.PASSIVE_ALLOW),
|
||||
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||
},
|
||||
PermState.CAUTIOUS_ALLOW: {
|
||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||
PermState.NORMAL: (False, PermState.ACTIVE_DENY),
|
||||
PermState.PASSIVE_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||
},
|
||||
PermState.ACTIVE_DENY: { # We can only start from ACTIVE_DENY if it is set on a cog.
|
||||
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW), # Should never happen
|
||||
PermState.NORMAL: (False, PermState.ACTIVE_DENY),
|
||||
PermState.PASSIVE_ALLOW: (False, PermState.ACTIVE_DENY), # Should never happen
|
||||
PermState.CAUTIOUS_ALLOW: (False, PermState.ACTIVE_DENY), # Should never happen
|
||||
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||
},
|
||||
}
|
||||
PermState.ALLOWED_STATES = (
|
||||
PermState.ACTIVE_ALLOW,
|
||||
PermState.PASSIVE_ALLOW,
|
||||
PermState.CAUTIOUS_ALLOW,
|
||||
)
|
||||
|
||||
|
||||
class Requires:
|
||||
"""This class describes the requirements for executing a specific command.
|
||||
|
||||
The permissions described include both bot permissions and user
|
||||
permissions.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
checks : List[Callable[[Context], Union[bool, Awaitable[bool]]]]
|
||||
A list of checks which can be overridden by rules. Use
|
||||
`Command.checks` if you would like them to never be overridden.
|
||||
privilege_level : PrivilegeLevel
|
||||
The required privilege level (bot owner, admin, etc.) for users
|
||||
to execute the command. Can be ``None``, in which case the
|
||||
`user_perms` will be used exclusively, otherwise, for levels
|
||||
other than bot owner, the user can still run the command if
|
||||
they have the required `user_perms`.
|
||||
user_perms : Optional[discord.Permissions]
|
||||
The required permissions for users to execute the command. Can
|
||||
be ``None``, in which case the `privilege_level` will be used
|
||||
exclusively, otherwise, it will pass whether the user has the
|
||||
required `privilege_level` _or_ `user_perms`.
|
||||
bot_perms : discord.Permissions
|
||||
The required bot permissions for a command to be executed. This
|
||||
is not overrideable by other conditions.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
privilege_level: Optional[PrivilegeLevel],
|
||||
user_perms: Union[Dict[str, bool], discord.Permissions, None],
|
||||
bot_perms: Union[Dict[str, bool], discord.Permissions],
|
||||
checks: List[CheckPredicate],
|
||||
):
|
||||
self.checks: List[CheckPredicate] = checks
|
||||
self.privilege_level: Optional[PrivilegeLevel] = privilege_level
|
||||
|
||||
if isinstance(user_perms, dict):
|
||||
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
|
||||
self.user_perms.update(**user_perms)
|
||||
else:
|
||||
self.user_perms = user_perms
|
||||
|
||||
if isinstance(bot_perms, dict):
|
||||
self.bot_perms: discord.Permissions = discord.Permissions.none()
|
||||
self.bot_perms.update(**bot_perms)
|
||||
else:
|
||||
self.bot_perms = bot_perms
|
||||
self.default_global_rule: PermState = PermState.NORMAL
|
||||
self._global_rules: _IntKeyDict[PermState] = _IntKeyDict()
|
||||
self._default_guild_rules: _IntKeyDict[PermState] = _IntKeyDict()
|
||||
self._guild_rules: _IntKeyDict[_IntKeyDict[PermState]] = _IntKeyDict()
|
||||
|
||||
@staticmethod
|
||||
def get_decorator(
|
||||
privilege_level: Optional[PrivilegeLevel], user_perms: Dict[str, bool]
|
||||
) -> Callable[["_CommandOrCoro"], "_CommandOrCoro"]:
|
||||
if not user_perms:
|
||||
user_perms = None
|
||||
|
||||
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
func.__requires_privilege_level__ = privilege_level
|
||||
func.__requires_user_perms__ = user_perms
|
||||
else:
|
||||
func.requires.privilege_level = privilege_level
|
||||
if user_perms is None:
|
||||
func.requires.user_perms = None
|
||||
else:
|
||||
func.requires.user_perms.update(**user_perms)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def get_rule(self, model: Union[int, PermissionModel], guild_id: int) -> PermState:
|
||||
"""Get the rule for a particular model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model : PermissionModel
|
||||
The model to get the rule for.
|
||||
guild_id : int
|
||||
The ID of the guild for the rule's scope. Set to ``0``
|
||||
for a global rule.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PermState
|
||||
The state for this rule. See the `PermState` class
|
||||
for an explanation.
|
||||
|
||||
"""
|
||||
if not isinstance(model, int):
|
||||
model = model.id
|
||||
if guild_id:
|
||||
rules = self._guild_rules.get(guild_id, _IntKeyDict())
|
||||
else:
|
||||
rules = self._global_rules
|
||||
return rules.get(model, PermState.NORMAL)
|
||||
|
||||
def set_rule(self, model_id: int, rule: PermState, guild_id: int) -> None:
|
||||
"""Set the rule for a particular model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model_id : PermissionModel
|
||||
The model to add a rule for.
|
||||
rule : PermState
|
||||
Which state this rule should be set as. See the `PermState`
|
||||
class for an explanation.
|
||||
guild_id : int
|
||||
The ID of the guild for the rule's scope. Set to ``0``
|
||||
for a global rule.
|
||||
|
||||
"""
|
||||
if guild_id:
|
||||
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict())
|
||||
else:
|
||||
rules = self._global_rules
|
||||
if rule is PermState.NORMAL:
|
||||
rules.pop(model_id, None)
|
||||
else:
|
||||
rules[model_id] = rule
|
||||
|
||||
def clear_all_rules(self, guild_id: int) -> None:
|
||||
"""Clear all rules of a particular scope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
guild_id : int
|
||||
The guild ID to clear rules for. If ``0``, this will
|
||||
clear all global rules and leave all guild rules
|
||||
untouched.
|
||||
|
||||
"""
|
||||
if guild_id:
|
||||
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict())
|
||||
else:
|
||||
rules = self._global_rules
|
||||
rules.clear()
|
||||
|
||||
def get_default_guild_rule(self, guild_id: int) -> PermState:
|
||||
"""Get the default rule for a guild."""
|
||||
return self._default_guild_rules.get(guild_id, PermState.NORMAL)
|
||||
|
||||
def set_default_guild_rule(self, guild_id: int, rule: PermState) -> None:
|
||||
"""Set the default rule for a guild."""
|
||||
self._default_guild_rules[guild_id] = rule
|
||||
|
||||
async def verify(self, ctx: "Context") -> bool:
|
||||
"""Check if the given context passes the requirements.
|
||||
|
||||
This will check the bot permissions, overrides, user permissions
|
||||
and privilege level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : "Context"
|
||||
The invkokation context to check with.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the context passes the requirements.
|
||||
|
||||
Raises
|
||||
------
|
||||
BotMissingPermissions
|
||||
If the bot is missing required permissions to run the
|
||||
command.
|
||||
CommandError
|
||||
Propogated from any permissions checks.
|
||||
|
||||
"""
|
||||
await self._verify_bot(ctx)
|
||||
# Owner-only commands are non-overrideable
|
||||
if self.privilege_level is PrivilegeLevel.BOT_OWNER:
|
||||
return await ctx.bot.is_owner(ctx.author)
|
||||
|
||||
hook_result = await ctx.bot.verify_permissions_hooks(ctx)
|
||||
if hook_result is not None:
|
||||
return hook_result
|
||||
|
||||
return await self._transition_state(ctx)
|
||||
|
||||
async def _verify_bot(self, ctx: "Context") -> None:
|
||||
if ctx.guild is None:
|
||||
bot_user = ctx.bot.user
|
||||
else:
|
||||
bot_user = ctx.guild.me
|
||||
bot_perms = ctx.channel.permissions_for(bot_user)
|
||||
if not (bot_perms.administrator or bot_perms >= self.bot_perms):
|
||||
raise BotMissingPermissions(missing=self._missing_perms(self.bot_perms, bot_perms))
|
||||
|
||||
async def _transition_state(self, ctx: "Context") -> bool:
|
||||
prev_state = ctx.permission_state
|
||||
cur_state = self._get_rule_from_ctx(ctx)
|
||||
should_invoke, next_state = prev_state.transition_to(cur_state)
|
||||
if should_invoke is None:
|
||||
# NORMAL invokation, we simply follow standard procedure
|
||||
should_invoke = await self._verify_user(ctx)
|
||||
elif isinstance(next_state, dict):
|
||||
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
|
||||
next_state = next_state[await self._verify_user(ctx)]
|
||||
|
||||
ctx.permission_state = next_state
|
||||
return should_invoke
|
||||
|
||||
async def _verify_user(self, ctx: "Context") -> bool:
|
||||
checks_pass = await self._verify_checks(ctx)
|
||||
if checks_pass is False:
|
||||
return False
|
||||
|
||||
if self.user_perms is not None:
|
||||
user_perms = ctx.channel.permissions_for(ctx.author)
|
||||
if user_perms.administrator or user_perms >= self.user_perms:
|
||||
return True
|
||||
|
||||
if self.privilege_level is not None:
|
||||
privilege_level = await PrivilegeLevel.from_ctx(ctx)
|
||||
if privilege_level >= self.privilege_level:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_rule_from_ctx(self, ctx: "Context") -> PermState:
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if ctx.guild is None:
|
||||
# We only check the user for DM channels
|
||||
rule = self._global_rules.get(author.id)
|
||||
if rule is not None:
|
||||
return rule
|
||||
return self.default_global_rule
|
||||
|
||||
rules_chain = [self._global_rules]
|
||||
guild_rules = self._guild_rules.get(ctx.guild.id)
|
||||
if guild_rules:
|
||||
rules_chain.append(guild_rules)
|
||||
|
||||
channels = []
|
||||
if author.voice is not None:
|
||||
channels.append(author.voice.channel)
|
||||
channels.append(ctx.channel)
|
||||
category = ctx.channel.category
|
||||
if category is not None:
|
||||
channels.append(category)
|
||||
|
||||
model_chain = [author, *channels, *author.roles, guild]
|
||||
|
||||
for rules in rules_chain:
|
||||
for model in model_chain:
|
||||
rule = rules.get(model.id)
|
||||
if rule is not None:
|
||||
return rule
|
||||
del model_chain[-1] # We don't check for the guild in guild rules
|
||||
|
||||
default_rule = self.get_default_guild_rule(guild.id)
|
||||
if default_rule is PermState.NORMAL:
|
||||
default_rule = self.default_global_rule
|
||||
return default_rule
|
||||
|
||||
async def _verify_checks(self, ctx: "Context") -> bool:
|
||||
if not self.checks:
|
||||
return True
|
||||
return await discord.utils.async_all(check(ctx) for check in self.checks)
|
||||
|
||||
@staticmethod
|
||||
def _get_perms_for(ctx: "Context", user: discord.abc.User) -> discord.Permissions:
|
||||
if ctx.guild is None:
|
||||
return DM_PERMS
|
||||
else:
|
||||
return ctx.channel.permissions_for(user)
|
||||
|
||||
@classmethod
|
||||
def _get_bot_perms(cls, ctx: "Context") -> discord.Permissions:
|
||||
return cls._get_perms_for(ctx, ctx.guild.me if ctx.guild else ctx.bot.user)
|
||||
|
||||
@staticmethod
|
||||
def _missing_perms(
|
||||
required: discord.Permissions, actual: discord.Permissions
|
||||
) -> discord.Permissions:
|
||||
# Explained in set theory terms:
|
||||
# Assuming R is the set of required permissions, and A is
|
||||
# the set of the user's permissions, the set of missing
|
||||
# permissions will be equal to R \ A, i.e. the relative
|
||||
# complement/difference of A with respect to R.
|
||||
relative_complement = required.value & ~actual.value
|
||||
return discord.Permissions(relative_complement)
|
||||
|
||||
@staticmethod
|
||||
def _member_as_user(member: discord.abc.User) -> discord.User:
|
||||
if isinstance(member, discord.Member):
|
||||
# noinspection PyProtectedMember
|
||||
return member._user
|
||||
return member
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Requires privilege_level={self.privilege_level!r} user_perms={self.user_perms!r} "
|
||||
f"bot_perms={self.bot_perms!r}>"
|
||||
)
|
||||
|
||||
|
||||
# check decorators
|
||||
|
||||
|
||||
def permissions_check(predicate: CheckPredicate):
|
||||
"""An overwriteable version of `discord.ext.commands.check`.
|
||||
|
||||
This has the same behaviour as `discord.ext.commands.check`,
|
||||
however this check can be ignored if the command is allowed
|
||||
through a permissions cog.
|
||||
"""
|
||||
|
||||
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||
if hasattr(func, "requires"):
|
||||
func.requires.checks.append(predicate)
|
||||
else:
|
||||
if not hasattr(func, "__requires_checks__"):
|
||||
func.__requires_checks__ = []
|
||||
# noinspection PyUnresolvedReferences
|
||||
func.__requires_checks__.append(predicate)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def bot_has_permissions(**perms: bool):
|
||||
"""Complain if the bot is missing permissions.
|
||||
|
||||
If the user tries to run the command, but the bot is missing the
|
||||
permissions, it will send a message describing which permissions
|
||||
are missing.
|
||||
|
||||
This check cannot be overridden by rules.
|
||||
"""
|
||||
|
||||
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
func.__requires_bot_perms__ = perms
|
||||
else:
|
||||
func.requires.bot_perms.update(**perms)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def has_permissions(**perms: bool):
|
||||
"""Restrict the command to users with these permissions.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return Requires.get_decorator(None, perms)
|
||||
|
||||
|
||||
def is_owner():
|
||||
"""Restrict the command to bot owners.
|
||||
|
||||
This check cannot be overridden by rules.
|
||||
"""
|
||||
return Requires.get_decorator(PrivilegeLevel.BOT_OWNER, {})
|
||||
|
||||
|
||||
def guildowner_or_permissions(**perms: bool):
|
||||
"""Restrict the command to the guild owner or users with these permissions.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return Requires.get_decorator(PrivilegeLevel.GUILD_OWNER, perms)
|
||||
|
||||
|
||||
def guildowner():
|
||||
"""Restrict the command to the guild owner.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return guildowner_or_permissions()
|
||||
|
||||
|
||||
def admin_or_permissions(**perms: bool):
|
||||
"""Restrict the command to users with the admin role or these permissions.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return Requires.get_decorator(PrivilegeLevel.ADMIN, perms)
|
||||
|
||||
|
||||
def admin():
|
||||
"""Restrict the command to users with the admin role.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return admin_or_permissions()
|
||||
|
||||
|
||||
def mod_or_permissions(**perms: bool):
|
||||
"""Restrict the command to users with the mod role or these permissions.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return Requires.get_decorator(PrivilegeLevel.MOD, perms)
|
||||
|
||||
|
||||
def mod():
|
||||
"""Restrict the command to users with the mod role.
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
return mod_or_permissions()
|
||||
|
||||
|
||||
class _IntKeyDict(Dict[int, _T]):
|
||||
"""Dict subclass which throws KeyError when a non-int key is used."""
|
||||
|
||||
def __getitem__(self, key: Any) -> _T:
|
||||
if not isinstance(key, int):
|
||||
raise TypeError("Keys must be of type `int`")
|
||||
return super().__getitem__(key)
|
||||
|
||||
def __setitem__(self, key: Any, value: _T) -> None:
|
||||
if not isinstance(key, int):
|
||||
raise TypeError("Keys must be of type `int`")
|
||||
return super().__setitem__(key, value)
|
||||
Reference in New Issue
Block a user