mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7685c4d5d5 | ||
|
|
e701ec9617 | ||
|
|
6c1ee096a1 | ||
|
|
2df282222f | ||
|
|
43c7bd48c7 | ||
|
|
86579068d9 | ||
|
|
8e6ab9aa35 | ||
|
|
77566a887a | ||
|
|
9d0eca1914 | ||
|
|
79a3164d9d | ||
|
|
eb73e48192 | ||
|
|
cd6af7f185 | ||
|
|
3d6020b9cf | ||
|
|
461f03aac0 | ||
|
|
35149f8837 | ||
|
|
c0d01f32a6 | ||
|
|
83a0459b6a | ||
|
|
50f6dcef2f | ||
|
|
5c514fd663 | ||
|
|
1c2196f78f | ||
|
|
43cc3c40f3 | ||
|
|
7a6a4cf59d | ||
|
|
3bcf375204 | ||
|
|
a175bdc1c7 | ||
|
|
b557b437a3 | ||
|
|
d1f0b59b5d | ||
|
|
3ece3a1f2b | ||
|
|
1f1a85de18 | ||
|
|
e08c9dafa6 | ||
|
|
ad27607ccc | ||
|
|
c1bcca4432 | ||
|
|
9f2ed694ce | ||
|
|
edadd8f2fd | ||
|
|
afa08713e0 | ||
|
|
d23620727e | ||
|
|
b456c6ad3b | ||
|
|
0298b53803 | ||
|
|
bfd6e4af3f | ||
|
|
31612aae4a |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -24,6 +24,7 @@ redbot/core/utils/mod.py @palmtree5
|
|||||||
redbot/core/utils/data_converter.py @mikeshardmind
|
redbot/core/utils/data_converter.py @mikeshardmind
|
||||||
redbot/core/utils/antispam.py @mikeshardmind
|
redbot/core/utils/antispam.py @mikeshardmind
|
||||||
redbot/core/utils/tunnel.py @mikeshardmind
|
redbot/core/utils/tunnel.py @mikeshardmind
|
||||||
|
redbot/core/utils/caching.py @mikeshardmind
|
||||||
|
|
||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
@@ -44,6 +45,7 @@ redbot/cogs/trivia/* @Tobotimus
|
|||||||
redbot/cogs/dataconverter/* @mikeshardmind
|
redbot/cogs/dataconverter/* @mikeshardmind
|
||||||
redbot/cogs/reports/* @mikeshardmind
|
redbot/cogs/reports/* @mikeshardmind
|
||||||
redbot/cogs/permissions/* @mikeshardmind
|
redbot/cogs/permissions/* @mikeshardmind
|
||||||
|
redbot/cogs/warnings/* @palmtree5
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|||||||
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -31,7 +31,7 @@ We love receiving contributions from our community. Any assistance you can provi
|
|||||||
# 2. Ground Rules
|
# 2. Ground Rules
|
||||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||||
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
||||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||||
@@ -79,7 +79,7 @@ Note: If you haven't used `pipenv` before but are comfortable with virtualenvs,
|
|||||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||||
|
|
||||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on both python 3.5 and 3.6 (test environments `py35` and `py36` respectively)
|
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 (test environment `py36`)
|
||||||
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||||
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||||
|
|
||||||
@@ -94,8 +94,6 @@ Our style checker of choice, [black](https://github.com/ambv/black), actually ha
|
|||||||
|
|
||||||
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
||||||
|
|
||||||
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
|
|
||||||
|
|
||||||
### 4.4 Make
|
### 4.4 Make
|
||||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||||
1. `make reformat`: Reformat all python files in the project with Black
|
1. `make reformat`: Reformat all python files in the project with Black
|
||||||
|
|||||||
16
README.rst
16
README.rst
@@ -11,10 +11,22 @@
|
|||||||
|
|
||||||
.. class:: center
|
.. class:: center
|
||||||
|
|
||||||
|
.. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
|
||||||
|
:target: https://discord.gg/red
|
||||||
|
:alt: Discord server
|
||||||
|
|
||||||
|
.. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
|
||||||
|
:target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
|
||||||
|
:alt: Travis CI status
|
||||||
|
|
||||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
||||||
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
||||||
:alt: Documentation Status
|
:alt: Documentation Status
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/discord-py-blue.svg
|
||||||
|
:target: https://github.com/Rapptz/discord.py
|
||||||
|
:alt: discord.py
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
:target: https://github.com/ambv/black
|
:target: https://github.com/ambv/black
|
||||||
:alt: Code style: black
|
:alt: Code style: black
|
||||||
@@ -27,6 +39,10 @@
|
|||||||
:target: https://www.patreon.com/Red_Devs
|
:target: https://www.patreon.com/Red_Devs
|
||||||
:alt: Patreon
|
:alt: Patreon
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||||
|
:target: http://makeapullrequest.com
|
||||||
|
:alt: PRs open
|
||||||
|
|
||||||
==========
|
==========
|
||||||
Overview
|
Overview
|
||||||
==========
|
==========
|
||||||
|
|||||||
@@ -89,8 +89,10 @@ Locking Audio to specific voice channel(s) as a serverowner or admin:
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setguilddefault Audio deny
|
[p]permissions setguilddefault deny play
|
||||||
[p]permissions addguildrule allow Audio [voice channel ID or name]
|
[p]permissions setguilddefault deny "playlist start"
|
||||||
|
[p]permissions addguildrule allow play [voice channel ID or name]
|
||||||
|
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
Allowing extra roles to use cleanup
|
Allowing extra roles to use cleanup
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Keys common to both repo and cog info.json (case sensitive)
|
|||||||
- ``install_msg`` (string) - The message that gets displayed when a cog
|
- ``install_msg`` (string) - The message that gets displayed when a cog
|
||||||
is installed or a repo is added
|
is installed or a repo is added
|
||||||
|
|
||||||
|
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||||
|
used for installing.
|
||||||
|
|
||||||
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||||
is displayed when a user executes ``!cog list``
|
is displayed when a user executes ``!cog list``
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ you in the process.
|
|||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
|
||||||
are on Windows, stick with 3.5). Open a terminal or command prompt and type
|
|
||||||
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||||
This will install the latest version of V3.
|
This will install the latest version of V3.
|
||||||
|
|||||||
@@ -12,12 +12,3 @@ if discord.version_info.major < 1:
|
|||||||
" >= 1.0.0."
|
" >= 1.0.0."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if sys.version_info < (3, 6, 0):
|
|
||||||
print(Back.RED + "[DEPRECATION WARNING]")
|
|
||||||
print(
|
|
||||||
Back.RED + "You are currently running Python 3.5."
|
|
||||||
" Support for Python 3.5 will end with the release of beta 16."
|
|
||||||
" Please update your environment to Python 3.6 as soon as possible to avoid"
|
|
||||||
" any interruptions after the beta 16 release."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sys
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core.bot import Red, ExitCodes
|
from redbot.core.bot import Red, ExitCodes
|
||||||
from redbot.core.cog_manager import CogManagerUI
|
from redbot.core.cog_manager import CogManagerUI
|
||||||
from redbot.core.data_manager import load_basic_configuration, config_file
|
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
||||||
from redbot.core.json_io import JsonIO
|
from redbot.core.json_io import JsonIO
|
||||||
from redbot.core.global_checks import init_global_checks
|
from redbot.core.global_checks import init_global_checks
|
||||||
from redbot.core.events import init_events
|
from redbot.core.events import init_events
|
||||||
@@ -106,9 +106,17 @@ def main():
|
|||||||
elif cli_flags.version:
|
elif cli_flags.version:
|
||||||
print(description)
|
print(description)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif not cli_flags.instance_name:
|
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||||
print("Error: No instance name was provided!")
|
print("Error: No instance name was provided!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if cli_flags.no_instance:
|
||||||
|
print(
|
||||||
|
"\033[1m"
|
||||||
|
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
||||||
|
"\033[0m"
|
||||||
|
)
|
||||||
|
cli_flags.instance_name = "temporary_red"
|
||||||
|
create_temp_config()
|
||||||
load_basic_configuration(cli_flags.instance_name)
|
load_basic_configuration(cli_flags.instance_name)
|
||||||
log, sentry_log = init_loggers(cli_flags)
|
log, sentry_log = init_loggers(cli_flags)
|
||||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||||
@@ -122,6 +130,8 @@ def main():
|
|||||||
tmp_data = {}
|
tmp_data = {}
|
||||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||||
|
if cli_flags.token:
|
||||||
|
token = cli_flags.token
|
||||||
prefix = cli_flags.prefix or tmp_data["prefix"]
|
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||||
if not (token and prefix):
|
if not (token and prefix):
|
||||||
if cli_flags.no_prompt is False:
|
if cli_flags.no_prompt is False:
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ class Admin:
|
|||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Adds a role to a user. If user is left blank it defaults to the
|
Adds a role to a user.
|
||||||
author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -145,8 +145,8 @@ class Admin:
|
|||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Removes a role from a user. If user is left blank it defaults to the
|
Removes a role from a user.
|
||||||
author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -156,7 +156,7 @@ class Admin:
|
|||||||
else:
|
else:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def editrole(self, ctx: commands.Context):
|
async def editrole(self, ctx: commands.Context):
|
||||||
@@ -291,11 +291,11 @@ class Admin:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return valid_roles
|
return valid_roles
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""
|
||||||
Add a role to yourself that server admins have configured as
|
Add a role to yourself that server admins have configured as user settable.
|
||||||
user settable.
|
|
||||||
|
|
||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
@@ -313,7 +313,7 @@ class Admin:
|
|||||||
await self._removerole(ctx, ctx.author, selfrole)
|
await self._removerole(ctx, ctx.author, selfrole)
|
||||||
|
|
||||||
@selfrole.command(name="add")
|
@selfrole.command(name="add")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""
|
"""
|
||||||
Add a role to the list of available selfroles.
|
Add a role to the list of available selfroles.
|
||||||
@@ -327,7 +327,7 @@ class Admin:
|
|||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send("The selfroles list has been successfully modified.")
|
||||||
|
|
||||||
@selfrole.command(name="delete")
|
@selfrole.command(name="delete")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||||
"""
|
"""
|
||||||
Removes a role from the list of available selfroles.
|
Removes a role from the list of available selfroles.
|
||||||
|
|||||||
@@ -170,13 +170,13 @@ class Alias:
|
|||||||
new_message.content = "{}{} {}".format(prefix, alias.command, args)
|
new_message.content = "{}{} {}".format(prefix, alias.command, args)
|
||||||
await self.bot.process_commands(new_message)
|
await self.bot.process_commands(new_message)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def alias(self, ctx: commands.Context):
|
async def alias(self, ctx: commands.Context):
|
||||||
"""Manage per-server aliases for commands"""
|
"""Manage per-server aliases for commands"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@alias.group(name="global", autohelp=True)
|
@alias.group(name="global")
|
||||||
async def global_(self, ctx: commands.Context):
|
async def global_(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Manage global aliases.
|
Manage global aliases.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from .manager import shutdown_lavalink_server
|
|||||||
|
|
||||||
_ = Translator("Audio", __file__)
|
_ = Translator("Audio", __file__)
|
||||||
|
|
||||||
__version__ = "0.0.6b"
|
__version__ = "0.0.6c"
|
||||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class Audio:
|
|||||||
await message_channel.send(embed=embed)
|
await message_channel.send(embed=embed)
|
||||||
await player.skip()
|
await player.skip()
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def audioset(self, ctx):
|
async def audioset(self, ctx):
|
||||||
"""Music configuration options."""
|
"""Music configuration options."""
|
||||||
@@ -586,6 +586,12 @@ class Audio:
|
|||||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -657,7 +663,7 @@ class Audio:
|
|||||||
await player.play()
|
await player.play()
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def playlist(self, ctx):
|
async def playlist(self, ctx):
|
||||||
"""Playlist configuration options."""
|
"""Playlist configuration options."""
|
||||||
@@ -708,8 +714,10 @@ class Audio:
|
|||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx, "Playlist name already exists, try again with a different name."
|
ctx, "Playlist name already exists, try again with a different name."
|
||||||
)
|
)
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlist_list = self._to_json(ctx, None, None)
|
playlist_list = self._to_json(ctx, None, None)
|
||||||
playlists[playlist_name] = playlist_list
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlists[playlist_name] = playlist_list
|
||||||
await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name))
|
await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name))
|
||||||
|
|
||||||
@playlist.command(name="delete")
|
@playlist.command(name="delete")
|
||||||
@@ -768,6 +776,7 @@ class Audio:
|
|||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.cooldown(1, 15, discord.ext.commands.BucketType.guild)
|
||||||
@playlist.command(name="queue")
|
@playlist.command(name="queue")
|
||||||
async def _playlist_queue(self, ctx, playlist_name=None):
|
async def _playlist_queue(self, ctx, playlist_name=None):
|
||||||
"""Save the queue to a playlist."""
|
"""Save the queue to a playlist."""
|
||||||
@@ -794,11 +803,11 @@ class Audio:
|
|||||||
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
return m.author == ctx.author
|
return m.author == ctx.author and not m.content.startswith(ctx.prefix)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
|
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
|
||||||
playlist_name = str(playlist_name_msg.content)
|
playlist_name = playlist_name_msg.content.split(" ")[0].strip('"')
|
||||||
if len(playlist_name) > 20:
|
if len(playlist_name) > 20:
|
||||||
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
||||||
if playlist_name in playlists:
|
if playlist_name in playlists:
|
||||||
@@ -809,11 +818,12 @@ class Audio:
|
|||||||
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
||||||
playlist_list = self._to_json(ctx, None, tracklist)
|
playlist_list = self._to_json(ctx, None, tracklist)
|
||||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlists[playlist_name] = playlist_list
|
playlists[playlist_name] = playlist_list
|
||||||
await self._embed_msg(
|
await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
"Playlist {} saved from current queue: {} tracks added.".format(
|
"Playlist {} saved from current queue: {} tracks added.".format(
|
||||||
playlist_name, len(tracklist)
|
playlist_name.split(" ")[0].strip('"'), len(tracklist)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -861,6 +871,7 @@ class Audio:
|
|||||||
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
||||||
if tracklist is not None:
|
if tracklist is not None:
|
||||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlists[playlist_name] = playlist_list
|
playlists[playlist_name] = playlist_list
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -919,8 +930,11 @@ class Audio:
|
|||||||
file_suffix = file_url.rsplit(".", 1)[1]
|
file_suffix = file_url.rsplit(".", 1)[1]
|
||||||
if file_suffix != "txt":
|
if file_suffix != "txt":
|
||||||
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
||||||
async with self.session.request("GET", file_url) as r:
|
try:
|
||||||
v2_playlist = await r.json(content_type="text/plain")
|
async with self.session.request("GET", file_url) as r:
|
||||||
|
v2_playlist = await r.json(content_type="text/plain")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return await self._embed_msg(ctx, "Not a valid playlist file.")
|
||||||
try:
|
try:
|
||||||
v2_playlist_url = v2_playlist["link"]
|
v2_playlist_url = v2_playlist["link"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -989,6 +1003,12 @@ class Audio:
|
|||||||
return False
|
return False
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -1206,6 +1226,12 @@ class Audio:
|
|||||||
"""
|
"""
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -1618,7 +1644,7 @@ class Audio:
|
|||||||
embed.set_footer(text="Nothing playing.")
|
embed.set_footer(text="Nothing playing.")
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.group(aliases=["llset"], autohelp=True)
|
@commands.group(aliases=["llset"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def llsetup(self, ctx):
|
async def llsetup(self, ctx):
|
||||||
@@ -1750,7 +1776,7 @@ class Audio:
|
|||||||
if server.id not in stop_times:
|
if server.id not in stop_times:
|
||||||
stop_times[server.id] = None
|
stop_times[server.id] = None
|
||||||
|
|
||||||
if p.current is None and [self.bot.user] == p.channel.members:
|
if [self.bot.user] == p.channel.members:
|
||||||
if stop_times[server.id] is None:
|
if stop_times[server.id] is None:
|
||||||
stop_times[server.id] = int(time.time())
|
stop_times[server.id] = int(time.time())
|
||||||
|
|
||||||
@@ -1883,6 +1909,15 @@ class Audio:
|
|||||||
track_obj[key] = value
|
track_obj[key] = value
|
||||||
return track_obj
|
return track_obj
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _userlimit(channel):
|
||||||
|
if channel.user_limit == 0:
|
||||||
|
return False
|
||||||
|
if channel.user_limit < len(channel.members) + 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
async def on_voice_state_update(self, member, before, after):
|
async def on_voice_state_update(self, member, before, after):
|
||||||
if after.channel != before.channel:
|
if after.channel != before.channel:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
|
|||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
return author == ctx.guild.owner or permissions.administrator
|
return author == ctx.guild.owner or permissions.administrator
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
@@ -36,15 +38,17 @@ def check_global_setting_admin():
|
|||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
is_guild_owner = author == ctx.guild.owner
|
is_guild_owner = author == ctx.guild.owner
|
||||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
@@ -58,8 +62,9 @@ class Bank:
|
|||||||
|
|
||||||
# SECTION commands
|
# SECTION commands
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@check_global_setting_guildowner()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.group(autohelp=True)
|
||||||
async def bankset(self, ctx: commands.Context):
|
async def bankset(self, ctx: commands.Context):
|
||||||
"""Base command for bank settings"""
|
"""Base command for bank settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ class Cleanup:
|
|||||||
to_delete = []
|
to_delete = []
|
||||||
too_old = False
|
too_old = False
|
||||||
|
|
||||||
while not too_old and len(to_delete) - 1 < number:
|
while not too_old and len(to_delete) < number:
|
||||||
message = None
|
message = None
|
||||||
async for message in channel.history(limit=limit, before=before, after=after):
|
async for message in channel.history(limit=limit, before=before, after=after):
|
||||||
if (
|
if (
|
||||||
(not number or len(to_delete) - 1 < number)
|
(not number or len(to_delete) < number)
|
||||||
and check(message)
|
and check(message)
|
||||||
and (ctx.message.created_at - message.created_at).days < 14
|
and (ctx.message.created_at - message.created_at).days < 14
|
||||||
and (delete_pinned or not message.pinned)
|
and (delete_pinned or not message.pinned)
|
||||||
@@ -92,7 +92,7 @@ class Cleanup:
|
|||||||
before = message
|
before = message
|
||||||
return to_delete
|
return to_delete
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def cleanup(self, ctx: commands.Context):
|
async def cleanup(self, ctx: commands.Context):
|
||||||
"""Deletes messages."""
|
"""Deletes messages."""
|
||||||
@@ -100,7 +100,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def text(
|
async def text(
|
||||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
@@ -112,6 +111,10 @@ class Cleanup:
|
|||||||
Remember to use double quotes."""
|
Remember to use double quotes."""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -150,7 +153,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def user(
|
async def user(
|
||||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
@@ -159,6 +161,10 @@ class Cleanup:
|
|||||||
Examples:
|
Examples:
|
||||||
cleanup user @\u200bTwentysix 2
|
cleanup user @\u200bTwentysix 2
|
||||||
cleanup user Red 6"""
|
cleanup user Red 6"""
|
||||||
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
|
|
||||||
member = None
|
member = None
|
||||||
try:
|
try:
|
||||||
@@ -171,7 +177,6 @@ class Cleanup:
|
|||||||
else:
|
else:
|
||||||
_id = member.id
|
_id = member.id
|
||||||
|
|
||||||
channel = ctx.channel
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -212,7 +217,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||||
"""Deletes all messages after specified message.
|
"""Deletes all messages after specified message.
|
||||||
|
|
||||||
@@ -224,6 +228,9 @@ class Cleanup:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -250,7 +257,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Deletes last X messages.
|
"""Deletes last X messages.
|
||||||
|
|
||||||
@@ -258,6 +264,9 @@ class Cleanup:
|
|||||||
cleanup messages 26"""
|
cleanup messages 26"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
@@ -284,11 +293,13 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command(name="bot")
|
@cleanup.command(name="bot")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Cleans up command messages and messages from the bot."""
|
"""Cleans up command messages and messages from the bot."""
|
||||||
|
|
||||||
channel = ctx.message.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -412,7 +423,7 @@ class Cleanup:
|
|||||||
if author == self.bot.user:
|
if author == self.bot.user:
|
||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
if channel.name:
|
if ctx.guild:
|
||||||
channel_name = "channel " + channel.name
|
channel_name = "channel " + channel.name
|
||||||
else:
|
else:
|
||||||
channel_name = str(channel)
|
channel_name = str(channel)
|
||||||
|
|||||||
@@ -141,13 +141,13 @@ class CustomCommands:
|
|||||||
self.config.register_guild(commands={})
|
self.config.register_guild(commands={})
|
||||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||||
|
|
||||||
@commands.group(aliases=["cc"], autohelp=True)
|
@commands.group(aliases=["cc"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def customcom(self, ctx: commands.Context):
|
async def customcom(self, ctx: commands.Context):
|
||||||
"""Custom commands management"""
|
"""Custom commands management"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@customcom.group(name="add", autohelp=True)
|
@customcom.group(name="add")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add(self, ctx: commands.Context):
|
async def cc_add(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class Downloader:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def repo(self, ctx):
|
async def repo(self, ctx):
|
||||||
"""
|
"""
|
||||||
@@ -234,7 +234,7 @@ class Downloader:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||||
if repo.install_msg is not None:
|
if repo.install_msg is not None:
|
||||||
await ctx.send(repo.install_msg)
|
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@repo.command(name="delete")
|
@repo.command(name="delete")
|
||||||
async def _repo_del(self, ctx, repo_name: Repo):
|
async def _repo_del(self, ctx, repo_name: Repo):
|
||||||
@@ -272,7 +272,7 @@ class Downloader:
|
|||||||
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
|
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def cog(self, ctx):
|
async def cog(self, ctx):
|
||||||
"""
|
"""
|
||||||
@@ -319,7 +319,7 @@ class Downloader:
|
|||||||
|
|
||||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||||
if cog.install_msg is not None:
|
if cog.install_msg is not None:
|
||||||
await ctx.send(cog.install_msg)
|
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@cog.command(name="uninstall")
|
@cog.command(name="uninstall")
|
||||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import discord
|
|||||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||||
from redbot.core import Config, bank, commands
|
from redbot.core import Config, bank, commands
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import pagify, box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
@@ -137,7 +138,8 @@ class Economy:
|
|||||||
self.config.register_role(**self.default_role_settings)
|
self.config.register_role(**self.default_role_settings)
|
||||||
self.slot_register = defaultdict(dict)
|
self.slot_register = defaultdict(dict)
|
||||||
|
|
||||||
@commands.group(name="bank", autohelp=True)
|
@guild_only_check()
|
||||||
|
@commands.group(name="bank")
|
||||||
async def _bank(self, ctx: commands.Context):
|
async def _bank(self, ctx: commands.Context):
|
||||||
"""Bank operations"""
|
"""Bank operations"""
|
||||||
pass
|
pass
|
||||||
@@ -209,7 +211,6 @@ class Economy:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
@guild_only_check()
|
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def reset(self, ctx, confirmation: bool = False):
|
async def reset(self, ctx, confirmation: bool = False):
|
||||||
"""Deletes bank accounts"""
|
"""Deletes bank accounts"""
|
||||||
@@ -230,8 +231,8 @@ class Economy:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
|
@commands.command()
|
||||||
async def payday(self, ctx: commands.Context):
|
async def payday(self, ctx: commands.Context):
|
||||||
"""Get some free currency"""
|
"""Get some free currency"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
@@ -251,7 +252,7 @@ class Economy:
|
|||||||
_(
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||||
"You currently have {3} {1}.\n\n"
|
"You currently have {3} {1}.\n\n"
|
||||||
"You are currently #{4} on the leaderboard!"
|
"You are currently #{4} on the global leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author,
|
author,
|
||||||
credits_name,
|
credits_name,
|
||||||
@@ -309,8 +310,8 @@ class Economy:
|
|||||||
"""Prints out the leaderboard
|
"""Prints out the leaderboard
|
||||||
|
|
||||||
Defaults to top 10"""
|
Defaults to top 10"""
|
||||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
author = ctx.author
|
||||||
if top < 1:
|
if top < 1:
|
||||||
top = 10
|
top = 10
|
||||||
if (
|
if (
|
||||||
@@ -320,25 +321,25 @@ class Economy:
|
|||||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||||
if len(bank_sorted) < top:
|
if len(bank_sorted) < top:
|
||||||
top = len(bank_sorted)
|
top = len(bank_sorted)
|
||||||
highscore = ""
|
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
||||||
for pos, acc in enumerate(bank_sorted, 1):
|
highscores = [
|
||||||
pos = pos
|
(
|
||||||
poswidth = 2
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||||
name = acc[1]["name"]
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
namewidth = 35
|
|
||||||
balance = acc[1]["balance"]
|
|
||||||
balwidth = 2
|
|
||||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
|
||||||
pos=pos,
|
|
||||||
poswidth=poswidth,
|
|
||||||
name=name,
|
|
||||||
namewidth=namewidth,
|
|
||||||
balance=balance,
|
|
||||||
balwidth=balwidth,
|
|
||||||
)
|
)
|
||||||
if highscore != "":
|
if acc[0] != author.id
|
||||||
for page in pagify(highscore, shorten_by=12):
|
else (
|
||||||
await ctx.send(box(page, lang="py"))
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
||||||
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
|
)
|
||||||
|
for pos, acc in enumerate(bank_sorted, 1)
|
||||||
|
]
|
||||||
|
if highscores:
|
||||||
|
pages = [
|
||||||
|
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
||||||
|
for x in range(0, len(highscores), 10)
|
||||||
|
]
|
||||||
|
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no accounts in the bank."))
|
await ctx.send(_("There are no accounts in the bank."))
|
||||||
|
|
||||||
@@ -438,7 +439,7 @@ class Economy:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def economyset(self, ctx: commands.Context):
|
async def economyset(self, ctx: commands.Context):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Filter:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@commands.group(name="filter", autohelp=True)
|
@commands.group(name="filter")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def _filter(self, ctx: commands.Context):
|
async def _filter(self, ctx: commands.Context):
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class Image:
|
|||||||
def __unload(self):
|
def __unload(self):
|
||||||
self.session.close()
|
self.session.close()
|
||||||
|
|
||||||
@commands.group(name="imgur", autohelp=True)
|
@commands.group(name="imgur")
|
||||||
async def _imgur(self, ctx):
|
async def _imgur(self, ctx):
|
||||||
"""Retrieves pictures from imgur
|
"""Retrieves pictures from imgur
|
||||||
|
|
||||||
|
|||||||
@@ -161,13 +161,13 @@ class Mod:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def modset(self, ctx: commands.Context):
|
async def modset(self, ctx: commands.Context):
|
||||||
"""Manages server administration settings."""
|
"""Manages server administration settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
|
guild = ctx.guild
|
||||||
# Display current settings
|
# Display current settings
|
||||||
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
||||||
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
||||||
@@ -628,7 +628,6 @@ class Mod:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
||||||
"""Unbans the target user.
|
"""Unbans the target user.
|
||||||
|
|
||||||
@@ -636,13 +635,17 @@ class Mod:
|
|||||||
1. Copy it from the mod log case (if one was created), or
|
1. Copy it from the mod log case (if one was created), or
|
||||||
2. enable developer mode, go to Bans in this server's settings, right-
|
2. enable developer mode, go to Bans in this server's settings, right-
|
||||||
click the user and select 'Copy ID'."""
|
click the user and select 'Copy ID'."""
|
||||||
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).ban_members:
|
||||||
|
await ctx.send("I need the Ban Members permission to do this.")
|
||||||
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
user = await self.bot.get_user_info(user_id)
|
user = await self.bot.get_user_info(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
await ctx.send(_("Couldn't find a user with that ID!"))
|
await ctx.send(_("Couldn't find a user with that ID!"))
|
||||||
return
|
return
|
||||||
reason = get_audit_reason(ctx.author, reason)
|
audit_reason = get_audit_reason(ctx.author, reason)
|
||||||
bans = await guild.bans()
|
bans = await guild.bans()
|
||||||
bans = [be.user for be in bans]
|
bans = [be.user for be in bans]
|
||||||
if user not in bans:
|
if user not in bans:
|
||||||
@@ -651,7 +654,7 @@ class Mod:
|
|||||||
queue_entry = (guild.id, user.id)
|
queue_entry = (guild.id, user.id)
|
||||||
self.unban_queue.append(queue_entry)
|
self.unban_queue.append(queue_entry)
|
||||||
try:
|
try:
|
||||||
await guild.unban(user, reason=reason)
|
await guild.unban(user, reason=audit_reason)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
self.unban_queue.remove(queue_entry)
|
self.unban_queue.remove(queue_entry)
|
||||||
await ctx.send(_("Something went wrong while attempting to unban that user"))
|
await ctx.send(_("Something went wrong while attempting to unban that user"))
|
||||||
@@ -832,7 +835,7 @@ class Mod:
|
|||||||
_("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
|
_("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_channel=True)
|
@checks.mod_or_permissions(manage_channel=True)
|
||||||
async def mute(self, ctx: commands.Context):
|
async def mute(self, ctx: commands.Context):
|
||||||
@@ -998,7 +1001,7 @@ class Mod:
|
|||||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_channel=True)
|
@checks.mod_or_permissions(manage_channel=True)
|
||||||
async def unmute(self, ctx: commands.Context):
|
async def unmute(self, ctx: commands.Context):
|
||||||
@@ -1164,13 +1167,12 @@ class Mod:
|
|||||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_channels=True)
|
@checks.admin_or_permissions(manage_channels=True)
|
||||||
async def ignore(self, ctx: commands.Context):
|
async def ignore(self, ctx: commands.Context):
|
||||||
"""Adds servers/channels to ignorelist"""
|
"""Adds servers/channels to ignorelist"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
await ctx.send(await self.count_ignored())
|
await ctx.send(await self.count_ignored())
|
||||||
|
|
||||||
@ignore.command(name="channel")
|
@ignore.command(name="channel")
|
||||||
@@ -1187,7 +1189,7 @@ class Mod:
|
|||||||
await ctx.send(_("Channel already in ignore list."))
|
await ctx.send(_("Channel already in ignore list."))
|
||||||
|
|
||||||
@ignore.command(name="server", aliases=["guild"])
|
@ignore.command(name="server", aliases=["guild"])
|
||||||
@commands.has_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
async def ignore_guild(self, ctx: commands.Context):
|
async def ignore_guild(self, ctx: commands.Context):
|
||||||
"""Ignores current server"""
|
"""Ignores current server"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -1197,7 +1199,7 @@ class Mod:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("This server is already being ignored."))
|
await ctx.send(_("This server is already being ignored."))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_channels=True)
|
@checks.admin_or_permissions(manage_channels=True)
|
||||||
async def unignore(self, ctx: commands.Context):
|
async def unignore(self, ctx: commands.Context):
|
||||||
@@ -1220,7 +1222,7 @@ class Mod:
|
|||||||
await ctx.send(_("That channel is not in the ignore list."))
|
await ctx.send(_("That channel is not in the ignore list."))
|
||||||
|
|
||||||
@unignore.command(name="server", aliases=["guild"])
|
@unignore.command(name="server", aliases=["guild"])
|
||||||
@commands.has_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
async def unignore_guild(self, ctx: commands.Context):
|
async def unignore_guild(self, ctx: commands.Context):
|
||||||
"""Removes current guild from ignore list"""
|
"""Removes current guild from ignore list"""
|
||||||
guild = ctx.message.guild
|
guild = ctx.message.guild
|
||||||
@@ -1362,9 +1364,9 @@ class Mod:
|
|||||||
names = await self.settings.user(user).past_names()
|
names = await self.settings.user(user).past_names()
|
||||||
nicks = await self.settings.member(user).past_nicks()
|
nicks = await self.settings.member(user).past_nicks()
|
||||||
if names:
|
if names:
|
||||||
names = [escape(name, mass_mentions=True) for name in names]
|
names = [escape(name, mass_mentions=True) for name in names if name]
|
||||||
if nicks:
|
if nicks:
|
||||||
nicks = [escape(nick, mass_mentions=True) for nick in nicks]
|
nicks = [escape(nick, mass_mentions=True) for nick in nicks if nick]
|
||||||
return names, nicks
|
return names, nicks
|
||||||
|
|
||||||
async def check_tempban_expirations(self):
|
async def check_tempban_expirations(self):
|
||||||
@@ -1602,18 +1604,22 @@ class Mod:
|
|||||||
if entry.target == target:
|
if entry.target == target:
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
async def on_member_update(self, before, after):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
if before.name != after.name:
|
if before.name != after.name:
|
||||||
async with self.settings.user(before).past_names() as name_list:
|
async with self.settings.user(before).past_names() as name_list:
|
||||||
if after.nick in name_list:
|
while None in name_list: # clean out null entries from a bug
|
||||||
|
name_list.remove(None)
|
||||||
|
if after.name in name_list:
|
||||||
# Ensure order is maintained without duplicates occuring
|
# Ensure order is maintained without duplicates occuring
|
||||||
name_list.remove(after.nick)
|
name_list.remove(after.name)
|
||||||
name_list.append(after.nick)
|
name_list.append(after.name)
|
||||||
while len(name_list) > 20:
|
while len(name_list) > 20:
|
||||||
name_list.pop(0)
|
name_list.pop(0)
|
||||||
|
|
||||||
if before.nick != after.nick and after.nick is not None:
|
if before.nick != after.nick and after.nick is not None:
|
||||||
async with self.settings.member(before).past_nicks() as nick_list:
|
async with self.settings.member(before).past_nicks() as nick_list:
|
||||||
|
while None in nick_list: # clean out null entries from a bug
|
||||||
|
nick_list.remove(None)
|
||||||
if after.nick in nick_list:
|
if after.nick in nick_list:
|
||||||
nick_list.remove(after.nick)
|
nick_list.remove(after.nick)
|
||||||
nick_list.append(after.nick)
|
nick_list.append(after.nick)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class ModLog:
|
|||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def modlogset(self, ctx: commands.Context):
|
async def modlogset(self, ctx: commands.Context):
|
||||||
"""Settings for the mod log"""
|
"""Settings for the mod log"""
|
||||||
|
|||||||
@@ -27,3 +27,18 @@ class RuleType(commands.Converter):
|
|||||||
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):
|
||||||
|
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||||
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
|
return "allow"
|
||||||
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
|
return "deny"
|
||||||
|
if arg.lower() in ("clear", "reset"):
|
||||||
|
return "clear"
|
||||||
|
|
||||||
|
raise commands.BadArgument(
|
||||||
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
||||||
|
"".format(arg=arg)
|
||||||
|
)
|
||||||
|
|||||||
102
redbot/cogs/permissions/mass_resolution.py
Normal file
102
redbot/cogs/permissions/mass_resolution.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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
|
||||||
@@ -7,16 +7,19 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core import checks
|
from redbot.core import checks
|
||||||
from redbot.core.config import Config
|
from redbot.core.config import Config
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.caching import LRUDict
|
||||||
|
|
||||||
from .resolvers import val_if_check_is_valid, resolve_models
|
from .resolvers import val_if_check_is_valid, resolve_models, entries_from_ctx
|
||||||
from .yaml_handler import yamlset_acl, yamlget_acl
|
from .yaml_handler import yamlset_acl, yamlget_acl
|
||||||
from .converters import CogOrCommand, RuleType
|
from .converters import CogOrCommand, RuleType, ClearableRuleType
|
||||||
|
from .mass_resolution import mass_resolve
|
||||||
|
|
||||||
_models = ["owner", "guildowner", "admin", "mod", "all"]
|
_models = ["owner", "guildowner", "admin", "mod", "all"]
|
||||||
|
|
||||||
_ = Translator("Permissions", __file__)
|
_ = Translator("Permissions", __file__)
|
||||||
|
|
||||||
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
||||||
|
Y_OR_N = {"y": True, "yes": True, "n": False, "no": False}
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@@ -34,8 +37,32 @@ class Permissions:
|
|||||||
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||||
self.config.register_global(owner_models={})
|
self.config.register_global(owner_models={})
|
||||||
self.config.register_guild(owner_models={})
|
self.config.register_guild(owner_models={})
|
||||||
|
self.cache = LRUDict(size=25000) # This can be tuned later
|
||||||
|
|
||||||
async def __global_check(self, ctx):
|
async def get_user_ctx_overrides(self, ctx: commands.Context) -> dict:
|
||||||
|
"""
|
||||||
|
This takes a context object, and returns a dict of
|
||||||
|
|
||||||
|
allowed: list of commands
|
||||||
|
denied: list of commands
|
||||||
|
default: list of commands
|
||||||
|
|
||||||
|
representing how permissions interacts with the
|
||||||
|
user, channel, guild, and (possibly) voice channel
|
||||||
|
for all commands on the bot (not just the one in the context object)
|
||||||
|
|
||||||
|
This mainly exists for use by the help formatter,
|
||||||
|
but others may find it useful
|
||||||
|
|
||||||
|
Unlike the rest of the permission system, if other models are added later,
|
||||||
|
due to optimizations made for this, this needs to be adjusted accordingly
|
||||||
|
|
||||||
|
This does not account for before and after permission hooks,
|
||||||
|
these need to be checked seperately
|
||||||
|
"""
|
||||||
|
return await mass_resolve(ctx=ctx, config=self.config)
|
||||||
|
|
||||||
|
async def __global_check(self, ctx: commands.Context) -> bool:
|
||||||
"""
|
"""
|
||||||
Yes, this is needed on top of hooking into checks.py
|
Yes, this is needed on top of hooking into checks.py
|
||||||
to ensure that unchecked commands can still be managed by permissions
|
to ensure that unchecked commands can still be managed by permissions
|
||||||
@@ -68,12 +95,6 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
return True
|
return True
|
||||||
voice_channel = None
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
voice_channel = ctx.author.voice.voice_channel
|
|
||||||
entries = [x 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])
|
|
||||||
|
|
||||||
before = [
|
before = [
|
||||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
|
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
|
||||||
@@ -86,11 +107,26 @@ class Permissions:
|
|||||||
if override is not None:
|
if override is not None:
|
||||||
return override
|
return override
|
||||||
|
|
||||||
for model in self.resolution_order[level]:
|
# checked ids + configureable to be checked against
|
||||||
override_model = getattr(self, model + "_model", None)
|
cache_tup = entries_from_ctx(ctx) + (
|
||||||
override = await override_model(ctx) if override_model else None
|
ctx.cog.__class__.__name__,
|
||||||
|
ctx.command.qualified_name,
|
||||||
|
)
|
||||||
|
if cache_tup in self.cache:
|
||||||
|
override = self.cache[cache_tup]
|
||||||
if override is not None:
|
if override is not None:
|
||||||
return override
|
return override
|
||||||
|
else:
|
||||||
|
for model in self.resolution_order[level]:
|
||||||
|
if ctx.guild is None and model != "owner":
|
||||||
|
break
|
||||||
|
override_model = getattr(self, model + "_model", None)
|
||||||
|
override = await override_model(ctx) if override_model else None
|
||||||
|
if override is not None:
|
||||||
|
self.cache[cache_tup] = override
|
||||||
|
return override
|
||||||
|
# This is intentional not being in an else block
|
||||||
|
self.cache[cache_tup] = None
|
||||||
|
|
||||||
after = [
|
after = [
|
||||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
|
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
|
||||||
@@ -115,7 +151,8 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
Handles guild level overrides
|
Handles guild level overrides
|
||||||
"""
|
"""
|
||||||
|
if ctx.guild is None:
|
||||||
|
return None
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
return resolve_models(ctx=ctx, models=models)
|
return resolve_models(ctx=ctx, models=models)
|
||||||
|
|
||||||
@@ -125,7 +162,7 @@ class Permissions:
|
|||||||
# async def admin_model(self, ctx: commands.Context) -> bool:
|
# async def admin_model(self, ctx: commands.Context) -> bool:
|
||||||
# async def mod_model(self, ctx: commands.Context) -> bool:
|
# async def mod_model(self, ctx: commands.Context) -> bool:
|
||||||
|
|
||||||
@commands.group(aliases=["p"], autohelp=True)
|
@commands.group(aliases=["p"])
|
||||||
async def permissions(self, ctx: commands.Context):
|
async def permissions(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Permission management tools
|
Permission management tools
|
||||||
@@ -223,6 +260,7 @@ class Permissions:
|
|||||||
return await ctx.send(_("Invalid syntax."))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="getglobalacl")
|
@permissions.command(name="getglobalacl")
|
||||||
@@ -249,6 +287,7 @@ class Permissions:
|
|||||||
return await ctx.send(_("Invalid syntax."))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -278,6 +317,7 @@ class Permissions:
|
|||||||
return await ctx.send(_("Invalid syntax."))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="updateglobalacl")
|
@permissions.command(name="updateglobalacl")
|
||||||
@@ -297,6 +337,7 @@ class Permissions:
|
|||||||
return await ctx.send(_("Invalid syntax."))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="addglobalrule")
|
@permissions.command(name="addglobalrule")
|
||||||
@@ -340,6 +381,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].append(obj)
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule added."))
|
await ctx.send(_("Rule added."))
|
||||||
|
self.invalidate_cache(type_name, obj)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -384,6 +426,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].append(obj)
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule added."))
|
await ctx.send(_("Rule added."))
|
||||||
|
self.invalidate_cache(type_name, obj)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="removeglobalrule")
|
@permissions.command(name="removeglobalrule")
|
||||||
@@ -427,6 +470,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule removed."))
|
await ctx.send(_("Rule removed."))
|
||||||
|
self.invalidate_cache(obj, type_name)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -471,23 +515,18 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule removed."))
|
await ctx.send(_("Rule removed."))
|
||||||
|
self.invalidate_cache(obj, type_name)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@permissions.command(name="setdefaultguildrule")
|
@permissions.command(name="setdefaultguildrule")
|
||||||
async def set_default_guild_rule(
|
async def set_default_guild_rule(
|
||||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Sets the default behavior for a cog or command if no rule is set
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
Use with a cog or command and no setting to clear the default and defer to
|
|
||||||
normal check logic
|
|
||||||
"""
|
"""
|
||||||
if allow_or_deny:
|
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
|
||||||
else:
|
|
||||||
val_to_set = None
|
|
||||||
|
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
@@ -501,23 +540,17 @@ class Permissions:
|
|||||||
|
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Default set."))
|
await ctx.send(_("Default set."))
|
||||||
|
self.invalidate_cache(type_name)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="setdefaultglobalrule")
|
@permissions.command(name="setdefaultglobalrule")
|
||||||
async def set_default_global_rule(
|
async def set_default_global_rule(
|
||||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Sets the default behavior for a cog or command if no rule is set
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
Use with a cog or command and no setting to clear the default and defer to
|
|
||||||
normal check logic
|
|
||||||
"""
|
"""
|
||||||
|
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||||
if allow_or_deny:
|
|
||||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
|
||||||
else:
|
|
||||||
val_to_set = None
|
|
||||||
|
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.owner_models() as models:
|
async with self.config.owner_models() as models:
|
||||||
@@ -531,32 +564,17 @@ class Permissions:
|
|||||||
|
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Default set."))
|
await ctx.send(_("Default set."))
|
||||||
|
self.invalidate_cache(type_name)
|
||||||
|
|
||||||
@commands.bot_has_permissions(add_reactions=True)
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="clearglobalsettings")
|
@permissions.command(name="clearglobalsettings")
|
||||||
async def clear_globals(self, ctx: commands.Context):
|
async def clear_globals(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Clears all global rules.
|
Clears all global rules.
|
||||||
"""
|
"""
|
||||||
|
await self._confirm_then_clear_rules(ctx, is_guild=False)
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
m = await ctx.send("Are you sure?")
|
|
||||||
for r in REACTS.keys():
|
|
||||||
await m.add_reaction(r)
|
|
||||||
try:
|
|
||||||
reaction, user = await self.bot.wait_for(
|
|
||||||
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
|
||||||
|
|
||||||
if REACTS.get(str(reaction)):
|
|
||||||
await self.config.owner_models.clear()
|
|
||||||
await ctx.send(_("Global settings cleared."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Okay."))
|
|
||||||
|
|
||||||
@commands.bot_has_permissions(add_reactions=True)
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@permissions.command(name="clearguildsettings")
|
@permissions.command(name="clearguildsettings")
|
||||||
@@ -564,23 +582,61 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
Clears all guild rules.
|
Clears all guild rules.
|
||||||
"""
|
"""
|
||||||
|
await self._confirm_then_clear_rules(ctx, is_guild=True)
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
m = await ctx.send("Are you sure?")
|
async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
|
||||||
for r in REACTS.keys():
|
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
|
||||||
await m.add_reaction(r)
|
m = await ctx.send(_("Are you sure?"))
|
||||||
try:
|
for r in REACTS.keys():
|
||||||
reaction, user = await self.bot.wait_for(
|
await m.add_reaction(r)
|
||||||
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
try:
|
||||||
)
|
reaction, user = await self.bot.wait_for(
|
||||||
except asyncio.TimeoutError:
|
"reaction_add",
|
||||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
check=lambda r, u: u == ctx.author and str(r) in REACTS,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||||
|
|
||||||
if REACTS.get(str(reaction)):
|
agreed = REACTS.get(str(reaction))
|
||||||
await self.config.guild(ctx.guild).owner_models.clear()
|
else:
|
||||||
await ctx.send(_("Guild settings cleared."))
|
await ctx.send(_("Are you sure? (y/n)"))
|
||||||
|
try:
|
||||||
|
message = await self.bot.wait_for(
|
||||||
|
"message",
|
||||||
|
check=lambda m: m.author == ctx.author and m.content in Y_OR_N,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with yes or no next time."))
|
||||||
|
|
||||||
|
agreed = Y_OR_N.get(message.content.lower())
|
||||||
|
|
||||||
|
if agreed:
|
||||||
|
if is_guild:
|
||||||
|
await self.config.guild(ctx.guild).owner_models.clear()
|
||||||
|
await ctx.send(_("Guild settings cleared."))
|
||||||
|
else:
|
||||||
|
await self.config.owner_models.clear()
|
||||||
|
await ctx.send(_("Global settings cleared."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Okay."))
|
await ctx.send(_("Okay."))
|
||||||
|
|
||||||
|
def invalidate_cache(self, *to_invalidate):
|
||||||
|
"""
|
||||||
|
Either invalidates the entire cache (if given no objects)
|
||||||
|
or does a partial invalidation based on passed objects
|
||||||
|
"""
|
||||||
|
if len(to_invalidate) == 0:
|
||||||
|
self.cache.clear()
|
||||||
|
return
|
||||||
|
# LRUDict inherits from ordered dict, hence the syntax below
|
||||||
|
stil_valid = [
|
||||||
|
(k, v) for k, v in self.cache.items() if not any(obj in k for obj in to_invalidate)
|
||||||
|
]
|
||||||
|
self.cache = LRUDict(*stil_valid, size=self.cache.size)
|
||||||
|
|
||||||
def find_object_uniquely(self, info: str) -> int:
|
def find_object_uniquely(self, info: str) -> int:
|
||||||
"""
|
"""
|
||||||
Finds an object uniquely, returns it's id or returns None
|
Finds an object uniquely, returns it's id or returns None
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ from redbot.core import commands
|
|||||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
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:
|
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
|
Returns the value from a check if it is valid
|
||||||
@@ -56,23 +73,7 @@ def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) ->
|
|||||||
"""
|
"""
|
||||||
resolves specific lists
|
resolves specific lists
|
||||||
"""
|
"""
|
||||||
|
for entry in entries_from_ctx(ctx):
|
||||||
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
|
|
||||||
|
|
||||||
for entry in entries:
|
|
||||||
if entry in whitelist:
|
if entry in whitelist:
|
||||||
return True
|
return True
|
||||||
if entry in blacklist:
|
if entry in blacklist:
|
||||||
|
|||||||
@@ -56,32 +56,32 @@ class Reports:
|
|||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.group(name="reportset", autohelp=True)
|
@commands.group(name="reportset")
|
||||||
async def reportset(self, ctx: commands.Context):
|
async def reportset(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
settings for reports
|
Settings for the report system.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="output")
|
@reportset.command(name="output")
|
||||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""sets the output channel"""
|
"""Set the channel where reports will show up"""
|
||||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||||
await ctx.send(_("Report Channel Set."))
|
await ctx.send(_("The report channel has been set."))
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="toggleactive")
|
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||||
async def report_toggle(self, ctx: commands.Context):
|
async def report_toggle(self, ctx: commands.Context):
|
||||||
"""Toggles whether the Reporting tool is enabled or not"""
|
"""Enables or Disables reporting for the server"""
|
||||||
|
|
||||||
active = await self.config.guild(ctx.guild).active()
|
active = await self.config.guild(ctx.guild).active()
|
||||||
active = not active
|
active = not active
|
||||||
await self.config.guild(ctx.guild).active.set(active)
|
await self.config.guild(ctx.guild).active.set(active)
|
||||||
if active:
|
if active:
|
||||||
await ctx.send(_("Reporting now enabled"))
|
await ctx.send(_("Reporting is now enabled"))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Reporting disabled."))
|
await ctx.send(_("Reporting is now disabled."))
|
||||||
|
|
||||||
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||||
ret = False
|
ret = False
|
||||||
@@ -105,7 +105,7 @@ class Reports:
|
|||||||
*,
|
*,
|
||||||
mod: bool = False,
|
mod: bool = False,
|
||||||
permissions: Union[discord.Permissions, dict] = None,
|
permissions: Union[discord.Permissions, dict] = None,
|
||||||
prompt: str = ""
|
prompt: str = "",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
discovers which of shared guilds between the bot
|
discovers which of shared guilds between the bot
|
||||||
@@ -175,7 +175,10 @@ class Reports:
|
|||||||
if await self.bot.embed_requested(channel, author):
|
if await self.bot.embed_requested(channel, author):
|
||||||
em = discord.Embed(description=report)
|
em = discord.Embed(description=report)
|
||||||
em.set_author(
|
em.set_author(
|
||||||
name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url
|
name=_("Report from {author}{maybe_nick}").format(
|
||||||
|
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
|
||||||
|
),
|
||||||
|
icon_url=author.avatar_url,
|
||||||
)
|
)
|
||||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||||
send_content = None
|
send_content = None
|
||||||
@@ -201,10 +204,10 @@ class Reports:
|
|||||||
@commands.group(name="report", invoke_without_command=True)
|
@commands.group(name="report", invoke_without_command=True)
|
||||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||||
"""
|
"""
|
||||||
Follow the prompts to make a report
|
Send a report.
|
||||||
|
|
||||||
optionally use with a report message
|
Use without arguments for interactive reporting, or do
|
||||||
to use it non interactively
|
[p]report <text> to use it non-interactively.
|
||||||
"""
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -224,14 +227,17 @@ class Reports:
|
|||||||
if self.antispam[guild.id][author.id].spammy:
|
if self.antispam[guild.id][author.id].spammy:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_(
|
_(
|
||||||
"You've sent a few too many of these recently. "
|
"You've sent too many reports recently. "
|
||||||
"Contact a server admin to resolve this, or try again "
|
"Please contact a server admin if this is important matter, "
|
||||||
"later."
|
"or please wait and try again later."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if author.id in self.user_cache:
|
if author.id in self.user_cache:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_("Please finish making your prior report before making an additional one")
|
_(
|
||||||
|
"Please finish making your prior report before trying to make an "
|
||||||
|
"additional one!"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.user_cache.append(author.id)
|
self.user_cache.append(author.id)
|
||||||
|
|
||||||
@@ -263,7 +269,9 @@ class Reports:
|
|||||||
|
|
||||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||||
if val is None:
|
if val is None:
|
||||||
await author.send(_("There was an error sending your report."))
|
await author.send(
|
||||||
|
_("There was an error sending your report, please contact a server admin.")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||||
self.antispam[guild.id][author.id].stamp()
|
self.antispam[guild.id][author.id].stamp()
|
||||||
@@ -313,10 +321,12 @@ class Reports:
|
|||||||
@report.command(name="interact")
|
@report.command(name="interact")
|
||||||
async def response(self, ctx, ticket_number: int):
|
async def response(self, ctx, ticket_number: int):
|
||||||
"""
|
"""
|
||||||
opens a message tunnel between things you say in this channel
|
Open a message tunnel.
|
||||||
and the ticket opener's direct messages
|
|
||||||
|
|
||||||
tunnels do not persist across bot restarts
|
This tunnel will forward things you say in this channel
|
||||||
|
to the ticket opener's direct messages.
|
||||||
|
|
||||||
|
Tunnels do not persist across bot restarts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# note, mod_or_permissions is an implicit guild_only
|
# note, mod_or_permissions is an implicit guild_only
|
||||||
@@ -342,14 +352,15 @@ class Reports:
|
|||||||
)
|
)
|
||||||
|
|
||||||
big_topic = _(
|
big_topic = _(
|
||||||
"{who} opened a 2-way communication."
|
"{who} opened a 2-way communication "
|
||||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||||
"(8MB file size limitation on uploads) "
|
"(8MB file size limitation on uploads) "
|
||||||
"will be forwarded to them until the communication is closed.\n"
|
"will be forwarded to them until the communication is closed.\n"
|
||||||
"You can close a communication at any point "
|
"You can close a communication at any point by reacting with "
|
||||||
"by reacting with the X to the last message recieved. "
|
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||||
"\nAny message succesfully forwarded will be marked with a check."
|
"Any message succesfully forwarded will be marked with "
|
||||||
"\nTunnels are not persistent across bot restarts."
|
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||||
|
"Tunnels are not persistent across bot restarts."
|
||||||
)
|
)
|
||||||
topic = big_topic.format(
|
topic = big_topic.format(
|
||||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||||
@@ -357,8 +368,7 @@ class Reports:
|
|||||||
try:
|
try:
|
||||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("User has disabled DMs."))
|
await ctx.send(_("That user has DMs disabled."))
|
||||||
tun.close()
|
|
||||||
else:
|
else:
|
||||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||||
|
|||||||
@@ -75,14 +75,14 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def twitch(self, ctx: commands.Context, channel_name: str):
|
async def twitch(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Twitch channel is streaming"""
|
"""Checks if a Twitch channel is live"""
|
||||||
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
||||||
stream = TwitchStream(name=channel_name, token=token)
|
stream = TwitchStream(name=channel_name, token=token)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
||||||
"""Checks if a Youtube channel is streaming"""
|
"""Checks if a Youtube channel is live"""
|
||||||
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
||||||
is_name = self.check_name_or_id(channel_id_or_name)
|
is_name = self.check_name_or_id(channel_id_or_name)
|
||||||
if is_name:
|
if is_name:
|
||||||
@@ -93,19 +93,19 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Hitbox channel is streaming"""
|
"""Checks if a Hitbox channel is live"""
|
||||||
stream = HitboxStream(name=channel_name)
|
stream = HitboxStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def mixer(self, ctx: commands.Context, channel_name: str):
|
async def mixer(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Mixer channel is streaming"""
|
"""Checks if a Mixer channel is live"""
|
||||||
stream = MixerStream(name=channel_name)
|
stream = MixerStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def picarto(self, ctx: commands.Context, channel_name: str):
|
async def picarto(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Picarto channel is streaming"""
|
"""Checks if a Picarto channel is live"""
|
||||||
stream = PicartoStream(name=channel_name)
|
stream = PicartoStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@@ -113,9 +113,9 @@ class Streams:
|
|||||||
try:
|
try:
|
||||||
embed = await stream.is_online()
|
embed = await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
await ctx.send(_("The stream is offline."))
|
await ctx.send(_("That user is offline."))
|
||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
await ctx.send(_("The channel doesn't seem to exist."))
|
await ctx.send(_("That channel doesn't seem to exist."))
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. See `{}`.").format(
|
_("The twitch token is either invalid or has not been set. See `{}`.").format(
|
||||||
@@ -124,7 +124,7 @@ class Streams:
|
|||||||
)
|
)
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The Youtube API key is either invalid or has not been set. See {}.").format(
|
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
|
||||||
"`{}streamset youtubekey`".format(ctx.prefix)
|
"`{}streamset youtubekey`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -135,51 +135,50 @@ class Streams:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamalert(self, ctx: commands.Context):
|
async def streamalert(self, ctx: commands.Context):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@streamalert.group(name="twitch", autohelp=True)
|
@streamalert.group(name="twitch")
|
||||||
async def _twitch(self, ctx: commands.Context):
|
async def _twitch(self, ctx: commands.Context):
|
||||||
"""Twitch stream alerts"""
|
"""Twitch stream alerts"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@_twitch.command(name="channel")
|
@_twitch.command(name="channel")
|
||||||
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Twitch stream alert notification in the channel"""
|
"""Sets a Twitch alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
||||||
|
|
||||||
@_twitch.command(name="community")
|
@_twitch.command(name="community")
|
||||||
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
||||||
"""Sets a Twitch stream alert notification in the channel for the specified community."""
|
"""Sets an alert notification in the channel for the specified twitch community."""
|
||||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||||
|
|
||||||
@streamalert.command(name="youtube")
|
@streamalert.command(name="youtube")
|
||||||
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
||||||
"""Sets a Youtube stream alert notification in the channel"""
|
"""Sets a Youtube alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
||||||
|
|
||||||
@streamalert.command(name="hitbox")
|
@streamalert.command(name="hitbox")
|
||||||
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Hitbox stream alert notification in the channel"""
|
"""Sets a Hitbox alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, HitboxStream, channel_name)
|
await self.stream_alert(ctx, HitboxStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="mixer")
|
@streamalert.command(name="mixer")
|
||||||
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Mixer stream alert notification in the channel"""
|
"""Sets a Mixer alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, MixerStream, channel_name)
|
await self.stream_alert(ctx, MixerStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="picarto")
|
@streamalert.command(name="picarto")
|
||||||
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Picarto stream alert notification in the channel"""
|
"""Sets a Picarto alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, PicartoStream, channel_name)
|
await self.stream_alert(ctx, PicartoStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="stop")
|
@streamalert.command(name="stop")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Stops all stream notifications in the channel
|
"""Stops all stream notifications in the channel
|
||||||
|
|
||||||
Adding 'yes' will disable all notifications in the server"""
|
Adding 'yes' will disable all notifications in the server"""
|
||||||
streams = self.streams.copy()
|
streams = self.streams.copy()
|
||||||
local_channel_ids = [c.id for c in ctx.guild.channels]
|
local_channel_ids = [c.id for c in ctx.guild.channels]
|
||||||
@@ -202,7 +201,7 @@ class Streams:
|
|||||||
self.streams = streams
|
self.streams = streams
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
|
|
||||||
msg = _("All {}'s stream alerts have been disabled.").format(
|
msg = _("All the alerts in the {} have been disabled.").format(
|
||||||
"server" if _all else "channel"
|
"server" if _all else "channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -212,7 +211,7 @@ class Streams:
|
|||||||
async def streamalert_list(self, ctx: commands.Context):
|
async def streamalert_list(self, ctx: commands.Context):
|
||||||
streams_list = defaultdict(list)
|
streams_list = defaultdict(list)
|
||||||
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
||||||
msg = _("Active stream alerts:\n\n")
|
msg = _("Active alerts:\n\n")
|
||||||
|
|
||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
for channel_id in stream.channels:
|
for channel_id in stream.channels:
|
||||||
@@ -220,7 +219,7 @@ class Streams:
|
|||||||
streams_list[channel_id].append(stream.name.lower())
|
streams_list[channel_id].append(stream.name.lower())
|
||||||
|
|
||||||
if not streams_list:
|
if not streams_list:
|
||||||
await ctx.send(_("There are no active stream alerts in this server."))
|
await ctx.send(_("There are no active alerts in this server."))
|
||||||
return
|
return
|
||||||
|
|
||||||
for channel_id, streams in streams_list.items():
|
for channel_id, streams in streams_list.items():
|
||||||
@@ -243,16 +242,16 @@ class Streams:
|
|||||||
exists = await self.check_exists(stream)
|
exists = await self.check_exists(stream)
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. See {}.").format(
|
_("Your twitch token is either invalid or has not been set. See {}.").format(
|
||||||
"`{}streamset twitchtoken`".format(ctx.prefix)
|
"`{}streamset twitchtoken`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The Youtube API key is either invalid or has not been set. See {}.").format(
|
_(
|
||||||
"`{}streamset youtubekey`".format(ctx.prefix)
|
"Your Youtube API key is either invalid or has not been set. See {}."
|
||||||
)
|
).format("`{}streamset youtubekey`".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
@@ -294,7 +293,7 @@ class Streams:
|
|||||||
|
|
||||||
await self.add_or_remove_community(ctx, community)
|
await self.add_or_remove_community(ctx, community)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamset(self, ctx: commands.Context):
|
async def streamset(self, ctx: commands.Context):
|
||||||
pass
|
pass
|
||||||
@@ -303,7 +302,6 @@ class Streams:
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||||
"""Set the Client ID for twitch.
|
"""Set the Client ID for twitch.
|
||||||
|
|
||||||
To do this, follow these steps:
|
To do this, follow these steps:
|
||||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
||||||
2. Click *Register Your Application*
|
2. Click *Register Your Application*
|
||||||
@@ -311,7 +309,6 @@ class Streams:
|
|||||||
select an Application Category of your choosing.
|
select an Application Category of your choosing.
|
||||||
4. Click *Register*, and on the following page, copy the Client ID.
|
4. Click *Register*, and on the following page, copy the Client ID.
|
||||||
5. Paste the Client ID into this command. Done!
|
5. Paste the Client ID into this command. Done!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||||
@@ -321,22 +318,19 @@ class Streams:
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||||
"""Sets the API key for Youtube.
|
"""Sets the API key for Youtube.
|
||||||
|
|
||||||
To get one, do the following:
|
To get one, do the following:
|
||||||
|
|
||||||
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
||||||
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
||||||
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
||||||
4. Copy your API key and paste it into this command. Done!
|
4. Copy your API key and paste it into this command. Done!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||||
await ctx.send(_("Youtube key set."))
|
await ctx.send(_("Youtube key set."))
|
||||||
|
|
||||||
@streamset.group(autohelp=True)
|
@streamset.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def mention(self, ctx: commands.Context):
|
async def mention(self, ctx: commands.Context):
|
||||||
"""Sets mentions for stream alerts."""
|
"""Sets mentions for alerts."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@mention.command(aliases=["everyone"])
|
@mention.command(aliases=["everyone"])
|
||||||
@@ -348,15 +342,16 @@ class Streams:
|
|||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_everyone.set(False)
|
await self.db.guild(guild).mention_everyone.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} will no longer be mentioned for a stream alert.").format("@\u200beveryone")
|
_("{} will no longer be mentioned when a stream or community is live").format(
|
||||||
|
"@\u200beveryone"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_everyone.set(True)
|
await self.db.guild(guild).mention_everyone.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200beveryone"
|
||||||
"comes online, {} will be mentioned."
|
)
|
||||||
).format("@\u200beveryone")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command(aliases=["here"])
|
@mention.command(aliases=["here"])
|
||||||
@@ -367,16 +362,13 @@ class Streams:
|
|||||||
current_setting = await self.db.guild(guild).mention_here()
|
current_setting = await self.db.guild(guild).mention_here()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_here.set(False)
|
await self.db.guild(guild).mention_here.set(False)
|
||||||
await ctx.send(
|
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
|
||||||
_("{} will no longer be mentioned for a stream alert.").format("@\u200bhere")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_here.set(True)
|
await self.db.guild(guild).mention_here.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200bhere"
|
||||||
"comes online, {} will be mentioned."
|
)
|
||||||
).format("@\u200bhere")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command()
|
@mention.command()
|
||||||
@@ -390,18 +382,16 @@ class Streams:
|
|||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.role(role).mention.set(False)
|
await self.db.role(role).mention.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} will no longer be mentioned for a stream alert.").format(
|
_("{} will no longer be mentioned for an alert.").format(
|
||||||
"@\u200b{}".format(role.name)
|
"@\u200b{}".format(role.name)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.role(role).mention.set(True)
|
await self.db.role(role).mention.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned." "").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200b{}".format(role.name)
|
||||||
"comes online, {} will be mentioned."
|
)
|
||||||
""
|
|
||||||
).format("@\u200b{}".format(role.name))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@@ -420,7 +410,7 @@ class Streams:
|
|||||||
if stream not in self.streams:
|
if stream not in self.streams:
|
||||||
self.streams.append(stream)
|
self.streams.append(stream)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I'll send a notification in this channel when {} is online.").format(
|
_("I'll now send a notification in this channel when {} is live.").format(
|
||||||
stream.name
|
stream.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -444,7 +434,7 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I'll send a notification in this channel when a "
|
"I'll send a notification in this channel when a "
|
||||||
"channel is streaming to the {} community."
|
"channel is live in the {} community."
|
||||||
""
|
""
|
||||||
).format(community.name)
|
).format(community.name)
|
||||||
)
|
)
|
||||||
@@ -455,7 +445,7 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I won't send notifications about channels streaming "
|
"I won't send notifications about channels streaming "
|
||||||
"to the {} community in this channel anymore."
|
"in the {} community in this channel anymore."
|
||||||
""
|
""
|
||||||
).format(community.name)
|
).format(community.name)
|
||||||
)
|
)
|
||||||
@@ -530,9 +520,9 @@ class Streams:
|
|||||||
mention_str = await self._get_mention_str(channel.guild)
|
mention_str = await self._get_mention_str(channel.guild)
|
||||||
|
|
||||||
if mention_str:
|
if mention_str:
|
||||||
content = "{}, {} is online!".format(mention_str, stream.name)
|
content = "{}, {} is live!".format(mention_str, stream.name)
|
||||||
else:
|
else:
|
||||||
content = "{} is online!".format(stream.name)
|
content = "{} is live!".format(stream.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
m = await channel.send(content, embed=embed)
|
m = await channel.send(content, embed=embed)
|
||||||
@@ -558,7 +548,7 @@ class Streams:
|
|||||||
try:
|
try:
|
||||||
stream_list = await community.get_community_streams()
|
stream_list = await community.get_community_streams()
|
||||||
except CommunityNotFound:
|
except CommunityNotFound:
|
||||||
print(_("Community {} not found!").format(community.name))
|
print(_("The Community {} was not found!").format(community.name))
|
||||||
continue
|
continue
|
||||||
except OfflineCommunity:
|
except OfflineCommunity:
|
||||||
for message in community._messages_cache:
|
for message in community._messages_cache:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Trivia:
|
|||||||
|
|
||||||
self.conf.register_member(wins=0, games=0, total_score=0)
|
self.conf.register_member(wins=0, games=0, total_score=0)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def triviaset(self, ctx: commands.Context):
|
async def triviaset(self, ctx: commands.Context):
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async def warning_points_add_check(
|
|||||||
act = a
|
act = a
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if act: # some action needs to be taken
|
if act and act["exceed_command"] is not None: # some action needs to be taken
|
||||||
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async def warning_points_remove_check(
|
|||||||
act = a
|
act = a
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if act: # some action needs to be taken
|
if act and act["drop_command"] is not None: # some action needs to be taken
|
||||||
await create_and_invoke_context(ctx, act["drop_command"], user)
|
await create_and_invoke_context(ctx, act["drop_command"], user)
|
||||||
|
|
||||||
|
|
||||||
@@ -81,10 +81,11 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
the points threshold for the action"""
|
the points threshold for the action"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Enter the command to be run when the user exceeds the points for "
|
"Enter the command to be run when the user **exceeds the points for "
|
||||||
"this action to occur.\nEnter it exactly as you would if you were "
|
"this action to occur.**\n**If you do not wish to have a command run, enter** "
|
||||||
|
"`none`.\n\nEnter it exactly as you would if you were "
|
||||||
"actually trying to run the command, except don't put a prefix and "
|
"actually trying to run the command, except don't put a prefix and "
|
||||||
"use {user} in place of any user/member arguments\n\n"
|
"use `{user}` in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response."
|
"Please wait 15 seconds before entering your response."
|
||||||
@@ -100,8 +101,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
if msg.content == "none":
|
||||||
|
return None
|
||||||
|
|
||||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||||
if command is None:
|
if command is None:
|
||||||
@@ -121,12 +124,13 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
"""
|
"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Enter the command to be run when the user returns to a value below "
|
"Enter the command to be run when the user **returns to a value below "
|
||||||
"the points for this action to occur. Please note that this is "
|
"the points for this action to occur.** Please note that this is "
|
||||||
"intended to be used for reversal of the action taken when the user "
|
"intended to be used for reversal of the action taken when the user "
|
||||||
"exceeded the action's point value\nEnter it exactly as you would "
|
"exceeded the action's point value.\n**If you do not wish to have a command run "
|
||||||
|
"on dropping points, enter** `none`.\n\nEnter it exactly as you would "
|
||||||
"if you were actually trying to run the command, except don't put a prefix "
|
"if you were actually trying to run the command, except don't put a prefix "
|
||||||
"and use {user} in place of any user/member arguments\n\n"
|
"and use `{user}` in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response."
|
"Please wait 15 seconds before entering your response."
|
||||||
@@ -142,9 +146,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
if msg.content == "none":
|
||||||
|
return None
|
||||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||||
if command is None:
|
if command is None:
|
||||||
await ctx.send(m)
|
await ctx.send(m)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.mod import is_admin_or_superior
|
from redbot.core.utils.mod import is_admin_or_superior
|
||||||
from redbot.core.utils.chat_formatting import warning, pagify
|
from redbot.core.utils.chat_formatting import warning, pagify
|
||||||
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
|
||||||
_ = Translator("Warnings", __file__)
|
_ = Translator("Warnings", __file__)
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ class Warnings:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warningset(self, ctx: commands.Context):
|
async def warningset(self, ctx: commands.Context):
|
||||||
@@ -58,7 +59,7 @@ class Warnings:
|
|||||||
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
|
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnaction(self, ctx: commands.Context):
|
async def warnaction(self, ctx: commands.Context):
|
||||||
@@ -74,27 +75,9 @@ class Warnings:
|
|||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await ctx.send("Would you like to enter commands to be run? (y/n)")
|
exceed_command = await get_command_for_exceeded_points(ctx)
|
||||||
|
drop_command = await get_command_for_dropping_points(ctx)
|
||||||
|
|
||||||
def same_author_check(m):
|
|
||||||
return m.author == ctx.author
|
|
||||||
|
|
||||||
try:
|
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await ctx.send(_("Ok then."))
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg.content.lower() == "y":
|
|
||||||
exceed_command = await get_command_for_exceeded_points(ctx)
|
|
||||||
if exceed_command is None:
|
|
||||||
return
|
|
||||||
drop_command = await get_command_for_dropping_points(ctx)
|
|
||||||
if drop_command is None:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
exceed_command = None
|
|
||||||
drop_command = None
|
|
||||||
to_add = {
|
to_add = {
|
||||||
"action_name": name,
|
"action_name": name,
|
||||||
"points": points,
|
"points": points,
|
||||||
@@ -114,7 +97,7 @@ class Warnings:
|
|||||||
# Sort in descending order by point count for ease in
|
# Sort in descending order by point count for ease in
|
||||||
# finding the highest possible action to take
|
# finding the highest possible action to take
|
||||||
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
||||||
await ctx.tick()
|
await ctx.send(_("Action {name} has been added.").format(name=name))
|
||||||
|
|
||||||
@warnaction.command(name="del")
|
@warnaction.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -134,7 +117,7 @@ class Warnings:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No action named {} exists!").format(action_name))
|
await ctx.send(_("No action named {} exists!").format(action_name))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnreason(self, ctx: commands.Context):
|
async def warnreason(self, ctx: commands.Context):
|
||||||
@@ -182,13 +165,20 @@ class Warnings:
|
|||||||
msg_list = []
|
msg_list = []
|
||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
for r, v in registered_reasons.items():
|
for r, v in registered_reasons.items():
|
||||||
msg_list.append(
|
if ctx.embed_requested():
|
||||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
em = discord.Embed(
|
||||||
r, v["points"], v["description"]
|
title=_("Reason: {name}").format(name=r), description=v["description"]
|
||||||
|
)
|
||||||
|
em.add_field(name=_("Points"), value=str(v["points"]))
|
||||||
|
msg_list.append(em)
|
||||||
|
else:
|
||||||
|
msg_list.append(
|
||||||
|
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||||
|
r, v["points"], v["description"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await ctx.send_interactive(msg_list)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no reasons configured!"))
|
await ctx.send(_("There are no reasons configured!"))
|
||||||
|
|
||||||
@@ -202,14 +192,21 @@ class Warnings:
|
|||||||
msg_list = []
|
msg_list = []
|
||||||
async with guild_settings.actions() as registered_actions:
|
async with guild_settings.actions() as registered_actions:
|
||||||
for r in registered_actions:
|
for r in registered_actions:
|
||||||
msg_list.append(
|
if await ctx.embed_requested():
|
||||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
em = discord.Embed(title=_("Action: {name}").format(name=r["action_name"]))
|
||||||
"Drop command: {}".format(
|
em.add_field(name=_("Points"), value="{}".format(r["points"]), inline=False)
|
||||||
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
em.add_field(name=_("Exceed command"), value=r["exceed_command"], inline=False)
|
||||||
|
em.add_field(name=_("Drop command"), value=r["drop_command"], inline=False)
|
||||||
|
msg_list.append(em)
|
||||||
|
else:
|
||||||
|
msg_list.append(
|
||||||
|
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||||
|
"Drop command: {}".format(
|
||||||
|
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await ctx.send_interactive(msg_list)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no actions configured!"))
|
await ctx.send(_("There are no actions configured!"))
|
||||||
|
|
||||||
@@ -221,6 +218,9 @@ class Warnings:
|
|||||||
|
|
||||||
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
||||||
"""
|
"""
|
||||||
|
if user == ctx.author:
|
||||||
|
await ctx.send(_("You cannot warn yourself."))
|
||||||
|
return
|
||||||
if reason.lower() == "custom":
|
if reason.lower() == "custom":
|
||||||
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
||||||
if not custom_allowed:
|
if not custom_allowed:
|
||||||
@@ -256,7 +256,27 @@ class Warnings:
|
|||||||
await member_settings.total_points.set(current_point_count)
|
await member_settings.total_points.set(current_point_count)
|
||||||
|
|
||||||
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
||||||
await ctx.tick()
|
try:
|
||||||
|
em = discord.Embed(
|
||||||
|
title=_("Warning from {mod_name}#{mod_discrim}").format(
|
||||||
|
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
|
||||||
|
),
|
||||||
|
description=reason_type["description"],
|
||||||
|
)
|
||||||
|
em.add_field(name=_("Points"), value=str(reason_type["points"]))
|
||||||
|
await user.send(
|
||||||
|
_("You have received a warning in {guild_name}.").format(
|
||||||
|
guild_name=ctx.guild.name
|
||||||
|
),
|
||||||
|
embed=em,
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await ctx.send(
|
||||||
|
_("User {user_name}#{user_discrim} has been warned.").format(
|
||||||
|
user_name=user.display_name, user_discrim=user.discriminator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -303,6 +323,9 @@ class Warnings:
|
|||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||||
"""Removes the specified warning from the user specified"""
|
"""Removes the specified warning from the user specified"""
|
||||||
|
if user_id == ctx.author.id:
|
||||||
|
await ctx.send(_("You cannot remove warnings from yourself."))
|
||||||
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
member = guild.get_member(user_id)
|
member = guild.get_member(user_id)
|
||||||
if member is None: # no longer in guild, but need a "member" object
|
if member is None: # no longer in guild, but need a "member" object
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ class VersionInfo:
|
|||||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.0.0b16"
|
__version__ = "3.0.0b17"
|
||||||
version_info = VersionInfo(3, 0, 0, "beta", 16)
|
version_info = VersionInfo(3, 0, 0, "beta", 17)
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ def parse_cli_flags(args):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enables the built-in RPC server. Please read the docs prior to enabling this!",
|
help="Enables the built-in RPC server. Please read the docs prior to enabling this!",
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--token", type=str, help="Run Red with the given token.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-instance",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Run Red without any existing instance. "
|
||||||
|
"The data will be saved under a temporary folder "
|
||||||
|
"and deleted on next system restart."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -137,11 +137,10 @@ class Group(Command, commands.Group):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.autohelp = kwargs.pop("autohelp", False)
|
self.autohelp = kwargs.pop("autohelp", True)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def invoke(self, ctx):
|
async def invoke(self, ctx):
|
||||||
|
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
previous = view.index
|
previous = view.index
|
||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
@@ -154,6 +153,7 @@ class Group(Command, commands.Group):
|
|||||||
|
|
||||||
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
||||||
if self.autohelp and not self.invoke_without_command:
|
if self.autohelp and not self.invoke_without_command:
|
||||||
|
await self._verify_checks(ctx)
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
await super().invoke(ctx)
|
await super().invoke(ctx)
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ class Context(commands.Context):
|
|||||||
else:
|
else:
|
||||||
return self.bot.color
|
return self.bot.color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embed_color(self):
|
||||||
|
# Rather than double awaiting.
|
||||||
|
return self.embed_colour
|
||||||
|
|
||||||
async def embed_requested(self):
|
async def embed_requested(self):
|
||||||
"""
|
"""
|
||||||
Simple helper to call bot.embed_requested
|
Simple helper to call bot.embed_requested
|
||||||
|
|||||||
@@ -29,12 +29,15 @@ class _ValueCtxManager:
|
|||||||
def __init__(self, value_obj, coro):
|
def __init__(self, value_obj, coro):
|
||||||
self.value_obj = value_obj
|
self.value_obj = value_obj
|
||||||
self.coro = coro
|
self.coro = coro
|
||||||
|
self.raw_value = None
|
||||||
|
self.__original_value = None
|
||||||
|
|
||||||
def __await__(self):
|
def __await__(self):
|
||||||
return self.coro.__await__()
|
return self.coro.__await__()
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self.raw_value = await self
|
self.raw_value = await self
|
||||||
|
self.__original_value = deepcopy(self.raw_value)
|
||||||
if not isinstance(self.raw_value, (list, dict)):
|
if not isinstance(self.raw_value, (list, dict)):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Type of retrieved value must be mutable (i.e. "
|
"Type of retrieved value must be mutable (i.e. "
|
||||||
@@ -44,7 +47,8 @@ class _ValueCtxManager:
|
|||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
async def __aexit__(self, *exc_info):
|
async def __aexit__(self, *exc_info):
|
||||||
await self.value_obj.set(self.raw_value)
|
if self.raw_value != self.__original_value:
|
||||||
|
await self.value_obj.set(self.raw_value)
|
||||||
|
|
||||||
|
|
||||||
class Value:
|
class Value:
|
||||||
@@ -335,7 +339,7 @@ class Group(Value):
|
|||||||
default = poss_default
|
default = poss_default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return deepcopy(await self.driver.get(*self.identifiers, *path))
|
return await self.driver.get(*self.identifiers, *path)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default is not ...:
|
if default is not ...:
|
||||||
return default
|
return default
|
||||||
@@ -365,7 +369,7 @@ class Group(Value):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not defaults:
|
if not defaults:
|
||||||
defaults = deepcopy(self.defaults)
|
defaults = self.defaults
|
||||||
|
|
||||||
for key, value in current.items():
|
for key, value in current.items():
|
||||||
if isinstance(value, collections.Mapping):
|
if isinstance(value, collections.Mapping):
|
||||||
@@ -392,7 +396,7 @@ class Group(Value):
|
|||||||
# is equivalent to
|
# is equivalent to
|
||||||
|
|
||||||
data = {"foo": {"bar": None}}
|
data = {"foo": {"bar": None}}
|
||||||
d["foo"]["bar"] = "baz"
|
data["foo"]["bar"] = "baz"
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ class Core(CoreLogic):
|
|||||||
"".format(red_repo, author_repo, org_repo, support_server_url)
|
"".format(red_repo, author_repo, org_repo, support_server_url)
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = discord.Embed(color=discord.Color.red())
|
embed = discord.Embed(color=(await ctx.embed_colour()))
|
||||||
embed.add_field(name="Instance owned by", value=str(owner))
|
embed.add_field(name="Instance owned by", value=str(owner))
|
||||||
embed.add_field(name="Python", value=python_version)
|
embed.add_field(name="Python", value=python_version)
|
||||||
embed.add_field(name="discord.py", value=dpy_version)
|
embed.add_field(name="discord.py", value=dpy_version)
|
||||||
@@ -321,7 +321,7 @@ class Core(CoreLogic):
|
|||||||
|
|
||||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
async def embedset(self, ctx: commands.Context):
|
async def embedset(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Commands for toggling embeds on or off.
|
Commands for toggling embeds on or off.
|
||||||
@@ -360,6 +360,7 @@ class Core(CoreLogic):
|
|||||||
|
|
||||||
@embedset.command(name="guild")
|
@embedset.command(name="guild")
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""
|
"""
|
||||||
Toggle the guild's embed setting.
|
Toggle the guild's embed setting.
|
||||||
@@ -598,7 +599,7 @@ class Core(CoreLogic):
|
|||||||
pass
|
pass
|
||||||
await ctx.bot.shutdown(restart=True)
|
await ctx.bot.shutdown(restart=True)
|
||||||
|
|
||||||
@commands.group(name="set", autohelp=True)
|
@commands.group(name="set")
|
||||||
async def _set(self, ctx):
|
async def _set(self, ctx):
|
||||||
"""Changes Red's settings"""
|
"""Changes Red's settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
@@ -698,7 +699,7 @@ class Core(CoreLogic):
|
|||||||
"""
|
"""
|
||||||
Sets a default colour to be used for the bot's embeds.
|
Sets a default colour to be used for the bot's embeds.
|
||||||
|
|
||||||
Acceptable values cor the colour parameter can be found at:
|
Acceptable values for the colour parameter can be found at:
|
||||||
|
|
||||||
http://discordpy.readthedocs.io/en/rewrite/ext/commands/api.html#discord.ext.commands.ColourConverter
|
http://discordpy.readthedocs.io/en/rewrite/ext/commands/api.html#discord.ext.commands.ColourConverter
|
||||||
"""
|
"""
|
||||||
@@ -981,7 +982,7 @@ class Core(CoreLogic):
|
|||||||
ctx.bot.disable_sentry()
|
ctx.bot.disable_sentry()
|
||||||
await ctx.send(_("Done. Sentry logging is now disabled."))
|
await ctx.send(_("Done. Sentry logging is now disabled."))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def helpset(self, ctx: commands.Context):
|
async def helpset(self, ctx: commands.Context):
|
||||||
"""Manage settings for the help command."""
|
"""Manage settings for the help command."""
|
||||||
@@ -1264,7 +1265,7 @@ class Core(CoreLogic):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def whitelist(self, ctx):
|
async def whitelist(self, ctx):
|
||||||
"""
|
"""
|
||||||
@@ -1322,7 +1323,7 @@ class Core(CoreLogic):
|
|||||||
await ctx.bot.db.whitelist.set([])
|
await ctx.bot.db.whitelist.set([])
|
||||||
await ctx.send(_("Whitelist has been cleared."))
|
await ctx.send(_("Whitelist has been cleared."))
|
||||||
|
|
||||||
@commands.group(autohelp=True)
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def blacklist(self, ctx):
|
async def blacklist(self, ctx):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import sys
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from copy import deepcopy
|
||||||
import hashlib
|
import hashlib
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from .json_io import JsonIO
|
from .json_io import JsonIO
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"create_temp_config",
|
||||||
"load_basic_configuration",
|
"load_basic_configuration",
|
||||||
"cog_data_path",
|
"cog_data_path",
|
||||||
"core_data_path",
|
"core_data_path",
|
||||||
@@ -39,6 +42,26 @@ if not config_dir:
|
|||||||
config_file = config_dir / "config.json"
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_config():
|
||||||
|
"""
|
||||||
|
Creates a default instance for Red, so it can be ran
|
||||||
|
without creating an instance.
|
||||||
|
|
||||||
|
.. warning:: The data of this instance will be removed
|
||||||
|
on next system restart.
|
||||||
|
"""
|
||||||
|
name = "temporary_red"
|
||||||
|
|
||||||
|
default_dirs = deepcopy(basic_config_default)
|
||||||
|
default_dirs["DATA_PATH"] = tempfile.mkdtemp()
|
||||||
|
default_dirs["STORAGE_TYPE"] = "JSON"
|
||||||
|
default_dirs["STORAGE_DETAILS"] = {}
|
||||||
|
|
||||||
|
config = JsonIO(config_file)._load_json()
|
||||||
|
config[name] = default_dirs
|
||||||
|
JsonIO(config_file)._save_json(config)
|
||||||
|
|
||||||
|
|
||||||
def load_basic_configuration(instance_name_: str):
|
def load_basic_configuration(instance_name_: str):
|
||||||
"""Loads the basic bootstrap configuration necessary for `Config`
|
"""Loads the basic bootstrap configuration necessary for `Config`
|
||||||
to know where to store or look for data.
|
to know where to store or look for data.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
import copy
|
||||||
import weakref
|
import weakref
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ class JSON(BaseDriver):
|
|||||||
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
||||||
for i in full_identifiers:
|
for i in full_identifiers:
|
||||||
partial = partial[i]
|
partial = partial[i]
|
||||||
return partial
|
return copy.deepcopy(partial)
|
||||||
|
|
||||||
async def set(self, *identifiers: str, value=None):
|
async def set(self, *identifiers: str, value=None):
|
||||||
partial = self.data
|
partial = self.data
|
||||||
@@ -107,7 +108,7 @@ class JSON(BaseDriver):
|
|||||||
partial[i] = {}
|
partial[i] = {}
|
||||||
partial = partial[i]
|
partial = partial[i]
|
||||||
|
|
||||||
partial[full_identifiers[-1]] = value
|
partial[full_identifiers[-1]] = copy.deepcopy(value)
|
||||||
await self.jsonIO._threadsafe_save_json(self.data)
|
await self.jsonIO._threadsafe_save_json(self.data)
|
||||||
|
|
||||||
async def clear(self, *identifiers: str):
|
async def clear(self, *identifiers: str):
|
||||||
|
|||||||
@@ -82,10 +82,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
if self.pm_check(self.context):
|
if self.pm_check(self.context):
|
||||||
return self.context.bot.color
|
return self.context.bot.color
|
||||||
else:
|
else:
|
||||||
if await self.context.bot.db.guild(self.context.guild).use_bot_color():
|
return await self.context.embed_colour()
|
||||||
return self.context.bot.color
|
|
||||||
else:
|
|
||||||
return self.me.color
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def destination(self):
|
def destination(self):
|
||||||
|
|||||||
53
redbot/core/utils/caching.py
Normal file
53
redbot/core/utils/caching.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
class LRUDict:
|
||||||
|
"""
|
||||||
|
dict with LRU-eviction and max-size
|
||||||
|
|
||||||
|
This is intended for caching, it may not behave how you want otherwise
|
||||||
|
|
||||||
|
This uses collections.OrderedDict under the hood, but does not directly expose
|
||||||
|
all of it's methods (intentional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *keyval_pairs, size):
|
||||||
|
self.size = size
|
||||||
|
self._dict = collections.OrderedDict(*keyval_pairs)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
if key in self._dict:
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
ret = self._dict.__getitem__(key)
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if key in self._dict:
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
self._dict[key] = value
|
||||||
|
if len(self._dict) > self.size:
|
||||||
|
self._dict.popitem(last=False)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
return self._dict.__delitem__(key)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
return self._dict.clear()
|
||||||
|
|
||||||
|
def pop(self, key):
|
||||||
|
return self._dict.pop(key)
|
||||||
|
|
||||||
|
# all of the below access all of the items, and therefore shouldnt modify the ordering for eviction
|
||||||
|
def keys(self):
|
||||||
|
return self._dict.keys()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._dict.items()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._dict.values()
|
||||||
@@ -135,16 +135,10 @@ async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Membe
|
|||||||
|
|
||||||
if isinstance(obj, discord.Role):
|
if isinstance(obj, discord.Role):
|
||||||
return obj.id in [admin_role_id, mod_role_id]
|
return obj.id in [admin_role_id, mod_role_id]
|
||||||
mod_roles = [r for r in server.roles if r.id == mod_role_id]
|
|
||||||
mod_role = mod_roles[0] if len(mod_roles) > 0 else None
|
|
||||||
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
|
||||||
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
|
||||||
|
|
||||||
if user and user == await bot.is_owner(user):
|
if await bot.is_owner(user):
|
||||||
return True
|
return True
|
||||||
elif admin_role and discord.utils.get(user.roles, name=admin_role):
|
elif discord.utils.find(lambda r: r.id in (admin_role_id, mod_role_id), user.roles):
|
||||||
return True
|
|
||||||
elif mod_role and discord.utils.get(user.roles, name=mod_role):
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -220,17 +214,14 @@ async def is_admin_or_superior(
|
|||||||
else:
|
else:
|
||||||
raise TypeError("Only messages, members or roles may be passed")
|
raise TypeError("Only messages, members or roles may be passed")
|
||||||
|
|
||||||
server = obj.guild
|
admin_role_id = await bot.db.guild(obj.guild).admin_role()
|
||||||
admin_role_id = await bot.db.guild(server).admin_role()
|
|
||||||
|
|
||||||
if isinstance(obj, discord.Role):
|
if isinstance(obj, discord.Role):
|
||||||
return obj.id == admin_role_id
|
return obj.id == admin_role_id
|
||||||
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
|
||||||
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
|
||||||
|
|
||||||
if user and await bot.is_owner(user):
|
if user and await bot.is_owner(user):
|
||||||
return True
|
return True
|
||||||
elif admin_roles and discord.utils.get(user.roles, name=admin_role):
|
elif discord.utils.get(user.roles, id=admin_role_id):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from distutils.version import StrictVersion
|
||||||
from redbot.setup import (
|
from redbot.setup import (
|
||||||
basic_setup,
|
basic_setup,
|
||||||
load_existing_config,
|
load_existing_config,
|
||||||
@@ -16,13 +18,14 @@ from redbot.setup import (
|
|||||||
create_backup,
|
create_backup,
|
||||||
save_config,
|
save_config,
|
||||||
)
|
)
|
||||||
|
from redbot.core import __version__
|
||||||
from redbot.core.utils import safe_delete
|
from redbot.core.utils import safe_delete
|
||||||
from redbot.core.cli import confirm
|
from redbot.core.cli import confirm
|
||||||
|
|
||||||
if sys.platform == "linux":
|
if sys.platform == "linux":
|
||||||
import distro
|
import distro
|
||||||
|
|
||||||
PYTHON_OK = sys.version_info >= (3, 5)
|
PYTHON_OK = sys.version_info >= (3, 6)
|
||||||
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
||||||
|
|
||||||
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
||||||
@@ -384,12 +387,27 @@ def debug_info():
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def is_outdated():
|
||||||
|
red_pypi = "https://pypi.python.org/pypi/Red-DiscordBot"
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get("{}/json".format(red_pypi)) as r:
|
||||||
|
data = await r.json()
|
||||||
|
new_version = data["info"]["version"]
|
||||||
|
return StrictVersion(new_version) > StrictVersion(__version__), new_version
|
||||||
|
|
||||||
|
|
||||||
def main_menu():
|
def main_menu():
|
||||||
if IS_WINDOWS:
|
if IS_WINDOWS:
|
||||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||||
clear_screen()
|
clear_screen()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
outdated, new_version = loop.run_until_complete(is_outdated())
|
||||||
while True:
|
while True:
|
||||||
print(INTRO)
|
print(INTRO)
|
||||||
|
print("\033[4mCurrent version:\033[0m {}".format(__version__))
|
||||||
|
if outdated:
|
||||||
|
print("Red is outdated. {} is available.".format(new_version))
|
||||||
|
print("")
|
||||||
print("1. Run Red w/ autorestart in case of issues")
|
print("1. Run Red w/ autorestart in case of issues")
|
||||||
print("2. Run Red")
|
print("2. Run Red")
|
||||||
print("3. Update Red")
|
print("3. Update Red")
|
||||||
@@ -418,13 +436,12 @@ def main_menu():
|
|||||||
basic_setup()
|
basic_setup()
|
||||||
wait()
|
wait()
|
||||||
elif choice == "5":
|
elif choice == "5":
|
||||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
loop.run_until_complete(remove_instance_interaction())
|
||||||
wait()
|
wait()
|
||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
debug_info()
|
debug_info()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
while True:
|
while True:
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print("==== Reinstall Red ====")
|
print("==== Reinstall Red ====")
|
||||||
print(
|
print(
|
||||||
@@ -455,7 +472,7 @@ def main_menu():
|
|||||||
def main():
|
def main():
|
||||||
if not PYTHON_OK:
|
if not PYTHON_OK:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Red requires Python 3.5 or greater. Please install the correct version!"
|
"Red requires Python 3.6 or greater. Please install the correct version!"
|
||||||
)
|
)
|
||||||
if args.debuginfo: # Check first since the function triggers an exit
|
if args.debuginfo: # Check first since the function triggers an exit
|
||||||
debug_info()
|
debug_info()
|
||||||
|
|||||||
1
redbot/pytest/__init__.py
Normal file
1
redbot/pytest/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import *
|
||||||
20
redbot/pytest/admin.py
Normal file
20
redbot/pytest/admin.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.admin import Admin
|
||||||
|
from redbot.cogs.admin.announcer import Announcer
|
||||||
|
|
||||||
|
__all__ = ["admin", "announcer"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def admin(config):
|
||||||
|
return Admin(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def announcer(admin):
|
||||||
|
a = Announcer(MagicMock(), "Some message", admin.conf)
|
||||||
|
yield a
|
||||||
|
a.cancel()
|
||||||
13
redbot/pytest/alias.py
Normal file
13
redbot/pytest/alias.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.alias import Alias
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
__all__ = ["alias"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def alias(config, monkeypatch):
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
return Alias(None)
|
||||||
13
redbot/pytest/cog_manager.py
Normal file
13
redbot/pytest/cog_manager.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["cog_mgr", "default_dir"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog_mgr(red):
|
||||||
|
return red.cog_mgr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def default_dir(red):
|
||||||
|
return red.main_dir
|
||||||
@@ -9,6 +9,26 @@ from redbot.core.bot import Red
|
|||||||
|
|
||||||
from redbot.core.drivers import red_json
|
from redbot.core.drivers import red_json
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"monkeysession",
|
||||||
|
"override_data_path",
|
||||||
|
"coroutine",
|
||||||
|
"json_driver",
|
||||||
|
"config",
|
||||||
|
"config_fr",
|
||||||
|
"red",
|
||||||
|
"guild_factory",
|
||||||
|
"empty_guild",
|
||||||
|
"empty_channel",
|
||||||
|
"empty_member",
|
||||||
|
"empty_message",
|
||||||
|
"empty_role",
|
||||||
|
"empty_user",
|
||||||
|
"member_factory",
|
||||||
|
"user_factory",
|
||||||
|
"ctx",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def monkeysession(request):
|
def monkeysession(request):
|
||||||
24
redbot/pytest/data_manager.py
Normal file
24
redbot/pytest/data_manager.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.core import data_manager
|
||||||
|
|
||||||
|
__all__ = ["cleanup_datamanager", "data_mgr_config", "cog_instance"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_datamanager():
|
||||||
|
data_manager.basic_config = None
|
||||||
|
data_manager.jsonio = None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def data_mgr_config(tmpdir):
|
||||||
|
default = data_manager.basic_config_default.copy()
|
||||||
|
default["BASE_DIR"] = str(tmpdir)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog_instance():
|
||||||
|
thing = type("CogTest", (object,), {})
|
||||||
|
return thing()
|
||||||
12
redbot/pytest/dataconverter.py
Normal file
12
redbot/pytest/dataconverter.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from redbot.cogs.dataconverter import core_specs
|
||||||
|
|
||||||
|
__all__ = ["get_specresolver"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_specresolver(path):
|
||||||
|
here = Path(path)
|
||||||
|
|
||||||
|
resolver = core_specs.SpecResolver(here.parent)
|
||||||
|
return resolver
|
||||||
103
redbot/pytest/downloader.py
Normal file
103
redbot/pytest/downloader.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.downloader.repo_manager import RepoManager, Repo
|
||||||
|
from redbot.cogs.downloader.installable import Installable
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"patch_relative_to",
|
||||||
|
"repo_manager",
|
||||||
|
"repo",
|
||||||
|
"repo_norun",
|
||||||
|
"bot_repo",
|
||||||
|
"INFO_JSON",
|
||||||
|
"installable",
|
||||||
|
"fake_run_noprint",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_run(*args, **kwargs):
|
||||||
|
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
||||||
|
res = fake_result_tuple(0, (args, kwargs))
|
||||||
|
print(args[0])
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_run_noprint(*args, **kwargs):
|
||||||
|
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
||||||
|
res = fake_result_tuple(0, (args, kwargs))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def patch_relative_to(monkeysession):
|
||||||
|
def fake_relative_to(self, some_path: Path):
|
||||||
|
return self
|
||||||
|
|
||||||
|
monkeysession.setattr("pathlib.Path.relative_to", fake_relative_to)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo_manager(tmpdir_factory):
|
||||||
|
rm = RepoManager()
|
||||||
|
# rm.repos_folder = Path(str(tmpdir_factory.getbasetemp())) / 'repos'
|
||||||
|
return rm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(tmpdir):
|
||||||
|
repo_folder = Path(str(tmpdir)) / "repos" / "squid"
|
||||||
|
repo_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return Repo(
|
||||||
|
url="https://github.com/tekulvw/Squid-Plugins",
|
||||||
|
name="squid",
|
||||||
|
branch="rewrite_cogs",
|
||||||
|
folder_path=repo_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo_norun(repo):
|
||||||
|
repo._run = fake_run
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bot_repo(event_loop):
|
||||||
|
cwd = Path.cwd()
|
||||||
|
return Repo(
|
||||||
|
name="Red-DiscordBot",
|
||||||
|
branch="WRONG",
|
||||||
|
url="https://empty.com/something.git",
|
||||||
|
folder_path=cwd,
|
||||||
|
loop=event_loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Installable
|
||||||
|
INFO_JSON = {
|
||||||
|
"author": ("tekulvw",),
|
||||||
|
"bot_version": (3, 0, 0),
|
||||||
|
"description": "A long description",
|
||||||
|
"hidden": False,
|
||||||
|
"install_msg": "A post-installation message",
|
||||||
|
"required_cogs": {},
|
||||||
|
"requirements": ("tabulate"),
|
||||||
|
"short": "A short description",
|
||||||
|
"tags": ("tag1", "tag2"),
|
||||||
|
"type": "COG",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def installable(tmpdir):
|
||||||
|
cog_path = tmpdir.mkdir("test_repo").mkdir("test_cog")
|
||||||
|
info_path = cog_path.join("info.json")
|
||||||
|
info_path.write_text(json.dumps(INFO_JSON), "utf-8")
|
||||||
|
|
||||||
|
cog_info = Installable(Path(str(cog_path)))
|
||||||
|
return cog_info
|
||||||
15
redbot/pytest/economy.py
Normal file
15
redbot/pytest/economy.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["bank"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def bank(config, monkeypatch):
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
from redbot.core import bank
|
||||||
|
|
||||||
|
bank._register_defaults()
|
||||||
|
return bank
|
||||||
15
redbot/pytest/mod.py
Normal file
15
redbot/pytest/mod.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["mod"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mod(config, monkeypatch):
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
from redbot.core import modlog
|
||||||
|
|
||||||
|
modlog._register_defaults()
|
||||||
|
return modlog
|
||||||
51
redbot/pytest/rpc.py
Normal file
51
redbot/pytest/rpc.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
from redbot.core.rpc import RPC, RPCMixin
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
__all__ = ["rpc", "rpcmixin", "cog", "existing_func", "existing_multi_func"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def rpc():
|
||||||
|
return RPC()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def rpcmixin():
|
||||||
|
r = RPCMixin()
|
||||||
|
r.rpc = MagicMock(spec=RPC)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog():
|
||||||
|
class Cog:
|
||||||
|
async def cofunc(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cofunc2(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cofunc3(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Cog()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def existing_func(rpc, cog):
|
||||||
|
rpc.add_method(cog.cofunc)
|
||||||
|
|
||||||
|
return cog.cofunc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def existing_multi_func(rpc, cog):
|
||||||
|
funcs = [cog.cofunc, cog.cofunc2, cog.cofunc3]
|
||||||
|
rpc.add_multi_method(*funcs)
|
||||||
|
|
||||||
|
return funcs
|
||||||
7
setup.py
7
setup.py
@@ -34,7 +34,7 @@ def check_compiler_available():
|
|||||||
tfile.write(b"int main(int argc, char** argv) {return 0;}")
|
tfile.write(b"int main(int argc, char** argv) {return 0;}")
|
||||||
tfile.seek(0)
|
tfile.seek(0)
|
||||||
try:
|
try:
|
||||||
m.compile([tfile.name])
|
m.compile([tfile.name], output_dir=tdir)
|
||||||
except (CCompilerError, DistutilsPlatformError):
|
except (CCompilerError, DistutilsPlatformError):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -111,10 +111,10 @@ setup(
|
|||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Framework :: AsyncIO",
|
"Framework :: AsyncIO",
|
||||||
|
"Framework :: Pytest",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Documentation :: Sphinx",
|
"Topic :: Documentation :: Sphinx",
|
||||||
@@ -124,7 +124,8 @@ setup(
|
|||||||
"redbot=redbot.__main__:main",
|
"redbot=redbot.__main__:main",
|
||||||
"redbot-setup=redbot.setup:main",
|
"redbot-setup=redbot.setup:main",
|
||||||
"redbot-launcher=redbot.launcher:main",
|
"redbot-launcher=redbot.launcher:main",
|
||||||
]
|
],
|
||||||
|
"pytest11": ["red-discordbot = redbot.pytest"],
|
||||||
},
|
},
|
||||||
python_requires=">=3.6,<3.7",
|
python_requires=">=3.6,<3.7",
|
||||||
setup_requires=get_requirements(),
|
setup_requires=get_requirements(),
|
||||||
|
|||||||
@@ -2,20 +2,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.admin import Admin
|
from redbot.pytest.admin import *
|
||||||
from redbot.cogs.admin.announcer import Announcer
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def admin(config):
|
|
||||||
return Admin(config)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def announcer(admin):
|
|
||||||
a = Announcer(MagicMock(), "Some message", admin.conf)
|
|
||||||
yield a
|
|
||||||
a.cancel()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from redbot.cogs.dataconverter import core_specs
|
from redbot.pytest.dataconverter import *
|
||||||
from redbot.core.utils.data_converter import DataConverter
|
from redbot.core.utils.data_converter import DataConverter
|
||||||
|
|
||||||
|
|
||||||
@@ -14,16 +13,9 @@ def mock_dpy_member(guildid, userid):
|
|||||||
return namedtuple("Member", "id guild")(int(userid), mock_dpy_object(guildid))
|
return namedtuple("Member", "id guild")(int(userid), mock_dpy_object(guildid))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def specresolver():
|
|
||||||
here = Path(__file__)
|
|
||||||
|
|
||||||
resolver = core_specs.SpecResolver(here.parent)
|
|
||||||
return resolver
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_mod_nicknames(red, specresolver: core_specs.SpecResolver):
|
async def test_mod_nicknames(red):
|
||||||
|
specresolver = get_specresolver(__file__)
|
||||||
filepath, converter, cogname, attr, _id = specresolver.get_conversion_info("Past Nicknames")
|
filepath, converter, cogname, attr, _id = specresolver.get_conversion_info("Past Nicknames")
|
||||||
conf = specresolver.get_config_object(red, cogname, attr, _id)
|
conf = specresolver.get_config_object(red, cogname, attr, _id)
|
||||||
|
|
||||||
|
|||||||
@@ -6,69 +6,12 @@ import pytest
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from raven.versioning import fetch_git_sha
|
from raven.versioning import fetch_git_sha
|
||||||
|
|
||||||
|
from redbot.pytest.downloader import *
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import RepoManager, Repo
|
from redbot.cogs.downloader.repo_manager import RepoManager, Repo
|
||||||
from redbot.cogs.downloader.errors import ExistingGitRepo
|
from redbot.cogs.downloader.errors import ExistingGitRepo
|
||||||
|
|
||||||
|
|
||||||
async def fake_run(*args, **kwargs):
|
|
||||||
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
|
||||||
res = fake_result_tuple(0, (args, kwargs))
|
|
||||||
print(args[0])
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
async def fake_run_noprint(*args, **kwargs):
|
|
||||||
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
|
||||||
res = fake_result_tuple(0, (args, kwargs))
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
|
||||||
def patch_relative_to(monkeysession):
|
|
||||||
def fake_relative_to(self, some_path: Path):
|
|
||||||
return self
|
|
||||||
|
|
||||||
monkeysession.setattr("pathlib.Path.relative_to", fake_relative_to)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def repo_manager(tmpdir_factory):
|
|
||||||
rm = RepoManager()
|
|
||||||
# rm.repos_folder = Path(str(tmpdir_factory.getbasetemp())) / 'repos'
|
|
||||||
return rm
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def repo(tmpdir):
|
|
||||||
repo_folder = Path(str(tmpdir)) / "repos" / "squid"
|
|
||||||
repo_folder.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
return Repo(
|
|
||||||
url="https://github.com/tekulvw/Squid-Plugins",
|
|
||||||
name="squid",
|
|
||||||
branch="rewrite_cogs",
|
|
||||||
folder_path=repo_folder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def repo_norun(repo):
|
|
||||||
repo._run = fake_run
|
|
||||||
return repo
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def bot_repo(event_loop):
|
|
||||||
cwd = Path.cwd()
|
|
||||||
return Repo(
|
|
||||||
name="Red-DiscordBot",
|
|
||||||
branch="WRONG",
|
|
||||||
url="https://empty.com/something.git",
|
|
||||||
folder_path=cwd,
|
|
||||||
loop=event_loop,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_existing_git_repo(tmpdir):
|
def test_existing_git_repo(tmpdir):
|
||||||
repo_folder = Path(str(tmpdir)) / "repos" / "squid" / ".git"
|
repo_folder = Path(str(tmpdir)) / "repos" / "squid" / ".git"
|
||||||
repo_folder.mkdir(parents=True, exist_ok=True)
|
repo_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -3,31 +3,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from redbot.pytest.downloader import *
|
||||||
from redbot.cogs.downloader.installable import Installable, InstallableType
|
from redbot.cogs.downloader.installable import Installable, InstallableType
|
||||||
|
|
||||||
INFO_JSON = {
|
|
||||||
"author": ("tekulvw",),
|
|
||||||
"bot_version": (3, 0, 0),
|
|
||||||
"description": "A long description",
|
|
||||||
"hidden": False,
|
|
||||||
"install_msg": "A post-installation message",
|
|
||||||
"required_cogs": {},
|
|
||||||
"requirements": ("tabulate"),
|
|
||||||
"short": "A short description",
|
|
||||||
"tags": ("tag1", "tag2"),
|
|
||||||
"type": "COG",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def installable(tmpdir):
|
|
||||||
cog_path = tmpdir.mkdir("test_repo").mkdir("test_cog")
|
|
||||||
info_path = cog_path.join("info.json")
|
|
||||||
info_path.write_text(json.dumps(INFO_JSON), "utf-8")
|
|
||||||
|
|
||||||
cog_info = Installable(Path(str(cog_path)))
|
|
||||||
return cog_info
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_info_file(installable):
|
def test_process_info_file(installable):
|
||||||
for k, v in INFO_JSON.items():
|
for k, v in INFO_JSON.items():
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from redbot.pytest.alias import *
|
||||||
from redbot.cogs.alias import Alias
|
|
||||||
from redbot.core import Config
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def alias(config, monkeypatch):
|
|
||||||
with monkeypatch.context() as m:
|
|
||||||
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
|
||||||
return Alias(None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_valid_alias_name(alias):
|
def test_is_valid_alias_name(alias):
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from redbot.pytest.economy import *
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def bank(config, monkeypatch):
|
|
||||||
from redbot.core import Config
|
|
||||||
|
|
||||||
with monkeypatch.context() as m:
|
|
||||||
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
|
||||||
from redbot.core import bank
|
|
||||||
|
|
||||||
bank._register_defaults()
|
|
||||||
return bank
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from redbot.pytest.mod import *
|
||||||
@pytest.fixture
|
|
||||||
def mod(config, monkeypatch):
|
|
||||||
from redbot.core import Config
|
|
||||||
|
|
||||||
with monkeypatch.context() as m:
|
|
||||||
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
|
||||||
from redbot.core import modlog
|
|
||||||
|
|
||||||
modlog._register_defaults()
|
|
||||||
return modlog
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -2,19 +2,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from redbot.pytest.cog_manager import *
|
||||||
from redbot.core import cog_manager
|
from redbot.core import cog_manager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def cog_mgr(red):
|
|
||||||
return red.cog_mgr
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def default_dir(red):
|
|
||||||
return red.main_dir
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip
|
@pytest.mark.skip
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_ensure_cogs_in_paths(cog_mgr, default_dir):
|
async def test_ensure_cogs_in_paths(cog_mgr, default_dir):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@@ -379,13 +380,9 @@ async def test_value_ctxmgr_saves(config):
|
|||||||
async def test_value_ctxmgr_immutable(config):
|
async def test_value_ctxmgr_immutable(config):
|
||||||
config.register_global(foo=True)
|
config.register_global(foo=True)
|
||||||
|
|
||||||
try:
|
with pytest.raises(TypeError):
|
||||||
async with config.foo() as foo:
|
async with config.foo() as foo:
|
||||||
foo = False
|
foo = False
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise AssertionError
|
|
||||||
|
|
||||||
foo = await config.foo()
|
foo = await config.foo()
|
||||||
assert foo is True
|
assert foo is True
|
||||||
@@ -401,3 +398,35 @@ async def test_ctxmgr_no_shared_default(config, member_factory):
|
|||||||
foo.append(1)
|
foo.append(1)
|
||||||
|
|
||||||
assert 1 not in await config.member(m2).foo()
|
assert 1 not in await config.member(m2).foo()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ctxmgr_no_unnecessary_write(config):
|
||||||
|
config.register_global(foo=[])
|
||||||
|
foo_value_obj = config.foo
|
||||||
|
with patch.object(foo_value_obj, "set") as set_method:
|
||||||
|
async with foo_value_obj() as foo:
|
||||||
|
pass
|
||||||
|
set_method.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_then_mutate(config):
|
||||||
|
"""Tests that mutating an object after getting it as a value doesn't mutate the data store."""
|
||||||
|
config.register_global(list1=[])
|
||||||
|
await config.list1.set([])
|
||||||
|
list1 = await config.list1()
|
||||||
|
list1.append("foo")
|
||||||
|
list1 = await config.list1()
|
||||||
|
assert "foo" not in list1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_set_then_mutate(config):
|
||||||
|
"""Tests that mutating an object after setting it as a value doesn't mutate the data store."""
|
||||||
|
config.register_global(list1=[])
|
||||||
|
list1 = []
|
||||||
|
await config.list1.set(list1)
|
||||||
|
list1.append("foo")
|
||||||
|
list1 = await config.list1()
|
||||||
|
assert "foo" not in list1
|
||||||
|
|||||||
@@ -3,28 +3,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from redbot.pytest.data_manager import *
|
||||||
from redbot.core import data_manager
|
from redbot.core import data_manager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def cleanup_datamanager():
|
|
||||||
data_manager.basic_config = None
|
|
||||||
data_manager.jsonio = None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def data_mgr_config(tmpdir):
|
|
||||||
default = data_manager.basic_config_default.copy()
|
|
||||||
default["BASE_DIR"] = str(tmpdir)
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def cog_instance():
|
|
||||||
thing = type("CogTest", (object,), {})
|
|
||||||
return thing()
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_basic(cog_instance):
|
def test_no_basic(cog_instance):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
data_manager.core_data_path()
|
data_manager.core_data_path()
|
||||||
|
|||||||
@@ -1,52 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from redbot.core.rpc import RPC, RPCMixin, get_name
|
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
from redbot.pytest.rpc import *
|
||||||
|
from redbot.core.rpc import get_name
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def rpc():
|
|
||||||
return RPC()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def rpcmixin():
|
|
||||||
r = RPCMixin()
|
|
||||||
r.rpc = MagicMock(spec=RPC)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def cog():
|
|
||||||
class Cog:
|
|
||||||
async def cofunc(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def cofunc2(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def cofunc3(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def func(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return Cog()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def existing_func(rpc, cog):
|
|
||||||
rpc.add_method(cog.cofunc)
|
|
||||||
|
|
||||||
return cog.cofunc
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def existing_multi_func(rpc, cog):
|
|
||||||
funcs = [cog.cofunc, cog.cofunc2, cog.cofunc3]
|
|
||||||
rpc.add_multi_method(*funcs)
|
|
||||||
|
|
||||||
return funcs
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_name(cog):
|
def test_get_name(cog):
|
||||||
|
|||||||
Reference in New Issue
Block a user