mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -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:
parent
f07b78bd0d
commit
0870403299
14
Pipfile.lock
generated
14
Pipfile.lock
generated
@ -236,6 +236,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.1.2"
|
"version": "==0.1.2"
|
||||||
},
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||||
@ -597,6 +604,13 @@
|
|||||||
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.6'",
|
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.6'",
|
||||||
"version": "==2.19.1"
|
"version": "==2.19.1"
|
||||||
},
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
|||||||
@ -8,100 +8,90 @@ Permissions Cog Reference
|
|||||||
How it works
|
How it works
|
||||||
------------
|
------------
|
||||||
|
|
||||||
When loaded, the permissions cog will allow you
|
When loaded, the permissions cog will allow you to define extra custom rules for who can use a
|
||||||
to define extra custom rules for who can use a command
|
command.
|
||||||
|
|
||||||
If no applicable rules are found, the command will behave as if
|
If no applicable rules are found, the command will behave normally.
|
||||||
the cog was not loaded.
|
|
||||||
|
Rules can also be added to cogs, which will affect all commands from that cog. The cog name can be
|
||||||
|
found from the help menu.
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
Rule priority
|
Rule priority
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Rules set will be checked in the following order
|
Rules set for subcommands will take precedence over rules set for the parent commands, which
|
||||||
|
lastly take precedence over rules set for the cog. So for example, if a user is denied the Core
|
||||||
|
cog, but allowed the ``[p]set token`` command, the user will not be able to use any command in the
|
||||||
|
Core cog except for ``[p]set token``.
|
||||||
|
|
||||||
|
In terms of scope, global rules will be checked first, then server rules.
|
||||||
|
|
||||||
1. Owner level command specific settings
|
For each of those, the first rule pertaining to one of the following models will be used:
|
||||||
2. Owner level cog specific settings
|
|
||||||
3. Server level command specific settings
|
|
||||||
4. Server level cog specific settings
|
|
||||||
|
|
||||||
For each of those, settings have varying priorities (listed below, highest to lowest priority)
|
1. User
|
||||||
|
2. Voice channel
|
||||||
|
3. Text channel
|
||||||
|
4. Channel category
|
||||||
|
5. Roles, highest to lowest
|
||||||
|
6. Server (can only be in global rules)
|
||||||
|
7. Default rules
|
||||||
|
|
||||||
1. User whitelist
|
In private messages, only global rules about a user will be checked.
|
||||||
2. User blacklist
|
|
||||||
3. Voice Channel whitelist
|
|
||||||
4. Voice Channel blacklist
|
|
||||||
5. Text Channel whitelist
|
|
||||||
6. Text Channel blacklist
|
|
||||||
7. Role settings (see below)
|
|
||||||
8. Server whitelist
|
|
||||||
9. Server blacklist
|
|
||||||
10. Default settings
|
|
||||||
|
|
||||||
For the role whitelist and blacklist settings,
|
|
||||||
roles will be checked individually in order from highest to lowest role the user has
|
|
||||||
Each role will be checked for whitelist, then blacklist. The first role with a setting
|
|
||||||
found will be the one used.
|
|
||||||
|
|
||||||
-------------------------
|
-------------------------
|
||||||
Setting Rules from a file
|
Setting Rules From a File
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The permissions cog can set rules from a yaml file:
|
The permissions cog can also set, display or update rules with a YAML file with the
|
||||||
All entries are based on ID.
|
``[p]permissions yaml`` command. Models must be represented by ID. Rules must be ``true`` for
|
||||||
An example of the expected format is shown below.
|
allow, or ``false`` for deny. Here is an example:
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
cogs:
|
COG:
|
||||||
Admin:
|
Admin:
|
||||||
allow:
|
78631113035100160: true
|
||||||
- 78631113035100160
|
96733288462286848: false
|
||||||
deny:
|
|
||||||
- 96733288462286848
|
|
||||||
Audio:
|
Audio:
|
||||||
allow:
|
133049272517001216: true
|
||||||
- 133049272517001216
|
default: false
|
||||||
default: deny
|
COMMAND:
|
||||||
commands:
|
|
||||||
cleanup bot:
|
cleanup bot:
|
||||||
allow:
|
78631113035100160: true
|
||||||
- 78631113035100160
|
default: false
|
||||||
default: deny
|
|
||||||
ping:
|
ping:
|
||||||
deny:
|
96733288462286848: false
|
||||||
- 96733288462286848
|
default: true
|
||||||
default: allow
|
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
Example configurations
|
Example configurations
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Locking Audio cog to approved server(s) as a bot owner
|
Locking the ``[p]play`` command to approved server(s) as a bot owner:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setglobaldefault Audio deny
|
[p]permissions setglobaldefault play deny
|
||||||
[p]permissions addglobalrule allow Audio [server ID or name]
|
[p]permissions addglobalrule allow play [server ID or name]
|
||||||
|
|
||||||
Locking Audio to specific voice channel(s) as a serverowner or admin:
|
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setguilddefault deny play
|
[p]permissions setserverdefault deny play
|
||||||
[p]permissions setguilddefault deny "playlist start"
|
[p]permissions setserverdefault deny "playlist start"
|
||||||
[p]permissions addguildrule allow play [voice channel ID or name]
|
[p]permissions addserverrule allow play [voice channel ID or name]
|
||||||
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
|
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
Allowing extra roles to use cleanup
|
Allowing extra roles to use ``[p]cleanup``:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions addguildrule allow Cleanup [role ID]
|
[p]permissions addserverrule allow cleanup [role ID]
|
||||||
|
|
||||||
Preventing cleanup from being used in channels where message history is important:
|
Preventing ``[p]cleanup`` from being used in channels where message history is important:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions addguildrule deny Cleanup [channel ID or mention]
|
[p]permissions addserverrule deny cleanup [channel ID or mention]
|
||||||
|
|||||||
10
docs/conf.py
10
docs/conf.py
@ -39,6 +39,7 @@ extensions = [
|
|||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx.ext.napoleon",
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx.ext.doctest",
|
||||||
"sphinxcontrib.asyncio",
|
"sphinxcontrib.asyncio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -197,9 +198,16 @@ texinfo_documents = [
|
|||||||
linkcheck_ignore = [r"https://java.com*"]
|
linkcheck_ignore = [r"https://java.com*"]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# -- Options for extensions -----------------------------------------------
|
||||||
|
|
||||||
|
# Intersphinx
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"python": ("https://docs.python.org/3.6", None),
|
"python": ("https://docs.python.org/3.6", None),
|
||||||
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
||||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Doctest
|
||||||
|
# If this string is non-empty, all blocks with ``>>>`` in them will be
|
||||||
|
# tested, not just the ones explicitly marked with ``.. doctest::``
|
||||||
|
doctest_test_doctest_blocks = ""
|
||||||
|
|||||||
11
docs/framework_checks.rst
Normal file
11
docs/framework_checks.rst
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.. _checks:
|
||||||
|
|
||||||
|
========================
|
||||||
|
Command Check Decorators
|
||||||
|
========================
|
||||||
|
|
||||||
|
The following are all decorators for commands, which add restrictions to where and when they can be
|
||||||
|
run.
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.checks
|
||||||
|
:members:
|
||||||
@ -21,3 +21,6 @@ extend functionlities used throughout the bot, as outlined below.
|
|||||||
|
|
||||||
.. autoclass:: redbot.core.commands.Context
|
.. autoclass:: redbot.core.commands.Context
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.commands.requires
|
||||||
|
:members: PrivilegeLevel, PermState, Requires
|
||||||
|
|||||||
@ -33,14 +33,15 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
guide_data_conversion
|
guide_data_conversion
|
||||||
framework_bank
|
framework_bank
|
||||||
framework_bot
|
framework_bot
|
||||||
|
framework_checks
|
||||||
framework_cogmanager
|
framework_cogmanager
|
||||||
|
framework_commands
|
||||||
framework_config
|
framework_config
|
||||||
framework_datamanager
|
framework_datamanager
|
||||||
framework_downloader
|
framework_downloader
|
||||||
framework_events
|
framework_events
|
||||||
framework_i18n
|
framework_i18n
|
||||||
framework_modlog
|
framework_modlog
|
||||||
framework_commands
|
|
||||||
framework_rpc
|
framework_rpc
|
||||||
framework_utils
|
framework_utils
|
||||||
|
|
||||||
|
|||||||
@ -30,3 +30,5 @@ colorama.init()
|
|||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
|
warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
|
||||||
|
# Prevent discord PyNaCl missing warning
|
||||||
|
discord.voice_client.VoiceClient.warn_nacl = False
|
||||||
|
|||||||
@ -38,8 +38,9 @@ RUNNING_ANNOUNCEMENT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Admin:
|
class Admin(commands.Cog):
|
||||||
def __init__(self, config=Config):
|
def __init__(self, config=Config):
|
||||||
|
super().__init__()
|
||||||
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||||
|
|
||||||
self.conf.register_global(serverlocked=False)
|
self.conf.register_global(serverlocked=False)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ _ = Translator("Alias", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Alias:
|
class Alias(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Alias
|
Alias
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ class Alias:
|
|||||||
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._aliases = Config.get_conf(self, 8927348724)
|
self._aliases = Config.get_conf(self, 8927348724)
|
||||||
|
|
||||||
|
|||||||
@ -27,8 +27,9 @@ __author__ = ["aikaterna", "billy/bollo/ati"]
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Audio:
|
class Audio(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
||||||
|
|
||||||
|
|||||||
@ -54,10 +54,11 @@ def check_global_setting_admin():
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Bank:
|
class Bank(commands.Cog):
|
||||||
"""Bank"""
|
"""Bank"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
# SECTION commands
|
# SECTION commands
|
||||||
|
|||||||
@ -14,10 +14,11 @@ _ = Translator("Cleanup", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Cleanup:
|
class Cleanup(commands.Cog):
|
||||||
"""Commands for cleaning messages"""
|
"""Commands for cleaning messages"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -172,12 +172,13 @@ class CommandObj:
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class CustomCommands:
|
class CustomCommands(commands.Cog):
|
||||||
"""Custom commands
|
"""Custom commands
|
||||||
|
|
||||||
Creates commands used to display text"""
|
Creates commands used to display text"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.key = 414589031223512
|
self.key = 414589031223512
|
||||||
self.config = Config.get_conf(self, self.key)
|
self.config = Config.get_conf(self, self.key)
|
||||||
|
|||||||
@ -11,12 +11,13 @@ _ = Translator("DataConverter", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class DataConverter:
|
class DataConverter(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Cog for importing Red v2 Data
|
Cog for importing Red v2 Data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
|
|||||||
@ -23,8 +23,9 @@ _ = Translator("Downloader", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Downloader:
|
class Downloader(commands.Cog):
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
||||||
|
|||||||
@ -105,7 +105,7 @@ class SetParser:
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Economy:
|
class Economy(commands.Cog):
|
||||||
"""Economy
|
"""Economy
|
||||||
|
|
||||||
Get rich and have fun with imaginary currency!"""
|
Get rich and have fun with imaginary currency!"""
|
||||||
@ -128,6 +128,7 @@ class Economy:
|
|||||||
default_user_settings = default_member_settings
|
default_user_settings = default_member_settings
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.file_path = "data/economy/settings.json"
|
self.file_path = "data/economy/settings.json"
|
||||||
self.config = Config.get_conf(self, 1256844281)
|
self.config = Config.get_conf(self, 1256844281)
|
||||||
|
|||||||
@ -10,10 +10,11 @@ _ = Translator("Filter", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Filter:
|
class Filter(commands.Cog):
|
||||||
"""Filter-related commands"""
|
"""Filter-related commands"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, 4766951341)
|
self.settings = Config.get_conf(self, 4766951341)
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
|
|||||||
@ -33,10 +33,11 @@ class RPSParser:
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class General:
|
class General(commands.Cog):
|
||||||
"""General commands."""
|
"""General commands."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.stopwatches = {}
|
self.stopwatches = {}
|
||||||
self.ball = [
|
self.ball = [
|
||||||
_("As I see it, yes"),
|
_("As I see it, yes"),
|
||||||
|
|||||||
@ -11,12 +11,13 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Image:
|
class Image(commands.Cog):
|
||||||
"""Image related commands."""
|
"""Image related commands."""
|
||||||
|
|
||||||
default_global = {"imgur_client_id": None}
|
default_global = {"imgur_client_id": None}
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
||||||
self.settings.register_global(**self.default_global)
|
self.settings.register_global(**self.default_global)
|
||||||
|
|||||||
@ -26,7 +26,7 @@ def mod_or_voice_permissions(**perms):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.permissions_check(pred)
|
||||||
|
|
||||||
|
|
||||||
def admin_or_voice_permissions(**perms):
|
def admin_or_voice_permissions(**perms):
|
||||||
@ -48,7 +48,7 @@ def admin_or_voice_permissions(**perms):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.permissions_check(pred)
|
||||||
|
|
||||||
|
|
||||||
def bot_has_voice_permissions(**perms):
|
def bot_has_voice_permissions(**perms):
|
||||||
|
|||||||
@ -18,7 +18,7 @@ _ = Translator("Mod", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Mod:
|
class Mod(commands.Cog):
|
||||||
"""Moderation tools."""
|
"""Moderation tools."""
|
||||||
|
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
@ -38,6 +38,7 @@ class Mod:
|
|||||||
default_user_settings = {"past_names": []}
|
default_user_settings = {"past_names": []}
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, 4961522000, force_registration=True)
|
self.settings = Config.get_conf(self, 4961522000, force_registration=True)
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,11 @@ _ = Translator("ModLog", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class ModLog:
|
class ModLog(commands.Cog):
|
||||||
"""Log for mod actions"""
|
"""Log for mod actions"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
async def setup(bot):
|
||||||
bot.add_cog(Permissions(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 redbot.core import commands
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
|
|
||||||
class CogOrCommand(commands.Converter):
|
class CogOrCommand(NamedTuple):
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
type: str
|
||||||
ret = ctx.bot.get_cog(arg)
|
name: str
|
||||||
if ret:
|
obj: Union[commands.Command, commands.Cog]
|
||||||
return "cogs", ret.__class__.__name__
|
|
||||||
ret = ctx.bot.get_command(arg)
|
# noinspection PyArgumentList
|
||||||
if ret:
|
@classmethod
|
||||||
return "commands", ret.qualified_name
|
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(
|
raise commands.BadArgument(
|
||||||
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
'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):
|
class RuleType:
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: commands.Context, arg: str) -> bool:
|
||||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
return "allow"
|
return True
|
||||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
return "deny"
|
return False
|
||||||
|
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClearableRuleType(commands.Converter):
|
class ClearableRuleType:
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: commands.Context, arg: str) -> Optional[bool]:
|
||||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
return "allow"
|
return True
|
||||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
return "deny"
|
return False
|
||||||
if arg.lower() in ("clear", "reset"):
|
if arg.lower() in ("clear", "reset"):
|
||||||
return "clear"
|
return None
|
||||||
|
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
|
||||||
"".format(arg=arg)
|
"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()
|
|
||||||
@ -20,7 +20,7 @@ log = logging.getLogger("red.reports")
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Reports:
|
class Reports(commands.Cog):
|
||||||
|
|
||||||
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ class Reports:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
||||||
self.config.register_guild(**self.default_guild_settings)
|
self.config.register_guild(**self.default_guild_settings)
|
||||||
|
|||||||
@ -35,7 +35,7 @@ _ = Translator("Streams", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Streams:
|
class Streams(commands.Cog):
|
||||||
|
|
||||||
global_defaults = {"tokens": {}, "streams": [], "communities": []}
|
global_defaults = {"tokens": {}, "streams": [], "communities": []}
|
||||||
|
|
||||||
@ -44,6 +44,7 @@ class Streams:
|
|||||||
role_defaults = {"mention": False}
|
role_defaults = {"mention": False}
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.db = Config.get_conf(self, 26262626)
|
self.db = Config.get_conf(self, 26262626)
|
||||||
|
|
||||||
self.db.register_global(**self.global_defaults)
|
self.db.register_global(**self.global_defaults)
|
||||||
|
|||||||
@ -23,10 +23,11 @@ class InvalidListError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Trivia:
|
class Trivia(commands.Cog):
|
||||||
"""Play trivia with friends!"""
|
"""Play trivia with friends!"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.trivia_sessions = []
|
self.trivia_sessions = []
|
||||||
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
|
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ _ = Translator("Warnings", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Warnings:
|
class Warnings(commands.Cog):
|
||||||
"""A warning system for Red"""
|
"""A warning system for Red"""
|
||||||
|
|
||||||
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
|
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
|
||||||
@ -28,6 +28,7 @@ class Warnings:
|
|||||||
default_member = {"total_points": 0, "status": "", "warnings": {}}
|
default_member = {"total_points": 0, "status": "", "warnings": {}}
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.config = Config.get_conf(self, identifier=5757575755)
|
self.config = Config.get_conf(self, identifier=5757575755)
|
||||||
self.config.register_guild(**self.default_guild)
|
self.config.register_guild(**self.default_guild)
|
||||||
self.config.register_member(**self.default_member)
|
self.config.register_member(**self.default_member)
|
||||||
|
|||||||
@ -5,17 +5,12 @@ from collections import Counter
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib.machinery import ModuleSpec
|
from importlib.machinery import ModuleSpec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union
|
from typing import Optional, Union, List
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import sys
|
import sys
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
|
|
||||||
# This supresses the PyNaCl warning that isn't relevant here
|
|
||||||
from discord.voice_client import VoiceClient
|
|
||||||
|
|
||||||
VoiceClient.warn_nacl = False
|
|
||||||
|
|
||||||
from .cog_manager import CogManager
|
from .cog_manager import CogManager
|
||||||
from . import Config, i18n, commands
|
from . import Config, i18n, commands
|
||||||
from .rpc import RPCMixin
|
from .rpc import RPCMixin
|
||||||
@ -124,6 +119,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
self.add_command(help_)
|
self.add_command(help_)
|
||||||
|
|
||||||
self._sentry_mgr = None
|
self._sentry_mgr = None
|
||||||
|
self._permissions_hooks: List[commands.CheckPredicate] = []
|
||||||
|
|
||||||
def enable_sentry(self):
|
def enable_sentry(self):
|
||||||
"""Enable Sentry logging for Red."""
|
"""Enable Sentry logging for Red."""
|
||||||
@ -200,7 +196,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
async def get_context(self, message, *, cls=commands.Context):
|
async def get_context(self, message, *, cls=commands.Context):
|
||||||
return await super().get_context(message, cls=cls)
|
return await super().get_context(message, cls=cls)
|
||||||
|
|
||||||
def list_packages(self):
|
@staticmethod
|
||||||
|
def list_packages():
|
||||||
"""Lists packages present in the cogs the folder"""
|
"""Lists packages present in the cogs the folder"""
|
||||||
return os.listdir("cogs")
|
return os.listdir("cogs")
|
||||||
|
|
||||||
@ -234,7 +231,26 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
|
|
||||||
self.extensions[name] = lib
|
self.extensions[name] = lib
|
||||||
|
|
||||||
def remove_cog(self, cogname):
|
def remove_cog(self, cogname: str):
|
||||||
|
cog = self.get_cog(cogname)
|
||||||
|
if cog is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for when in ("before", "after"):
|
||||||
|
try:
|
||||||
|
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.remove_permissions_hook(hook, when)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.remove_permissions_hook(hook)
|
||||||
|
|
||||||
super().remove_cog(cogname)
|
super().remove_cog(cogname)
|
||||||
|
|
||||||
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
|
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
|
||||||
@ -365,9 +381,19 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
|
|
||||||
await destination.send(content=content, **kwargs)
|
await destination.send(content=content, **kwargs)
|
||||||
|
|
||||||
def add_cog(self, cog):
|
def add_cog(self, cog: commands.Cog):
|
||||||
|
if not isinstance(cog, commands.Cog):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The {cog.__class__.__name__} cog in the {cog.__module__} package does "
|
||||||
|
f"not inherit from the commands.Cog base class. The cog author must update "
|
||||||
|
f"the cog to adhere to this requirement."
|
||||||
|
)
|
||||||
|
if not hasattr(cog, "requires"):
|
||||||
|
commands.Cog.__init__(cog)
|
||||||
for attr in dir(cog):
|
for attr in dir(cog):
|
||||||
_attr = getattr(cog, attr)
|
_attr = getattr(cog, attr)
|
||||||
|
if attr == f"_{cog.__class__.__name__}__permissions_hook":
|
||||||
|
self.add_permissions_hook(_attr)
|
||||||
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
|
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
|
||||||
_attr, commands.Command
|
_attr, commands.Command
|
||||||
):
|
):
|
||||||
@ -380,6 +406,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
"http://red-discordbot.readthedocs.io/en/v3-develop/framework_commands.html"
|
"http://red-discordbot.readthedocs.io/en/v3-develop/framework_commands.html"
|
||||||
)
|
)
|
||||||
super().add_cog(cog)
|
super().add_cog(cog)
|
||||||
|
self.dispatch("cog_add", cog)
|
||||||
|
|
||||||
def add_command(self, command: commands.Command):
|
def add_command(self, command: commands.Command):
|
||||||
if not isinstance(command, commands.Command):
|
if not isinstance(command, commands.Command):
|
||||||
@ -388,6 +415,76 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
super().add_command(command)
|
super().add_command(command)
|
||||||
self.dispatch("command_add", command)
|
self.dispatch("command_add", command)
|
||||||
|
|
||||||
|
def clear_permission_rules(self, guild_id: Optional[int]) -> None:
|
||||||
|
"""Clear all permission overrides in a scope.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild_id : Optional[int]
|
||||||
|
The guild ID to wipe permission overrides for. If
|
||||||
|
``None``, this will clear all global rules and leave all
|
||||||
|
guild rules untouched.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for cog in self.cogs.values():
|
||||||
|
cog.requires.clear_all_rules(guild_id)
|
||||||
|
for command in self.walk_commands():
|
||||||
|
command.requires.clear_all_rules(guild_id)
|
||||||
|
|
||||||
|
def add_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
||||||
|
"""Add a permissions hook.
|
||||||
|
|
||||||
|
Permissions hooks are check predicates which are called before
|
||||||
|
calling `Requires.verify`, and they can optionally return an
|
||||||
|
override: ``True`` to allow, ``False`` to deny, and ``None`` to
|
||||||
|
default to normal behaviour.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hook
|
||||||
|
A command check predicate which returns ``True``, ``False``
|
||||||
|
or ``None``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._permissions_hooks.append(hook)
|
||||||
|
|
||||||
|
def remove_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
||||||
|
"""Remove a permissions hook.
|
||||||
|
|
||||||
|
Parameters are the same as those in `add_permissions_hook`.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the permissions hook has not been added.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._permissions_hooks.remove(hook)
|
||||||
|
|
||||||
|
async def verify_permissions_hooks(self, ctx: commands.Context) -> Optional[bool]:
|
||||||
|
"""Run permissions hooks.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : commands.Context
|
||||||
|
The context for the command being invoked.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[bool]
|
||||||
|
``False`` if any hooks returned ``False``, ``True`` if any
|
||||||
|
hooks return ``True`` and none returned ``False``, ``None``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
hook_results = []
|
||||||
|
for hook in self._permissions_hooks:
|
||||||
|
result = await discord.utils.maybe_coroutine(hook, ctx)
|
||||||
|
if result is not None:
|
||||||
|
hook_results.append(result)
|
||||||
|
if hook_results:
|
||||||
|
return all(hook_results)
|
||||||
|
|
||||||
|
|
||||||
class Red(RedBase, discord.AutoShardedClient):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,126 +1,77 @@
|
|||||||
|
import warnings
|
||||||
|
from typing import Awaitable, TYPE_CHECKING, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
|
||||||
|
|
||||||
|
from .commands import (
|
||||||
async def check_overrides(ctx, *, level):
|
bot_has_permissions,
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
has_permissions,
|
||||||
return True
|
is_owner,
|
||||||
perm_cog = ctx.bot.get_cog("Permissions")
|
guildowner,
|
||||||
if not perm_cog or ctx.cog == perm_cog:
|
guildowner_or_permissions,
|
||||||
return None
|
admin,
|
||||||
# don't break if someone loaded a cog named
|
admin_or_permissions,
|
||||||
# permissions that doesn't implement this
|
mod,
|
||||||
func = getattr(perm_cog, "check_overrides", None)
|
mod_or_permissions,
|
||||||
val = None if func is None else await func(ctx, level)
|
check as _check_decorator,
|
||||||
return val
|
)
|
||||||
|
from .utils.mod import (
|
||||||
|
is_mod_or_superior as _is_mod_or_superior,
|
||||||
def is_owner(**kwargs):
|
is_admin_or_superior as _is_admin_or_superior,
|
||||||
async def check(ctx):
|
check_permissions as _check_permissions,
|
||||||
return await ctx.bot.is_owner(ctx.author, **kwargs)
|
|
||||||
|
|
||||||
return commands.check(check)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_permissions(ctx, perms):
|
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
|
||||||
return True
|
|
||||||
elif not perms:
|
|
||||||
return False
|
|
||||||
resolved = ctx.channel.permissions_for(ctx.author)
|
|
||||||
|
|
||||||
return resolved.administrator or all(
|
|
||||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .bot import Red
|
||||||
|
from .commands import Context
|
||||||
|
|
||||||
async def is_mod_or_superior(ctx):
|
__all__ = [
|
||||||
if ctx.guild is None:
|
"bot_has_permissions",
|
||||||
return await ctx.bot.is_owner(ctx.author)
|
"has_permissions",
|
||||||
else:
|
"is_owner",
|
||||||
author = ctx.author
|
"guildowner",
|
||||||
settings = ctx.bot.db.guild(ctx.guild)
|
"guildowner_or_permissions",
|
||||||
mod_role_id = await settings.mod_role()
|
"admin",
|
||||||
admin_role_id = await settings.admin_role()
|
"admin_or_permissions",
|
||||||
|
"mod",
|
||||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
"mod_or_permissions",
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
"is_mod_or_superior",
|
||||||
|
"is_admin_or_superior",
|
||||||
return (
|
"bot_in_a_guild",
|
||||||
await ctx.bot.is_owner(ctx.author)
|
"check_permissions",
|
||||||
or mod_role in author.roles
|
]
|
||||||
or admin_role in author.roles
|
|
||||||
or author == ctx.guild.owner
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def is_admin_or_superior(ctx):
|
def bot_in_a_guild():
|
||||||
if ctx.guild is None:
|
"""Deny the command if the bot is not in a guild."""
|
||||||
return await ctx.bot.is_owner(ctx.author)
|
|
||||||
else:
|
|
||||||
author = ctx.author
|
|
||||||
settings = ctx.bot.db.guild(ctx.guild)
|
|
||||||
admin_role_id = await settings.admin_role()
|
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
await ctx.bot.is_owner(ctx.author)
|
|
||||||
or admin_role in author.roles
|
|
||||||
or author == ctx.guild.owner
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mod_or_permissions(**perms):
|
|
||||||
async def predicate(ctx):
|
|
||||||
override = await check_overrides(ctx, level="mod")
|
|
||||||
return (
|
|
||||||
override
|
|
||||||
if override is not None
|
|
||||||
else await check_permissions(ctx, perms) or await is_mod_or_superior(ctx)
|
|
||||||
)
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def admin_or_permissions(**perms):
|
|
||||||
async def predicate(ctx):
|
|
||||||
override = await check_overrides(ctx, level="admin")
|
|
||||||
return (
|
|
||||||
override
|
|
||||||
if override is not None
|
|
||||||
else await check_permissions(ctx, perms) or await is_admin_or_superior(ctx)
|
|
||||||
)
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def bot_in_a_guild(**kwargs):
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
return len(ctx.bot.guilds) > 0
|
return len(ctx.bot.guilds) > 0
|
||||||
|
|
||||||
return commands.check(predicate)
|
return _check_decorator(predicate)
|
||||||
|
|
||||||
|
|
||||||
def guildowner_or_permissions(**perms):
|
def is_mod_or_superior(bot: "Red", member: discord.Member) -> Awaitable[bool]:
|
||||||
async def predicate(ctx):
|
warnings.warn(
|
||||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
"`redbot.core.checks.is_mod_or_superior` is deprecated and will be removed in a future "
|
||||||
if ctx.guild is None:
|
"release, please use `redbot.core.utils.mod.is_mod_or_superior` instead.",
|
||||||
return has_perms_or_is_owner
|
category=DeprecationWarning,
|
||||||
is_guild_owner = ctx.author == ctx.guild.owner
|
)
|
||||||
|
return _is_mod_or_superior(bot, member)
|
||||||
override = await check_overrides(ctx, level="guildowner")
|
|
||||||
return override if override is not None else is_guild_owner or has_perms_or_is_owner
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def guildowner():
|
def is_admin_or_superior(bot: "Red", member: discord.Member) -> Awaitable[bool]:
|
||||||
return guildowner_or_permissions()
|
warnings.warn(
|
||||||
|
"`redbot.core.checks.is_admin_or_superior` is deprecated and will be removed in a future "
|
||||||
|
"release, please use `redbot.core.utils.mod.is_admin_or_superior` instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
)
|
||||||
|
return _is_admin_or_superior(bot, member)
|
||||||
|
|
||||||
|
|
||||||
def admin():
|
def check_permissions(ctx: "Context", perms: Dict[str, bool]) -> Awaitable[bool]:
|
||||||
return admin_or_permissions()
|
warnings.warn(
|
||||||
|
"`redbot.core.checks.check_permissions` is deprecated and will be removed in a future "
|
||||||
|
"release, please use `redbot.core.utils.mod.check_permissions`."
|
||||||
def mod():
|
)
|
||||||
return mod_or_permissions()
|
return _check_permissions(ctx, perms)
|
||||||
|
|||||||
@ -311,7 +311,7 @@ _ = Translator("CogManagerUI", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class CogManagerUI:
|
class CogManagerUI(commands.Cog):
|
||||||
"""Commands to interface with Red's cog manager."""
|
"""Commands to interface with Red's cog manager."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -2,4 +2,6 @@
|
|||||||
from discord.ext.commands import *
|
from discord.ext.commands import *
|
||||||
from .commands import *
|
from .commands import *
|
||||||
from .context import *
|
from .context import *
|
||||||
|
from .converter import *
|
||||||
from .errors import *
|
from .errors import *
|
||||||
|
from .requires import *
|
||||||
|
|||||||
@ -5,33 +5,118 @@ replace those from the `discord.ext.commands` module.
|
|||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Awaitable, Callable, TYPE_CHECKING
|
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import converter as converters
|
||||||
from .errors import ConversionFailure
|
from .errors import ConversionFailure
|
||||||
|
from .requires import PermState, PrivilegeLevel, Requires
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
|
||||||
__all__ = ["Command", "GroupMixin", "Group", "command", "group"]
|
__all__ = [
|
||||||
|
"Cog",
|
||||||
|
"CogCommandMixin",
|
||||||
|
"CogGroupMixin",
|
||||||
|
"Command",
|
||||||
|
"Group",
|
||||||
|
"GroupMixin",
|
||||||
|
"command",
|
||||||
|
"group",
|
||||||
|
]
|
||||||
|
|
||||||
_ = Translator("commands.commands", __file__)
|
_ = Translator("commands.commands", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Command(commands.Command):
|
class CogCommandMixin:
|
||||||
|
"""A mixin for cogs and commands."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if isinstance(self, Command):
|
||||||
|
decorated = self.callback
|
||||||
|
else:
|
||||||
|
decorated = self
|
||||||
|
self.requires: Requires = Requires(
|
||||||
|
privilege_level=getattr(
|
||||||
|
decorated, "__requires_privilege_level__", PrivilegeLevel.NONE
|
||||||
|
),
|
||||||
|
user_perms=getattr(decorated, "__requires_user_perms__", {}),
|
||||||
|
bot_perms=getattr(decorated, "__requires_bot_perms__", {}),
|
||||||
|
checks=getattr(decorated, "__requires_checks__", []),
|
||||||
|
)
|
||||||
|
|
||||||
|
def allow_for(self, model_id: int, guild_id: int) -> None:
|
||||||
|
"""Actively allow this command for the given model."""
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_ALLOW, guild_id=guild_id)
|
||||||
|
|
||||||
|
def deny_to(self, model_id: int, guild_id: int) -> None:
|
||||||
|
"""Actively deny this command to the given model."""
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.PASSIVE_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
|
||||||
|
else:
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
||||||
|
|
||||||
|
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]:
|
||||||
|
"""Clear the rule which is currently set for this model."""
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.ACTIVE_ALLOW:
|
||||||
|
new_rule = PermState.NORMAL
|
||||||
|
elif cur_rule is PermState.ACTIVE_DENY:
|
||||||
|
new_rule = PermState.NORMAL
|
||||||
|
elif cur_rule is PermState.CAUTIOUS_ALLOW:
|
||||||
|
new_rule = PermState.PASSIVE_ALLOW
|
||||||
|
else:
|
||||||
|
return cur_rule, cur_rule
|
||||||
|
self.requires.set_rule(model_id, new_rule, guild_id=guild_id)
|
||||||
|
return cur_rule, new_rule
|
||||||
|
|
||||||
|
def set_default_rule(self, rule: Optional[bool], guild_id: int) -> None:
|
||||||
|
"""Set the default rule for this cog or command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
rule : Optional[bool]
|
||||||
|
The rule to set as default. If ``True`` for allow,
|
||||||
|
``False`` for deny and ``None`` for normal.
|
||||||
|
guild_id : Optional[int]
|
||||||
|
Specify to set the default rule for a specific guild.
|
||||||
|
When ``None``, this will set the global default rule.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if guild_id:
|
||||||
|
self.requires.set_default_guild_rule(guild_id, PermState.from_bool(rule))
|
||||||
|
else:
|
||||||
|
self.requires.default_global_rule = PermState.from_bool(rule)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(CogCommandMixin, commands.Command):
|
||||||
"""Command class for Red.
|
"""Command class for Red.
|
||||||
|
|
||||||
This should not be created directly, and instead via the decorator.
|
This should not be created directly, and instead via the decorator.
|
||||||
|
|
||||||
This class inherits from `discord.ext.commands.Command`.
|
This class inherits from `discord.ext.commands.Command`. The
|
||||||
|
attributes listed below are simply additions to the ones listed
|
||||||
|
with that class.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
checks : List[`coroutine function`]
|
||||||
|
A list of check predicates which cannot be overridden, unlike
|
||||||
|
`Requires.checks`.
|
||||||
|
translator : Translator
|
||||||
|
A translator for this command's help docstring.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._help_override = kwargs.pop("help_override", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self._help_override = kwargs.pop("help_override", None)
|
||||||
self.translator = kwargs.pop("i18n", None)
|
self.translator = kwargs.pop("i18n", None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -59,11 +144,10 @@ class Command(commands.Command):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parents(self):
|
def parents(self) -> List["Group"]:
|
||||||
"""
|
"""List[Group] : Returns all parent commands of this command.
|
||||||
Returns all parent commands of this command.
|
|
||||||
|
|
||||||
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
||||||
If the command has no parents, this will be an empty list.
|
If the command has no parents, this will be an empty list.
|
||||||
"""
|
"""
|
||||||
cmd = self.parent
|
cmd = self.parent
|
||||||
@ -73,6 +157,33 @@ class Command(commands.Command):
|
|||||||
cmd = cmd.parent
|
cmd = cmd.parent
|
||||||
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
||||||
|
|
||||||
|
async def can_run(self, ctx: "Context") -> bool:
|
||||||
|
"""Check if this command can be run in the given context.
|
||||||
|
|
||||||
|
This function first checks if the command can be run using
|
||||||
|
discord.py's method `discord.ext.commands.Command.can_run`,
|
||||||
|
then will return the result of `Requires.verify`.
|
||||||
|
"""
|
||||||
|
ret = await super().can_run(ctx)
|
||||||
|
if ret is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# This is so contexts invoking other commands can be checked with
|
||||||
|
# this command as well
|
||||||
|
original_command = ctx.command
|
||||||
|
ctx.command = self
|
||||||
|
|
||||||
|
if self.parent is None and self.instance is not None:
|
||||||
|
# For top-level commands, we need to check the cog's requires too
|
||||||
|
ret = await self.instance.requires.verify(ctx)
|
||||||
|
if ret is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self.requires.verify(ctx)
|
||||||
|
finally:
|
||||||
|
ctx.command = original_command
|
||||||
|
|
||||||
async def do_conversion(
|
async def do_conversion(
|
||||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||||
):
|
):
|
||||||
@ -179,8 +290,32 @@ class Command(commands.Command):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def allow_for(self, model_id: int, guild_id: int) -> None:
|
||||||
|
super().allow_for(model_id, guild_id=guild_id)
|
||||||
|
parents = self.parents
|
||||||
|
if self.instance is not None:
|
||||||
|
parents.append(self.instance)
|
||||||
|
for parent in parents:
|
||||||
|
cur_rule = parent.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.NORMAL:
|
||||||
|
parent.requires.set_rule(model_id, PermState.PASSIVE_ALLOW, guild_id=guild_id)
|
||||||
|
elif cur_rule is PermState.ACTIVE_DENY:
|
||||||
|
parent.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
|
||||||
|
|
||||||
class GroupMixin(commands.GroupMixin):
|
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]:
|
||||||
|
old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id)
|
||||||
|
if old_rule is PermState.ACTIVE_ALLOW:
|
||||||
|
parents = self.parents
|
||||||
|
if self.instance is not None:
|
||||||
|
parents.append(self.instance)
|
||||||
|
for parent in parents:
|
||||||
|
should_continue = parent.reevaluate_rules_for(model_id, guild_id=guild_id)[1]
|
||||||
|
if not should_continue:
|
||||||
|
break
|
||||||
|
return old_rule, new_rule
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMixin(discord.ext.commands.GroupMixin):
|
||||||
"""Mixin for `Group` and `Red` classes.
|
"""Mixin for `Group` and `Red` classes.
|
||||||
|
|
||||||
This class inherits from :class:`discord.ext.commands.GroupMixin`.
|
This class inherits from :class:`discord.ext.commands.GroupMixin`.
|
||||||
@ -211,7 +346,34 @@ class GroupMixin(commands.GroupMixin):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Group(GroupMixin, Command, commands.Group):
|
class CogGroupMixin:
|
||||||
|
requires: Requires
|
||||||
|
all_commands: Dict[str, Command]
|
||||||
|
|
||||||
|
def reevaluate_rules_for(
|
||||||
|
self, model_id: int, guild_id: Optional[int]
|
||||||
|
) -> Tuple[PermState, bool]:
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
|
||||||
|
# These three states are unaffected by subcommand rules
|
||||||
|
return cur_rule, False
|
||||||
|
else:
|
||||||
|
# Remaining states can be changed if there exists no actively-allowed
|
||||||
|
# subcommand (this includes subcommands multiple levels below)
|
||||||
|
if any(
|
||||||
|
cmd.requires.get_rule(model_id, guild_id=guild_id) in PermState.ALLOWED_STATES
|
||||||
|
for cmd in self.all_commands.values()
|
||||||
|
):
|
||||||
|
return cur_rule, False
|
||||||
|
elif cur_rule is PermState.PASSIVE_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.NORMAL, guild_id=guild_id)
|
||||||
|
return PermState.NORMAL, True
|
||||||
|
elif cur_rule is PermState.CAUTIOUS_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
||||||
|
return PermState.ACTIVE_DENY, True
|
||||||
|
|
||||||
|
|
||||||
|
class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
|
||||||
"""Group command class for Red.
|
"""Group command class for Red.
|
||||||
|
|
||||||
This class inherits from `Command`, with :class:`GroupMixin` and
|
This class inherits from `Command`, with :class:`GroupMixin` and
|
||||||
@ -222,7 +384,7 @@ class Group(GroupMixin, Command, commands.Group):
|
|||||||
self.autohelp = kwargs.pop("autohelp", True)
|
self.autohelp = kwargs.pop("autohelp", True)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def invoke(self, ctx):
|
async def invoke(self, ctx: "Context"):
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
previous = view.index
|
previous = view.index
|
||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
@ -247,7 +409,12 @@ class Group(GroupMixin, Command, commands.Group):
|
|||||||
await super().invoke(ctx)
|
await super().invoke(ctx)
|
||||||
|
|
||||||
|
|
||||||
# decorators
|
class Cog(CogCommandMixin, CogGroupMixin):
|
||||||
|
"""Base class for a cog."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_commands(self) -> Dict[str, Command]:
|
||||||
|
return {cmd.name: cmd for cmd in self.__dict__.values() if isinstance(cmd, Command)}
|
||||||
|
|
||||||
|
|
||||||
def command(name=None, cls=Command, **attrs):
|
def command(name=None, cls=Command, **attrs):
|
||||||
|
|||||||
@ -4,8 +4,9 @@ from typing import Iterable, List
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from redbot.core.utils.chat_formatting import box
|
from .requires import PermState
|
||||||
from redbot.core.utils import common_filters
|
from ..utils.chat_formatting import box
|
||||||
|
from ..utils import common_filters
|
||||||
|
|
||||||
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
|
||||||
@ -20,6 +21,10 @@ class Context(commands.Context):
|
|||||||
This class inherits from `discord.ext.commands.Context`.
|
This class inherits from `discord.ext.commands.Context`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **attrs):
|
||||||
|
super().__init__(**attrs)
|
||||||
|
self.permission_state: PermState = PermState.NORMAL
|
||||||
|
|
||||||
async def send(self, content=None, **kwargs):
|
async def send(self, content=None, **kwargs):
|
||||||
"""Sends a message to the destination with the content given.
|
"""Sends a message to the destination with the content given.
|
||||||
|
|
||||||
|
|||||||
41
redbot/core/commands/converter.py
Normal file
41
redbot/core/commands/converter.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from . import BadArgument
|
||||||
|
from ..i18n import Translator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
__all__ = ["GuildConverter"]
|
||||||
|
|
||||||
|
_ = Translator("commands.converter", __file__)
|
||||||
|
|
||||||
|
ID_REGEX = re.compile(r"([0-9]{15,21})")
|
||||||
|
|
||||||
|
|
||||||
|
class GuildConverter(discord.Guild):
|
||||||
|
"""Converts to a `discord.Guild` object.
|
||||||
|
|
||||||
|
The lookup strategy is as follows (in order):
|
||||||
|
|
||||||
|
1. Lookup by ID.
|
||||||
|
2. Lookup by name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: "Context", argument: str) -> discord.Guild:
|
||||||
|
match = ID_REGEX.fullmatch(argument)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
ret = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||||
|
else:
|
||||||
|
guild_id = int(match.group(1))
|
||||||
|
ret = ctx.bot.get_guild(guild_id)
|
||||||
|
|
||||||
|
if ret is None:
|
||||||
|
raise BadArgument(_('Server "{name}" not found.').format(name=argument))
|
||||||
|
|
||||||
|
return ret
|
||||||
@ -1,8 +1,9 @@
|
|||||||
"""Errors module for the commands package."""
|
"""Errors module for the commands package."""
|
||||||
import inspect
|
import inspect
|
||||||
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
__all__ = ["ConversionFailure"]
|
__all__ = ["ConversionFailure", "BotMissingPermissions"]
|
||||||
|
|
||||||
|
|
||||||
class ConversionFailure(commands.BadArgument):
|
class ConversionFailure(commands.BadArgument):
|
||||||
@ -13,3 +14,11 @@ class ConversionFailure(commands.BadArgument):
|
|||||||
self.argument = argument
|
self.argument = argument
|
||||||
self.param = param
|
self.param = param
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
|
|
||||||
|
|
||||||
|
class BotMissingPermissions(commands.CheckFailure):
|
||||||
|
"""Raised if the bot is missing permissions required to run a command."""
|
||||||
|
|
||||||
|
def __init__(self, missing: discord.Permissions, *args):
|
||||||
|
self.missing: discord.Permissions = missing
|
||||||
|
super().__init__(*args)
|
||||||
|
|||||||
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)
|
||||||
@ -241,7 +241,7 @@ class CoreLogic:
|
|||||||
|
|
||||||
|
|
||||||
@i18n.cog_i18n(_)
|
@i18n.cog_i18n(_)
|
||||||
class Core(CoreLogic):
|
class Core(commands.Cog, CoreLogic):
|
||||||
"""Commands related to core functions"""
|
"""Commands related to core functions"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
|||||||
@ -25,10 +25,11 @@ _ = Translator("Dev", __file__)
|
|||||||
START_CODE_BLOCK_RE = re.compile(r"^((```py)(?=\s)|(```))")
|
START_CODE_BLOCK_RE = re.compile(r"^((```py)(?=\s)|(```))")
|
||||||
|
|
||||||
|
|
||||||
class Dev:
|
class Dev(commands.Cog):
|
||||||
"""Various development focused utilities."""
|
"""Various development focused utilities."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._last_result = None
|
self._last_result = None
|
||||||
self.sessions = set()
|
self.sessions = set()
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
@ -14,7 +15,7 @@ from pkg_resources import DistributionNotFound
|
|||||||
|
|
||||||
from . import __version__, commands
|
from . import __version__, commands
|
||||||
from .data_manager import storage_type
|
from .data_manager import storage_type
|
||||||
from .utils.chat_formatting import inline, bordered
|
from .utils.chat_formatting import inline, bordered, humanize_list
|
||||||
from .utils import fuzzy_command_search, format_fuzzy_results
|
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
@ -67,6 +68,14 @@ def init_events(bot, cli_flags):
|
|||||||
packages.extend(cli_flags.load_cogs)
|
packages.extend(cli_flags.load_cogs)
|
||||||
|
|
||||||
if packages:
|
if packages:
|
||||||
|
# Load permissions first, for security reasons
|
||||||
|
try:
|
||||||
|
packages.remove("permissions")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
packages.insert(0, "permissions")
|
||||||
|
|
||||||
to_remove = []
|
to_remove = []
|
||||||
print("Loading packages...")
|
print("Loading packages...")
|
||||||
for package in packages:
|
for package in packages:
|
||||||
@ -227,6 +236,21 @@ def init_events(bot, cli_flags):
|
|||||||
await ctx.send(embed=await format_fuzzy_results(ctx, fuzzy_commands, embed=True))
|
await ctx.send(embed=await format_fuzzy_results(ctx, fuzzy_commands, embed=True))
|
||||||
else:
|
else:
|
||||||
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
|
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
|
||||||
|
elif isinstance(error, commands.BotMissingPermissions):
|
||||||
|
missing_perms: List[str] = []
|
||||||
|
for perm, value in error.missing:
|
||||||
|
if value is True:
|
||||||
|
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||||
|
missing_perms.append(perm_name)
|
||||||
|
if len(missing_perms) == 1:
|
||||||
|
plural = ""
|
||||||
|
else:
|
||||||
|
plural = "s"
|
||||||
|
await ctx.send(
|
||||||
|
"I require the {perms} permission{plural} to execute that command.".format(
|
||||||
|
perms=humanize_list(missing_perms), plural=plural
|
||||||
|
)
|
||||||
|
)
|
||||||
elif isinstance(error, commands.CheckFailure):
|
elif isinstance(error, commands.CheckFailure):
|
||||||
pass
|
pass
|
||||||
elif isinstance(error, commands.NoPrivateMessage):
|
elif isinstance(error, commands.NoPrivateMessage):
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from . import commands
|
|||||||
|
|
||||||
|
|
||||||
def init_global_checks(bot):
|
def init_global_checks(bot):
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def global_perms(ctx):
|
async def global_perms(ctx):
|
||||||
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
||||||
if await bot.is_owner(ctx.author):
|
if await bot.is_owner(ctx.author):
|
||||||
@ -15,7 +15,7 @@ def init_global_checks(bot):
|
|||||||
|
|
||||||
return ctx.author.id not in await bot.db.blacklist()
|
return ctx.author.id not in await bot.db.blacklist()
|
||||||
|
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def local_perms(ctx: commands.Context):
|
async def local_perms(ctx: commands.Context):
|
||||||
"""Check the user is/isn't locally whitelisted/blacklisted."""
|
"""Check the user is/isn't locally whitelisted/blacklisted."""
|
||||||
if await bot.is_owner(ctx.author):
|
if await bot.is_owner(ctx.author):
|
||||||
@ -33,7 +33,7 @@ def init_global_checks(bot):
|
|||||||
|
|
||||||
return not any(i in local_blacklist for i in _ids)
|
return not any(i in local_blacklist for i in _ids)
|
||||||
|
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def bots(ctx):
|
async def bots(ctx):
|
||||||
"""Check the user is not another bot."""
|
"""Check the user is not another bot."""
|
||||||
return not ctx.author.bot
|
return not ctx.author.bot
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import itertools
|
import itertools
|
||||||
from typing import Sequence, Iterator
|
from typing import Sequence, Iterator, List
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("UtilsChatFormatting", __file__)
|
||||||
|
|
||||||
|
|
||||||
def error(text: str) -> str:
|
def error(text: str) -> str:
|
||||||
@ -317,3 +320,33 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
|
|||||||
if formatting:
|
if formatting:
|
||||||
text = text.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
|
text = text.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_list(items: Sequence[str]):
|
||||||
|
"""Get comma-separted list, with the last element joined with *and*.
|
||||||
|
|
||||||
|
This uses an Oxford comma, because without one, items containing
|
||||||
|
the word *and* would make the output difficult to interpret.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
items : Sequence[str]
|
||||||
|
The items of the list to join together.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
.. testsetup::
|
||||||
|
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_list
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> humanize_list(['One', 'Two', 'Three'])
|
||||||
|
'One, Two, and Three'
|
||||||
|
>>> humanize_list(['One'])
|
||||||
|
'One'
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(items) == 1:
|
||||||
|
return items[0]
|
||||||
|
return ", ".join(items[:-1]) + _(", and ") + items[-1]
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List, Iterable, Union
|
from typing import List, Iterable, Union, TYPE_CHECKING, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import Config
|
if TYPE_CHECKING:
|
||||||
from redbot.core.bot import Red
|
from .. import Config
|
||||||
|
from ..bot import Red
|
||||||
|
from ..commands import Context
|
||||||
|
|
||||||
|
|
||||||
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
|
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
|
||||||
@ -87,7 +89,7 @@ def get_audit_reason(author: discord.Member, reason: str = None):
|
|||||||
|
|
||||||
|
|
||||||
async def is_allowed_by_hierarchy(
|
async def is_allowed_by_hierarchy(
|
||||||
bot: Red, settings: Config, guild: discord.Guild, mod: discord.Member, user: discord.Member
|
bot: "Red", settings: "Config", guild: discord.Guild, mod: discord.Member, user: discord.Member
|
||||||
):
|
):
|
||||||
if not await settings.guild(guild).respect_hierarchy():
|
if not await settings.guild(guild).respect_hierarchy():
|
||||||
return True
|
return True
|
||||||
@ -95,7 +97,9 @@ async def is_allowed_by_hierarchy(
|
|||||||
return mod.top_role.position > user.top_role.position or is_special
|
return mod.top_role.position > user.top_role.position or is_special
|
||||||
|
|
||||||
|
|
||||||
async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
|
async def is_mod_or_superior(
|
||||||
|
bot: "Red", obj: Union[discord.Message, discord.Member, discord.Role]
|
||||||
|
):
|
||||||
"""Check if an object has mod or superior permissions.
|
"""Check if an object has mod or superior permissions.
|
||||||
|
|
||||||
If a message is passed, its author's permissions are checked. If a role is
|
If a message is passed, its author's permissions are checked. If a role is
|
||||||
@ -179,7 +183,7 @@ def strfdelta(delta: timedelta):
|
|||||||
|
|
||||||
|
|
||||||
async def is_admin_or_superior(
|
async def is_admin_or_superior(
|
||||||
bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]
|
bot: "Red", obj: Union[discord.Message, discord.Member, discord.Role]
|
||||||
):
|
):
|
||||||
"""Same as `is_mod_or_superior` except for admin permissions.
|
"""Same as `is_mod_or_superior` except for admin permissions.
|
||||||
|
|
||||||
@ -225,3 +229,36 @@ async def is_admin_or_superior(
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def check_permissions(ctx: "Context", perms: Dict[str, bool]) -> bool:
|
||||||
|
"""Check if the author has required permissions.
|
||||||
|
|
||||||
|
This will always return ``True`` if the author is a bot owner, or
|
||||||
|
has the ``administrator`` permission. If ``perms`` is empty, this
|
||||||
|
will only check if the user is a bot owner.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : Context
|
||||||
|
The command invokation context to check.
|
||||||
|
perms : Dict[str, bool]
|
||||||
|
A dictionary mapping permissions to their required states.
|
||||||
|
Valid permission names are those listed as properties of
|
||||||
|
the `discord.Permissions` class.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if the author has the required permissions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
return True
|
||||||
|
elif not perms:
|
||||||
|
return False
|
||||||
|
resolved = ctx.channel.permissions_for(ctx.author)
|
||||||
|
|
||||||
|
return resolved.administrator or all(
|
||||||
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
|
)
|
||||||
|
|||||||
11
redbot/pytest/permissions.py
Normal file
11
redbot/pytest/permissions.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.permissions import Permissions
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def permissions(config, monkeypatch, red):
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
return Permissions(red)
|
||||||
1
setup.py
1
setup.py
@ -23,6 +23,7 @@ requirements = [
|
|||||||
"pyyaml==3.13",
|
"pyyaml==3.13",
|
||||||
"raven==6.9.0",
|
"raven==6.9.0",
|
||||||
"raven-aiohttp==0.7.0",
|
"raven-aiohttp==0.7.0",
|
||||||
|
"schema==0.6.8",
|
||||||
"websockets==6.0",
|
"websockets==6.0",
|
||||||
"yarl==1.2.6",
|
"yarl==1.2.6",
|
||||||
]
|
]
|
||||||
|
|||||||
67
tests/cogs/test_permissions.py
Normal file
67
tests/cogs/test_permissions.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
from redbot.cogs.permissions.permissions import Permissions, GLOBAL
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_update():
|
||||||
|
old = {
|
||||||
|
GLOBAL: {
|
||||||
|
"owner_models": {
|
||||||
|
"cogs": {
|
||||||
|
"Admin": {"allow": [78631113035100160], "deny": [96733288462286848]},
|
||||||
|
"Audio": {"allow": [133049272517001216], "default": "deny"},
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"cleanup bot": {"allow": [78631113035100160], "default": "deny"},
|
||||||
|
"ping": {
|
||||||
|
"allow": [96733288462286848],
|
||||||
|
"deny": [96733288462286848],
|
||||||
|
"default": "allow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
43733288462286848: {
|
||||||
|
"owner_models": {
|
||||||
|
"cogs": {
|
||||||
|
"Admin": {
|
||||||
|
"allow": [24231113035100160],
|
||||||
|
"deny": [35533288462286848, 24231113035100160],
|
||||||
|
},
|
||||||
|
"General": {"allow": [133049272517001216], "default": "deny"},
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"cleanup bot": {"allow": [17831113035100160], "default": "allow"},
|
||||||
|
"set adminrole": {
|
||||||
|
"allow": [87733288462286848],
|
||||||
|
"deny": [95433288462286848],
|
||||||
|
"default": "allow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
new = Permissions._get_updated_schema(old)
|
||||||
|
assert new == (
|
||||||
|
{
|
||||||
|
"Admin": {
|
||||||
|
GLOBAL: {78631113035100160: True, 96733288462286848: False},
|
||||||
|
43733288462286848: {24231113035100160: True, 35533288462286848: False},
|
||||||
|
},
|
||||||
|
"Audio": {GLOBAL: {133049272517001216: True, "default": False}},
|
||||||
|
"General": {43733288462286848: {133049272517001216: True, "default": False}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cleanup bot": {
|
||||||
|
GLOBAL: {78631113035100160: True, "default": False},
|
||||||
|
43733288462286848: {17831113035100160: True, "default": True},
|
||||||
|
},
|
||||||
|
"ping": {GLOBAL: {96733288462286848: True, "default": True}},
|
||||||
|
"set adminrole": {
|
||||||
|
43733288462286848: {
|
||||||
|
87733288462286848: True,
|
||||||
|
95433288462286848: False,
|
||||||
|
"default": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@ -101,7 +101,7 @@ async def test_bounded_gather():
|
|||||||
if isinstance(result, RuntimeError):
|
if isinstance(result, RuntimeError):
|
||||||
num_failed += 1
|
num_failed += 1
|
||||||
else:
|
else:
|
||||||
assert result == i # verify original orde
|
assert result == i # verify_permissions original orde
|
||||||
assert 0 <= result < num_tasks
|
assert 0 <= result < num_tasks
|
||||||
|
|
||||||
assert 0 < status[1] <= num_concurrent
|
assert 0 < status[1] <= num_concurrent
|
||||||
|
|||||||
5
tox.ini
5
tox.ini
@ -29,8 +29,9 @@ whitelist_externals =
|
|||||||
basepython = python3.6
|
basepython = python3.6
|
||||||
extras = docs, mongo
|
extras = docs, mongo
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" -W -bhtml
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/html" -W -bhtml
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" -W -blinkcheck
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/linkcheck" -W -blinkcheck
|
||||||
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/doctest" -W -bdoctest
|
||||||
|
|
||||||
[testenv:style]
|
[testenv:style]
|
||||||
description = Stylecheck the code with black to see if anything needs changes.
|
description = Stylecheck the code with black to see if anything needs changes.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user