mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -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/antispam.py @mikeshardmind
|
||||
redbot/core/utils/tunnel.py @mikeshardmind
|
||||
redbot/core/utils/caching.py @mikeshardmind
|
||||
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
@@ -44,6 +45,7 @@ redbot/cogs/trivia/* @Tobotimus
|
||||
redbot/cogs/dataconverter/* @mikeshardmind
|
||||
redbot/cogs/reports/* @mikeshardmind
|
||||
redbot/cogs/permissions/* @mikeshardmind
|
||||
redbot/cogs/warnings/* @palmtree5
|
||||
|
||||
# Docs
|
||||
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
|
||||
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.
|
||||
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:
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
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 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>`.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
16
README.rst
16
README.rst
@@ -11,9 +11,21 @@
|
||||
|
||||
.. 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
|
||||
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
||||
: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
|
||||
:target: https://github.com/ambv/black
|
||||
@@ -26,6 +38,10 @@
|
||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
||||
:target: https://www.patreon.com/Red_Devs
|
||||
:alt: Patreon
|
||||
|
||||
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
:target: http://makeapullrequest.com
|
||||
:alt: PRs open
|
||||
|
||||
==========
|
||||
Overview
|
||||
|
||||
@@ -89,8 +89,10 @@ Locking Audio to specific voice channel(s) as a serverowner or admin:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions setguilddefault Audio deny
|
||||
[p]permissions addguildrule allow Audio [voice channel ID or name]
|
||||
[p]permissions setguilddefault deny play
|
||||
[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
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ Keys common to both repo and cog info.json (case sensitive)
|
||||
|
||||
- ``install_msg`` (string) - The message that gets displayed when a cog
|
||||
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
|
||||
is displayed when a user executes ``!cog list``
|
||||
|
||||
@@ -17,11 +17,10 @@ you in the process.
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
||||
are on Windows, stick with 3.5). Open a terminal or command prompt and type
|
||||
To start off, be sure that you have installed Python 3.6 or higher. 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]`
|
||||
(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.
|
||||
|
||||
--------------------
|
||||
Setting up a package
|
||||
|
||||
@@ -12,12 +12,3 @@ if discord.version_info.major < 1:
|
||||
" >= 1.0.0."
|
||||
)
|
||||
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
|
||||
from redbot.core.bot import Red, ExitCodes
|
||||
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.global_checks import init_global_checks
|
||||
from redbot.core.events import init_events
|
||||
@@ -106,9 +106,17 @@ def main():
|
||||
elif cli_flags.version:
|
||||
print(description)
|
||||
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!")
|
||||
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)
|
||||
log, sentry_log = init_loggers(cli_flags)
|
||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||
@@ -122,6 +130,8 @@ def main():
|
||||
tmp_data = {}
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
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"]
|
||||
if not (token and prefix):
|
||||
if cli_flags.no_prompt is False:
|
||||
|
||||
@@ -127,8 +127,8 @@ class Admin:
|
||||
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
|
||||
author of the command.
|
||||
Adds a role to a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -145,8 +145,8 @@ class Admin:
|
||||
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
|
||||
author of the command.
|
||||
Removes a role from a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -156,7 +156,7 @@ class Admin:
|
||||
else:
|
||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def editrole(self, ctx: commands.Context):
|
||||
@@ -291,11 +291,11 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
return valid_roles
|
||||
|
||||
@commands.guild_only()
|
||||
@commands.group(invoke_without_command=True)
|
||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Add a role to yourself that server admins have configured as
|
||||
user settable.
|
||||
Add a role to yourself that server admins have configured as user settable.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
@@ -313,7 +313,7 @@ class Admin:
|
||||
await self._removerole(ctx, ctx.author, selfrole)
|
||||
|
||||
@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):
|
||||
"""
|
||||
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.")
|
||||
|
||||
@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):
|
||||
"""
|
||||
Removes a role from the list of available selfroles.
|
||||
|
||||
@@ -170,13 +170,13 @@ class Alias:
|
||||
new_message.content = "{}{} {}".format(prefix, alias.command, args)
|
||||
await self.bot.process_commands(new_message)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
async def alias(self, ctx: commands.Context):
|
||||
"""Manage per-server aliases for commands"""
|
||||
pass
|
||||
|
||||
@alias.group(name="global", autohelp=True)
|
||||
@alias.group(name="global")
|
||||
async def global_(self, ctx: commands.Context):
|
||||
"""
|
||||
Manage global aliases.
|
||||
|
||||
@@ -15,7 +15,7 @@ from .manager import shutdown_lavalink_server
|
||||
|
||||
_ = Translator("Audio", __file__)
|
||||
|
||||
__version__ = "0.0.6b"
|
||||
__version__ = "0.0.6c"
|
||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ class Audio:
|
||||
await message_channel.send(embed=embed)
|
||||
await player.skip()
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
async def audioset(self, ctx):
|
||||
"""Music configuration options."""
|
||||
@@ -586,6 +586,12 @@ class Audio:
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -657,7 +663,7 @@ class Audio:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
async def playlist(self, ctx):
|
||||
"""Playlist configuration options."""
|
||||
@@ -708,8 +714,10 @@ class Audio:
|
||||
return await self._embed_msg(
|
||||
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)
|
||||
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))
|
||||
|
||||
@playlist.command(name="delete")
|
||||
@@ -768,6 +776,7 @@ class Audio:
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.cooldown(1, 15, discord.ext.commands.BucketType.guild)
|
||||
@playlist.command(name="queue")
|
||||
async def _playlist_queue(self, ctx, playlist_name=None):
|
||||
"""Save the queue to a playlist."""
|
||||
@@ -794,11 +803,11 @@ class Audio:
|
||||
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
||||
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
return m.author == ctx.author and not m.content.startswith(ctx.prefix)
|
||||
|
||||
try:
|
||||
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:
|
||||
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
||||
if playlist_name in playlists:
|
||||
@@ -809,11 +818,12 @@ class Audio:
|
||||
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
||||
playlist_list = self._to_json(ctx, None, tracklist)
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(
|
||||
ctx,
|
||||
"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)
|
||||
if tracklist is not None:
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||
playlists[playlist_name] = playlist_list
|
||||
return await self._embed_msg(
|
||||
ctx,
|
||||
@@ -919,8 +930,11 @@ class Audio:
|
||||
file_suffix = file_url.rsplit(".", 1)[1]
|
||||
if file_suffix != "txt":
|
||||
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
||||
async with self.session.request("GET", file_url) as r:
|
||||
v2_playlist = await r.json(content_type="text/plain")
|
||||
try:
|
||||
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:
|
||||
v2_playlist_url = v2_playlist["link"]
|
||||
except KeyError:
|
||||
@@ -989,6 +1003,12 @@ class Audio:
|
||||
return False
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -1206,6 +1226,12 @@ class Audio:
|
||||
"""
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -1618,7 +1644,7 @@ class Audio:
|
||||
embed.set_footer(text="Nothing playing.")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(aliases=["llset"], autohelp=True)
|
||||
@commands.group(aliases=["llset"])
|
||||
@commands.guild_only()
|
||||
@checks.is_owner()
|
||||
async def llsetup(self, ctx):
|
||||
@@ -1750,7 +1776,7 @@ class Audio:
|
||||
if server.id not in stop_times:
|
||||
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:
|
||||
stop_times[server.id] = int(time.time())
|
||||
|
||||
@@ -1883,6 +1909,15 @@ class Audio:
|
||||
track_obj[key] = value
|
||||
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):
|
||||
if after.channel != before.channel:
|
||||
try:
|
||||
|
||||
@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -36,15 +38,17 @@ def check_global_setting_admin():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
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
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -58,8 +62,9 @@ class Bank:
|
||||
|
||||
# SECTION commands
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@check_global_setting_guildowner()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.group(autohelp=True)
|
||||
async def bankset(self, ctx: commands.Context):
|
||||
"""Base command for bank settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
|
||||
@@ -71,11 +71,11 @@ class Cleanup:
|
||||
to_delete = []
|
||||
too_old = False
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
while not too_old and len(to_delete) < number:
|
||||
message = None
|
||||
async for message in channel.history(limit=limit, before=before, after=after):
|
||||
if (
|
||||
(not number or len(to_delete) - 1 < number)
|
||||
(not number or len(to_delete) < number)
|
||||
and check(message)
|
||||
and (ctx.message.created_at - message.created_at).days < 14
|
||||
and (delete_pinned or not message.pinned)
|
||||
@@ -92,7 +92,7 @@ class Cleanup:
|
||||
before = message
|
||||
return to_delete
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: commands.Context):
|
||||
"""Deletes messages."""
|
||||
@@ -100,7 +100,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def text(
|
||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
@@ -112,6 +111,10 @@ class Cleanup:
|
||||
Remember to use double quotes."""
|
||||
|
||||
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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -150,7 +153,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(
|
||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
@@ -159,6 +161,10 @@ class Cleanup:
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
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
|
||||
try:
|
||||
@@ -171,7 +177,6 @@ class Cleanup:
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -212,7 +217,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||
"""Deletes all messages after specified message.
|
||||
|
||||
@@ -224,6 +228,9 @@ class Cleanup:
|
||||
"""
|
||||
|
||||
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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -250,7 +257,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Deletes last X messages.
|
||||
|
||||
@@ -258,6 +264,9 @@ class Cleanup:
|
||||
cleanup messages 26"""
|
||||
|
||||
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
|
||||
|
||||
is_bot = self.bot.user.bot
|
||||
@@ -284,11 +293,13 @@ class Cleanup:
|
||||
|
||||
@cleanup.command(name="bot")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -412,7 +423,7 @@ class Cleanup:
|
||||
if author == self.bot.user:
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
if channel.name:
|
||||
if ctx.guild:
|
||||
channel_name = "channel " + channel.name
|
||||
else:
|
||||
channel_name = str(channel)
|
||||
|
||||
@@ -141,13 +141,13 @@ class CustomCommands:
|
||||
self.config.register_guild(commands={})
|
||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||
|
||||
@commands.group(aliases=["cc"], autohelp=True)
|
||||
@commands.group(aliases=["cc"])
|
||||
@commands.guild_only()
|
||||
async def customcom(self, ctx: commands.Context):
|
||||
"""Custom commands management"""
|
||||
pass
|
||||
|
||||
@customcom.group(name="add", autohelp=True)
|
||||
@customcom.group(name="add")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add(self, ctx: commands.Context):
|
||||
"""
|
||||
|
||||
@@ -204,7 +204,7 @@ class Downloader:
|
||||
)
|
||||
)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def repo(self, ctx):
|
||||
"""
|
||||
@@ -234,7 +234,7 @@ class Downloader:
|
||||
else:
|
||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||
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")
|
||||
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 "")
|
||||
await ctx.send(box(msg))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def cog(self, ctx):
|
||||
"""
|
||||
@@ -319,7 +319,7 @@ class Downloader:
|
||||
|
||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||
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")
|
||||
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.core import Config, bank, commands
|
||||
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
|
||||
|
||||
@@ -137,7 +138,8 @@ class Economy:
|
||||
self.config.register_role(**self.default_role_settings)
|
||||
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):
|
||||
"""Bank operations"""
|
||||
pass
|
||||
@@ -209,7 +211,6 @@ class Economy:
|
||||
)
|
||||
|
||||
@_bank.command()
|
||||
@guild_only_check()
|
||||
@check_global_setting_guildowner()
|
||||
async def reset(self, ctx, confirmation: bool = False):
|
||||
"""Deletes bank accounts"""
|
||||
@@ -230,8 +231,8 @@ class Economy:
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
@commands.command()
|
||||
async def payday(self, ctx: commands.Context):
|
||||
"""Get some free currency"""
|
||||
author = ctx.author
|
||||
@@ -251,7 +252,7 @@ class Economy:
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {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(
|
||||
author,
|
||||
credits_name,
|
||||
@@ -309,8 +310,8 @@ class Economy:
|
||||
"""Prints out the leaderboard
|
||||
|
||||
Defaults to top 10"""
|
||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
if top < 1:
|
||||
top = 10
|
||||
if (
|
||||
@@ -320,25 +321,25 @@ class Economy:
|
||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
highscore = ""
|
||||
for pos, acc in enumerate(bank_sorted, 1):
|
||||
pos = pos
|
||||
poswidth = 2
|
||||
name = acc[1]["name"]
|
||||
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,
|
||||
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
||||
highscores = [
|
||||
(
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
if acc[0] != author.id
|
||||
else (
|
||||
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:
|
||||
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()
|
||||
@check_global_setting_admin()
|
||||
async def economyset(self, ctx: commands.Context):
|
||||
|
||||
@@ -39,7 +39,7 @@ class Filter:
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@commands.group(name="filter", autohelp=True)
|
||||
@commands.group(name="filter")
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def _filter(self, ctx: commands.Context):
|
||||
|
||||
@@ -26,7 +26,7 @@ class Image:
|
||||
def __unload(self):
|
||||
self.session.close()
|
||||
|
||||
@commands.group(name="imgur", autohelp=True)
|
||||
@commands.group(name="imgur")
|
||||
async def _imgur(self, ctx):
|
||||
"""Retrieves pictures from imgur
|
||||
|
||||
|
||||
@@ -161,13 +161,13 @@ class Mod:
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modset(self, ctx: commands.Context):
|
||||
"""Manages server administration settings."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
|
||||
guild = ctx.guild
|
||||
# Display current settings
|
||||
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
||||
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
||||
@@ -628,7 +628,6 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@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):
|
||||
"""Unbans the target user.
|
||||
|
||||
@@ -636,13 +635,17 @@ class Mod:
|
||||
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-
|
||||
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
|
||||
author = ctx.author
|
||||
user = await self.bot.get_user_info(user_id)
|
||||
if not user:
|
||||
await ctx.send(_("Couldn't find a user with that ID!"))
|
||||
return
|
||||
reason = get_audit_reason(ctx.author, reason)
|
||||
audit_reason = get_audit_reason(ctx.author, reason)
|
||||
bans = await guild.bans()
|
||||
bans = [be.user for be in bans]
|
||||
if user not in bans:
|
||||
@@ -651,7 +654,7 @@ class Mod:
|
||||
queue_entry = (guild.id, user.id)
|
||||
self.unban_queue.append(queue_entry)
|
||||
try:
|
||||
await guild.unban(user, reason=reason)
|
||||
await guild.unban(user, reason=audit_reason)
|
||||
except discord.HTTPException:
|
||||
self.unban_queue.remove(queue_entry)
|
||||
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")
|
||||
)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
async def mute(self, ctx: commands.Context):
|
||||
@@ -998,7 +1001,7 @@ class Mod:
|
||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||
return True, None
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
async def unmute(self, ctx: commands.Context):
|
||||
@@ -1164,13 +1167,12 @@ class Mod:
|
||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||
return True, None
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
async def ignore(self, ctx: commands.Context):
|
||||
"""Adds servers/channels to ignorelist"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
await ctx.send(await self.count_ignored())
|
||||
|
||||
@ignore.command(name="channel")
|
||||
@@ -1187,7 +1189,7 @@ class Mod:
|
||||
await ctx.send(_("Channel already in ignore list."))
|
||||
|
||||
@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):
|
||||
"""Ignores current server"""
|
||||
guild = ctx.guild
|
||||
@@ -1197,7 +1199,7 @@ class Mod:
|
||||
else:
|
||||
await ctx.send(_("This server is already being ignored."))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
async def unignore(self, ctx: commands.Context):
|
||||
@@ -1220,7 +1222,7 @@ class Mod:
|
||||
await ctx.send(_("That channel is not in the ignore list."))
|
||||
|
||||
@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):
|
||||
"""Removes current guild from ignore list"""
|
||||
guild = ctx.message.guild
|
||||
@@ -1362,9 +1364,9 @@ class Mod:
|
||||
names = await self.settings.user(user).past_names()
|
||||
nicks = await self.settings.member(user).past_nicks()
|
||||
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:
|
||||
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
|
||||
|
||||
async def check_tempban_expirations(self):
|
||||
@@ -1602,18 +1604,22 @@ class Mod:
|
||||
if entry.target == target:
|
||||
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:
|
||||
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
|
||||
name_list.remove(after.nick)
|
||||
name_list.append(after.nick)
|
||||
name_list.remove(after.name)
|
||||
name_list.append(after.name)
|
||||
while len(name_list) > 20:
|
||||
name_list.pop(0)
|
||||
|
||||
if before.nick != after.nick and after.nick is not None:
|
||||
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:
|
||||
nick_list.remove(after.nick)
|
||||
nick_list.append(after.nick)
|
||||
|
||||
@@ -15,7 +15,7 @@ class ModLog:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modlogset(self, ctx: commands.Context):
|
||||
"""Settings for the mod log"""
|
||||
|
||||
@@ -27,3 +27,18 @@ class RuleType(commands.Converter):
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class ClearableRuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
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.config import Config
|
||||
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 .converters import CogOrCommand, RuleType
|
||||
from .converters import CogOrCommand, RuleType, ClearableRuleType
|
||||
from .mass_resolution import mass_resolve
|
||||
|
||||
_models = ["owner", "guildowner", "admin", "mod", "all"]
|
||||
|
||||
_ = Translator("Permissions", __file__)
|
||||
|
||||
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(_)
|
||||
@@ -34,8 +37,32 @@ class Permissions:
|
||||
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||
self.config.register_global(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
|
||||
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):
|
||||
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 = [
|
||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
|
||||
@@ -86,11 +107,26 @@ class Permissions:
|
||||
if override is not None:
|
||||
return override
|
||||
|
||||
for model in self.resolution_order[level]:
|
||||
override_model = getattr(self, model + "_model", None)
|
||||
override = await override_model(ctx) if override_model else None
|
||||
# checked ids + configureable to be checked against
|
||||
cache_tup = entries_from_ctx(ctx) + (
|
||||
ctx.cog.__class__.__name__,
|
||||
ctx.command.qualified_name,
|
||||
)
|
||||
if cache_tup in self.cache:
|
||||
override = self.cache[cache_tup]
|
||||
if override is not None:
|
||||
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 = [
|
||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
|
||||
@@ -115,7 +151,8 @@ class Permissions:
|
||||
"""
|
||||
Handles guild level overrides
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
return None
|
||||
async with self.config.guild(ctx.guild).owner_models() as 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 mod_model(self, ctx: commands.Context) -> bool:
|
||||
|
||||
@commands.group(aliases=["p"], autohelp=True)
|
||||
@commands.group(aliases=["p"])
|
||||
async def permissions(self, ctx: commands.Context):
|
||||
"""
|
||||
Permission management tools
|
||||
@@ -223,6 +260,7 @@ class Permissions:
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="getglobalacl")
|
||||
@@ -249,6 +287,7 @@ class Permissions:
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -278,6 +317,7 @@ class Permissions:
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="updateglobalacl")
|
||||
@@ -297,6 +337,7 @@ class Permissions:
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="addglobalrule")
|
||||
@@ -340,6 +381,7 @@ class Permissions:
|
||||
data[model_type][type_name][allow_or_deny].append(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -384,6 +426,7 @@ class Permissions:
|
||||
data[model_type][type_name][allow_or_deny].append(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="removeglobalrule")
|
||||
@@ -427,6 +470,7 @@ class Permissions:
|
||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -471,23 +515,18 @@ class Permissions:
|
||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="setdefaultguildrule")
|
||||
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
|
||||
|
||||
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}.get(allow_or_deny)
|
||||
else:
|
||||
val_to_set = None
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
@@ -501,23 +540,17 @@ class Permissions:
|
||||
|
||||
models.update(data)
|
||||
await ctx.send(_("Default set."))
|
||||
self.invalidate_cache(type_name)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="setdefaultglobalrule")
|
||||
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
|
||||
|
||||
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}.get(allow_or_deny)
|
||||
else:
|
||||
val_to_set = None
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.owner_models() as models:
|
||||
@@ -531,32 +564,17 @@ class Permissions:
|
||||
|
||||
models.update(data)
|
||||
await ctx.send(_("Default set."))
|
||||
self.invalidate_cache(type_name)
|
||||
|
||||
@commands.bot_has_permissions(add_reactions=True)
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="clearglobalsettings")
|
||||
async def clear_globals(self, ctx: commands.Context):
|
||||
"""
|
||||
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()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="clearguildsettings")
|
||||
@@ -564,23 +582,61 @@ class Permissions:
|
||||
"""
|
||||
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?")
|
||||
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."))
|
||||
async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
|
||||
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
|
||||
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.guild(ctx.guild).owner_models.clear()
|
||||
await ctx.send(_("Guild settings cleared."))
|
||||
agreed = REACTS.get(str(reaction))
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
def entries_from_ctx(ctx: commands.Context) -> tuple:
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
# entries now contains the following (in order) (if applicable)
|
||||
# author.id
|
||||
# author.voice.voice_channel.id
|
||||
# channel.id
|
||||
# role.id for each role (highest to lowest)
|
||||
# (implicitly) guild.id because
|
||||
# the @everyone role shares an id with the guild
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
||||
"""
|
||||
Returns the value from a check if it is valid
|
||||
@@ -56,23 +73,7 @@ def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) ->
|
||||
"""
|
||||
resolves specific lists
|
||||
"""
|
||||
|
||||
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:
|
||||
for entry in entries_from_ctx(ctx):
|
||||
if entry in whitelist:
|
||||
return True
|
||||
if entry in blacklist:
|
||||
|
||||
@@ -56,32 +56,32 @@ class Reports:
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@commands.guild_only()
|
||||
@commands.group(name="reportset", autohelp=True)
|
||||
@commands.group(name="reportset")
|
||||
async def reportset(self, ctx: commands.Context):
|
||||
"""
|
||||
settings for reports
|
||||
Settings for the report system.
|
||||
"""
|
||||
pass
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="output")
|
||||
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 ctx.send(_("Report Channel Set."))
|
||||
await ctx.send(_("The report channel has been set."))
|
||||
|
||||
@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):
|
||||
"""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 = not active
|
||||
await self.config.guild(ctx.guild).active.set(active)
|
||||
if active:
|
||||
await ctx.send(_("Reporting now enabled"))
|
||||
await ctx.send(_("Reporting is now enabled"))
|
||||
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):
|
||||
ret = False
|
||||
@@ -105,7 +105,7 @@ class Reports:
|
||||
*,
|
||||
mod: bool = False,
|
||||
permissions: Union[discord.Permissions, dict] = None,
|
||||
prompt: str = ""
|
||||
prompt: str = "",
|
||||
):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
@@ -175,7 +175,10 @@ class Reports:
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
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))
|
||||
send_content = None
|
||||
@@ -201,10 +204,10 @@ class Reports:
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
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
|
||||
to use it non interactively
|
||||
Use without arguments for interactive reporting, or do
|
||||
[p]report <text> to use it non-interactively.
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -224,14 +227,17 @@ class Reports:
|
||||
if self.antispam[guild.id][author.id].spammy:
|
||||
return await author.send(
|
||||
_(
|
||||
"You've sent a few too many of these recently. "
|
||||
"Contact a server admin to resolve this, or try again "
|
||||
"later."
|
||||
"You've sent too many reports recently. "
|
||||
"Please contact a server admin if this is important matter, "
|
||||
"or please wait and try again later."
|
||||
)
|
||||
)
|
||||
if author.id in self.user_cache:
|
||||
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)
|
||||
|
||||
@@ -263,7 +269,9 @@ class Reports:
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
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:
|
||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
@@ -313,10 +321,12 @@ class Reports:
|
||||
@report.command(name="interact")
|
||||
async def response(self, ctx, ticket_number: int):
|
||||
"""
|
||||
opens a message tunnel between things you say in this channel
|
||||
and the ticket opener's direct messages
|
||||
Open a message tunnel.
|
||||
|
||||
This tunnel will forward things you say in this channel
|
||||
to the ticket opener's direct messages.
|
||||
|
||||
tunnels do not persist across bot restarts
|
||||
Tunnels do not persist across bot restarts.
|
||||
"""
|
||||
|
||||
# note, mod_or_permissions is an implicit guild_only
|
||||
@@ -342,14 +352,15 @@ class Reports:
|
||||
)
|
||||
|
||||
big_topic = _(
|
||||
"{who} opened a 2-way communication."
|
||||
"{who} opened a 2-way communication "
|
||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||
"(8MB file size limitation on uploads) "
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded will be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
"You can close a communication at any point by reacting with "
|
||||
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||
"Any message succesfully forwarded will be marked with "
|
||||
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||
"Tunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||
@@ -357,8 +368,7 @@ class Reports:
|
||||
try:
|
||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("User has disabled DMs."))
|
||||
tun.close()
|
||||
await ctx.send(_("That user has DMs disabled."))
|
||||
else:
|
||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||
|
||||
@@ -75,14 +75,14 @@ class Streams:
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
stream = TwitchStream(name=channel_name, token=token)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
is_name = self.check_name_or_id(channel_id_or_name)
|
||||
if is_name:
|
||||
@@ -93,19 +93,19 @@ class Streams:
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@@ -113,9 +113,9 @@ class Streams:
|
||||
try:
|
||||
embed = await stream.is_online()
|
||||
except OfflineStream:
|
||||
await ctx.send(_("The stream is offline."))
|
||||
await ctx.send(_("That user is offline."))
|
||||
except StreamNotFound:
|
||||
await ctx.send(_("The channel doesn't seem to exist."))
|
||||
await ctx.send(_("That channel doesn't seem to exist."))
|
||||
except InvalidTwitchCredentials:
|
||||
await ctx.send(
|
||||
_("The twitch token is either invalid or has not been set. See `{}`.").format(
|
||||
@@ -124,7 +124,7 @@ class Streams:
|
||||
)
|
||||
except InvalidYoutubeCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
@@ -135,51 +135,50 @@ class Streams:
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod()
|
||||
async def streamalert(self, ctx: commands.Context):
|
||||
pass
|
||||
|
||||
@streamalert.group(name="twitch", autohelp=True)
|
||||
@streamalert.group(name="twitch")
|
||||
async def _twitch(self, ctx: commands.Context):
|
||||
"""Twitch stream alerts"""
|
||||
pass
|
||||
|
||||
@_twitch.command(name="channel")
|
||||
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())
|
||||
|
||||
@_twitch.command(name="community")
|
||||
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())
|
||||
|
||||
@streamalert.command(name="youtube")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="hitbox")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="mixer")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="picarto")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="stop")
|
||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||
"""Stops all stream notifications in the channel
|
||||
|
||||
Adding 'yes' will disable all notifications in the server"""
|
||||
streams = self.streams.copy()
|
||||
local_channel_ids = [c.id for c in ctx.guild.channels]
|
||||
@@ -202,7 +201,7 @@ class Streams:
|
||||
self.streams = 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"
|
||||
)
|
||||
|
||||
@@ -212,7 +211,7 @@ class Streams:
|
||||
async def streamalert_list(self, ctx: commands.Context):
|
||||
streams_list = defaultdict(list)
|
||||
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 channel_id in stream.channels:
|
||||
@@ -220,7 +219,7 @@ class Streams:
|
||||
streams_list[channel_id].append(stream.name.lower())
|
||||
|
||||
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
|
||||
|
||||
for channel_id, streams in streams_list.items():
|
||||
@@ -243,16 +242,16 @@ class Streams:
|
||||
exists = await self.check_exists(stream)
|
||||
except InvalidTwitchCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
return
|
||||
except InvalidYoutubeCredentials:
|
||||
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
|
||||
except APIError:
|
||||
@@ -294,7 +293,7 @@ class Streams:
|
||||
|
||||
await self.add_or_remove_community(ctx, community)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.mod()
|
||||
async def streamset(self, ctx: commands.Context):
|
||||
pass
|
||||
@@ -303,7 +302,6 @@ class Streams:
|
||||
@checks.is_owner()
|
||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||
"""Set the Client ID for twitch.
|
||||
|
||||
To do this, follow these steps:
|
||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
||||
2. Click *Register Your Application*
|
||||
@@ -311,7 +309,6 @@ class Streams:
|
||||
select an Application Category of your choosing.
|
||||
4. Click *Register*, and on the following page, copy the Client ID.
|
||||
5. Paste the Client ID into this command. Done!
|
||||
|
||||
"""
|
||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||
@@ -321,22 +318,19 @@ class Streams:
|
||||
@checks.is_owner()
|
||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||
"""Sets the API key for Youtube.
|
||||
|
||||
To get one, do the following:
|
||||
|
||||
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)
|
||||
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!
|
||||
|
||||
"""
|
||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||
await ctx.send(_("Youtube key set."))
|
||||
|
||||
@streamset.group(autohelp=True)
|
||||
@streamset.group()
|
||||
@commands.guild_only()
|
||||
async def mention(self, ctx: commands.Context):
|
||||
"""Sets mentions for stream alerts."""
|
||||
"""Sets mentions for alerts."""
|
||||
pass
|
||||
|
||||
@mention.command(aliases=["everyone"])
|
||||
@@ -348,15 +342,16 @@ class Streams:
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_everyone.set(False)
|
||||
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:
|
||||
await self.db.guild(guild).mention_everyone.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned."
|
||||
).format("@\u200beveryone")
|
||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||
"@\u200beveryone"
|
||||
)
|
||||
)
|
||||
|
||||
@mention.command(aliases=["here"])
|
||||
@@ -367,16 +362,13 @@ class Streams:
|
||||
current_setting = await self.db.guild(guild).mention_here()
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_here.set(False)
|
||||
await ctx.send(
|
||||
_("{} will no longer be mentioned for a stream alert.").format("@\u200bhere")
|
||||
)
|
||||
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
|
||||
else:
|
||||
await self.db.guild(guild).mention_here.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned."
|
||||
).format("@\u200bhere")
|
||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||
"@\u200bhere"
|
||||
)
|
||||
)
|
||||
|
||||
@mention.command()
|
||||
@@ -390,18 +382,16 @@ class Streams:
|
||||
if current_setting:
|
||||
await self.db.role(role).mention.set(False)
|
||||
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)
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.db.role(role).mention.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned."
|
||||
""
|
||||
).format("@\u200b{}".format(role.name))
|
||||
_("When a stream or community " "is live, {} will be mentioned." "").format(
|
||||
"@\u200b{}".format(role.name)
|
||||
)
|
||||
)
|
||||
|
||||
@streamset.command()
|
||||
@@ -420,7 +410,7 @@ class Streams:
|
||||
if stream not in self.streams:
|
||||
self.streams.append(stream)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -444,7 +434,7 @@ class Streams:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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)
|
||||
)
|
||||
@@ -455,7 +445,7 @@ class Streams:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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)
|
||||
)
|
||||
@@ -530,9 +520,9 @@ class Streams:
|
||||
mention_str = await self._get_mention_str(channel.guild)
|
||||
|
||||
if mention_str:
|
||||
content = "{}, {} is online!".format(mention_str, stream.name)
|
||||
content = "{}, {} is live!".format(mention_str, stream.name)
|
||||
else:
|
||||
content = "{} is online!".format(stream.name)
|
||||
content = "{} is live!".format(stream.name)
|
||||
|
||||
try:
|
||||
m = await channel.send(content, embed=embed)
|
||||
@@ -558,7 +548,7 @@ class Streams:
|
||||
try:
|
||||
stream_list = await community.get_community_streams()
|
||||
except CommunityNotFound:
|
||||
print(_("Community {} not found!").format(community.name))
|
||||
print(_("The Community {} was not found!").format(community.name))
|
||||
continue
|
||||
except OfflineCommunity:
|
||||
for message in community._messages_cache:
|
||||
|
||||
@@ -41,7 +41,7 @@ class Trivia:
|
||||
|
||||
self.conf.register_member(wins=0, games=0, total_score=0)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def triviaset(self, ctx: commands.Context):
|
||||
|
||||
@@ -22,7 +22,7 @@ async def warning_points_add_check(
|
||||
act = a
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ async def warning_points_remove_check(
|
||||
act = a
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@@ -81,10 +81,11 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
||||
the points threshold for the action"""
|
||||
await ctx.send(
|
||||
_(
|
||||
"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 "
|
||||
"Enter the command to be run when the user **exceeds the points for "
|
||||
"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 "
|
||||
"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. "
|
||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||
"Please wait 15 seconds before entering your response."
|
||||
@@ -100,8 +101,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then."))
|
||||
return None
|
||||
else:
|
||||
if msg.content == "none":
|
||||
return None
|
||||
|
||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||
if command is None:
|
||||
@@ -121,12 +124,13 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
||||
"""
|
||||
await ctx.send(
|
||||
_(
|
||||
"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 "
|
||||
"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 "
|
||||
"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 "
|
||||
"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. "
|
||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||
"Please wait 15 seconds before entering your response."
|
||||
@@ -142,9 +146,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then."))
|
||||
return None
|
||||
|
||||
else:
|
||||
if msg.content == "none":
|
||||
return None
|
||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||
if command is None:
|
||||
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.utils.mod import is_admin_or_superior
|
||||
from redbot.core.utils.chat_formatting import warning, pagify
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
_ = Translator("Warnings", __file__)
|
||||
|
||||
@@ -41,7 +42,7 @@ class Warnings:
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warningset(self, ctx: commands.Context):
|
||||
@@ -58,7 +59,7 @@ class Warnings:
|
||||
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
|
||||
)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnaction(self, ctx: commands.Context):
|
||||
@@ -74,27 +75,9 @@ class Warnings:
|
||||
"""
|
||||
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 = {
|
||||
"action_name": name,
|
||||
"points": points,
|
||||
@@ -114,7 +97,7 @@ class Warnings:
|
||||
# Sort in descending order by point count for ease in
|
||||
# finding the highest possible action to take
|
||||
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")
|
||||
@commands.guild_only()
|
||||
@@ -134,7 +117,7 @@ class Warnings:
|
||||
else:
|
||||
await ctx.send(_("No action named {} exists!").format(action_name))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnreason(self, ctx: commands.Context):
|
||||
@@ -182,13 +165,20 @@ class Warnings:
|
||||
msg_list = []
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
for r, v in registered_reasons.items():
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, v["points"], v["description"]
|
||||
if ctx.embed_requested():
|
||||
em = discord.Embed(
|
||||
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:
|
||||
await ctx.send_interactive(msg_list)
|
||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(_("There are no reasons configured!"))
|
||||
|
||||
@@ -202,14 +192,21 @@ class Warnings:
|
||||
msg_list = []
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for r in registered_actions:
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||
"Drop command: {}".format(
|
||||
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
||||
if await ctx.embed_requested():
|
||||
em = discord.Embed(title=_("Action: {name}").format(name=r["action_name"]))
|
||||
em.add_field(name=_("Points"), value="{}".format(r["points"]), inline=False)
|
||||
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:
|
||||
await ctx.send_interactive(msg_list)
|
||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
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
|
||||
"""
|
||||
if user == ctx.author:
|
||||
await ctx.send(_("You cannot warn yourself."))
|
||||
return
|
||||
if reason.lower() == "custom":
|
||||
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
||||
if not custom_allowed:
|
||||
@@ -256,7 +256,27 @@ class Warnings:
|
||||
await member_settings.total_points.set(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.guild_only()
|
||||
@@ -303,6 +323,9 @@ class Warnings:
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||
"""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
|
||||
member = guild.get_member(user_id)
|
||||
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]
|
||||
|
||||
|
||||
__version__ = "3.0.0b16"
|
||||
version_info = VersionInfo(3, 0, 0, "beta", 16)
|
||||
__version__ = "3.0.0b17"
|
||||
version_info = VersionInfo(3, 0, 0, "beta", 17)
|
||||
|
||||
@@ -138,6 +138,16 @@ def parse_cli_flags(args):
|
||||
action="store_true",
|
||||
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(
|
||||
"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):
|
||||
self.autohelp = kwargs.pop("autohelp", False)
|
||||
self.autohelp = kwargs.pop("autohelp", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def invoke(self, ctx):
|
||||
|
||||
view = ctx.view
|
||||
previous = view.index
|
||||
view.skip_ws()
|
||||
@@ -154,6 +153,7 @@ class Group(Command, commands.Group):
|
||||
|
||||
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
||||
if self.autohelp and not self.invoke_without_command:
|
||||
await self._verify_checks(ctx)
|
||||
await ctx.send_help()
|
||||
|
||||
await super().invoke(ctx)
|
||||
|
||||
@@ -152,6 +152,11 @@ class Context(commands.Context):
|
||||
else:
|
||||
return self.bot.color
|
||||
|
||||
@property
|
||||
def embed_color(self):
|
||||
# Rather than double awaiting.
|
||||
return self.embed_colour
|
||||
|
||||
async def embed_requested(self):
|
||||
"""
|
||||
Simple helper to call bot.embed_requested
|
||||
|
||||
@@ -29,12 +29,15 @@ class _ValueCtxManager:
|
||||
def __init__(self, value_obj, coro):
|
||||
self.value_obj = value_obj
|
||||
self.coro = coro
|
||||
self.raw_value = None
|
||||
self.__original_value = None
|
||||
|
||||
def __await__(self):
|
||||
return self.coro.__await__()
|
||||
|
||||
async def __aenter__(self):
|
||||
self.raw_value = await self
|
||||
self.__original_value = deepcopy(self.raw_value)
|
||||
if not isinstance(self.raw_value, (list, dict)):
|
||||
raise TypeError(
|
||||
"Type of retrieved value must be mutable (i.e. "
|
||||
@@ -44,7 +47,8 @@ class _ValueCtxManager:
|
||||
return self.raw_value
|
||||
|
||||
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:
|
||||
@@ -335,7 +339,7 @@ class Group(Value):
|
||||
default = poss_default
|
||||
|
||||
try:
|
||||
return deepcopy(await self.driver.get(*self.identifiers, *path))
|
||||
return await self.driver.get(*self.identifiers, *path)
|
||||
except KeyError:
|
||||
if default is not ...:
|
||||
return default
|
||||
@@ -365,7 +369,7 @@ class Group(Value):
|
||||
|
||||
"""
|
||||
if not defaults:
|
||||
defaults = deepcopy(self.defaults)
|
||||
defaults = self.defaults
|
||||
|
||||
for key, value in current.items():
|
||||
if isinstance(value, collections.Mapping):
|
||||
@@ -392,7 +396,7 @@ class Group(Value):
|
||||
# is equivalent to
|
||||
|
||||
data = {"foo": {"bar": None}}
|
||||
d["foo"]["bar"] = "baz"
|
||||
data["foo"]["bar"] = "baz"
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
@@ -275,7 +275,7 @@ class Core(CoreLogic):
|
||||
"".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="Python", value=python_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)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
async def embedset(self, ctx: commands.Context):
|
||||
"""
|
||||
Commands for toggling embeds on or off.
|
||||
@@ -360,6 +360,7 @@ class Core(CoreLogic):
|
||||
|
||||
@embedset.command(name="guild")
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.guild_only()
|
||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
||||
"""
|
||||
Toggle the guild's embed setting.
|
||||
@@ -598,7 +599,7 @@ class Core(CoreLogic):
|
||||
pass
|
||||
await ctx.bot.shutdown(restart=True)
|
||||
|
||||
@commands.group(name="set", autohelp=True)
|
||||
@commands.group(name="set")
|
||||
async def _set(self, ctx):
|
||||
"""Changes Red's settings"""
|
||||
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.
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -981,7 +982,7 @@ class Core(CoreLogic):
|
||||
ctx.bot.disable_sentry()
|
||||
await ctx.send(_("Done. Sentry logging is now disabled."))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def helpset(self, ctx: commands.Context):
|
||||
"""Manage settings for the help command."""
|
||||
@@ -1264,7 +1265,7 @@ class Core(CoreLogic):
|
||||
else:
|
||||
await ctx.send(_("Message delivered to {}").format(destination))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def whitelist(self, ctx):
|
||||
"""
|
||||
@@ -1322,7 +1323,7 @@ class Core(CoreLogic):
|
||||
await ctx.bot.db.whitelist.set([])
|
||||
await ctx.send(_("Whitelist has been cleared."))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def blacklist(self, ctx):
|
||||
"""
|
||||
|
||||
@@ -2,15 +2,18 @@ import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from copy import deepcopy
|
||||
import hashlib
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import appdirs
|
||||
import tempfile
|
||||
|
||||
from .json_io import JsonIO
|
||||
|
||||
__all__ = [
|
||||
"create_temp_config",
|
||||
"load_basic_configuration",
|
||||
"cog_data_path",
|
||||
"core_data_path",
|
||||
@@ -39,6 +42,26 @@ if not config_dir:
|
||||
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):
|
||||
"""Loads the basic bootstrap configuration necessary for `Config`
|
||||
to know where to store or look for data.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
import copy
|
||||
import weakref
|
||||
import logging
|
||||
|
||||
@@ -97,7 +98,7 @@ class JSON(BaseDriver):
|
||||
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
||||
for i in full_identifiers:
|
||||
partial = partial[i]
|
||||
return partial
|
||||
return copy.deepcopy(partial)
|
||||
|
||||
async def set(self, *identifiers: str, value=None):
|
||||
partial = self.data
|
||||
@@ -107,7 +108,7 @@ class JSON(BaseDriver):
|
||||
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)
|
||||
|
||||
async def clear(self, *identifiers: str):
|
||||
|
||||
@@ -82,10 +82,7 @@ class Help(formatter.HelpFormatter):
|
||||
if self.pm_check(self.context):
|
||||
return self.context.bot.color
|
||||
else:
|
||||
if await self.context.bot.db.guild(self.context.guild).use_bot_color():
|
||||
return self.context.bot.color
|
||||
else:
|
||||
return self.me.color
|
||||
return await self.context.embed_colour()
|
||||
|
||||
@property
|
||||
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):
|
||||
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
|
||||
elif admin_role and discord.utils.get(user.roles, name=admin_role):
|
||||
return True
|
||||
elif mod_role and discord.utils.get(user.roles, name=mod_role):
|
||||
elif discord.utils.find(lambda r: r.id in (admin_role_id, mod_role_id), user.roles):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -220,17 +214,14 @@ async def is_admin_or_superior(
|
||||
else:
|
||||
raise TypeError("Only messages, members or roles may be passed")
|
||||
|
||||
server = obj.guild
|
||||
admin_role_id = await bot.db.guild(server).admin_role()
|
||||
admin_role_id = await bot.db.guild(obj.guild).admin_role()
|
||||
|
||||
if isinstance(obj, discord.Role):
|
||||
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):
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -5,9 +5,11 @@ import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
import pkg_resources
|
||||
from pathlib import Path
|
||||
from distutils.version import StrictVersion
|
||||
from redbot.setup import (
|
||||
basic_setup,
|
||||
load_existing_config,
|
||||
@@ -16,13 +18,14 @@ from redbot.setup import (
|
||||
create_backup,
|
||||
save_config,
|
||||
)
|
||||
from redbot.core import __version__
|
||||
from redbot.core.utils import safe_delete
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
if sys.platform == "linux":
|
||||
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
|
||||
|
||||
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
||||
@@ -384,12 +387,27 @@ def debug_info():
|
||||
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():
|
||||
if IS_WINDOWS:
|
||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||
clear_screen()
|
||||
loop = asyncio.get_event_loop()
|
||||
outdated, new_version = loop.run_until_complete(is_outdated())
|
||||
while True:
|
||||
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("2. Run Red")
|
||||
print("3. Update Red")
|
||||
@@ -418,13 +436,12 @@ def main_menu():
|
||||
basic_setup()
|
||||
wait()
|
||||
elif choice == "5":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
||||
loop.run_until_complete(remove_instance_interaction())
|
||||
wait()
|
||||
elif choice == "6":
|
||||
debug_info()
|
||||
elif choice == "7":
|
||||
while True:
|
||||
loop = asyncio.get_event_loop()
|
||||
clear_screen()
|
||||
print("==== Reinstall Red ====")
|
||||
print(
|
||||
@@ -455,7 +472,7 @@ def main_menu():
|
||||
def main():
|
||||
if not PYTHON_OK:
|
||||
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
|
||||
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
|
||||
|
||||
__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")
|
||||
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.seek(0)
|
||||
try:
|
||||
m.compile([tfile.name])
|
||||
m.compile([tfile.name], output_dir=tdir)
|
||||
except (CCompilerError, DistutilsPlatformError):
|
||||
return False
|
||||
return True
|
||||
@@ -111,10 +111,10 @@ setup(
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Framework :: AsyncIO",
|
||||
"Framework :: Pytest",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Documentation :: Sphinx",
|
||||
@@ -124,7 +124,8 @@ setup(
|
||||
"redbot=redbot.__main__:main",
|
||||
"redbot-setup=redbot.setup:main",
|
||||
"redbot-launcher=redbot.launcher:main",
|
||||
]
|
||||
],
|
||||
"pytest11": ["red-discordbot = redbot.pytest"],
|
||||
},
|
||||
python_requires=">=3.6,<3.7",
|
||||
setup_requires=get_requirements(),
|
||||
|
||||
@@ -2,20 +2,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from redbot.cogs.admin import Admin
|
||||
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()
|
||||
from redbot.pytest.admin import *
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from collections import namedtuple
|
||||
|
||||
from redbot.cogs.dataconverter import core_specs
|
||||
from redbot.pytest.dataconverter import *
|
||||
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))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def specresolver():
|
||||
here = Path(__file__)
|
||||
|
||||
resolver = core_specs.SpecResolver(here.parent)
|
||||
return resolver
|
||||
|
||||
|
||||
@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")
|
||||
conf = specresolver.get_config_object(red, cogname, attr, _id)
|
||||
|
||||
|
||||
@@ -6,69 +6,12 @@ import pytest
|
||||
from unittest.mock import MagicMock
|
||||
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.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):
|
||||
repo_folder = Path(str(tmpdir)) / "repos" / "squid" / ".git"
|
||||
repo_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -3,31 +3,9 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from redbot.pytest.downloader import *
|
||||
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):
|
||||
for k, v in INFO_JSON.items():
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import pytest
|
||||
|
||||
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)
|
||||
from redbot.pytest.alias import *
|
||||
|
||||
|
||||
def test_is_valid_alias_name(alias):
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@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
|
||||
from redbot.pytest.economy import *
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@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
|
||||
from redbot.pytest.mod import *
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2,19 +2,10 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from redbot.pytest.cog_manager import *
|
||||
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.asyncio
|
||||
async def test_ensure_cogs_in_paths(cog_mgr, default_dir):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -379,13 +380,9 @@ async def test_value_ctxmgr_saves(config):
|
||||
async def test_value_ctxmgr_immutable(config):
|
||||
config.register_global(foo=True)
|
||||
|
||||
try:
|
||||
with pytest.raises(TypeError):
|
||||
async with config.foo() as foo:
|
||||
foo = False
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
foo = await config.foo()
|
||||
assert foo is True
|
||||
@@ -401,3 +398,35 @@ async def test_ctxmgr_no_shared_default(config, member_factory):
|
||||
foo.append(1)
|
||||
|
||||
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
|
||||
|
||||
from redbot.pytest.data_manager import *
|
||||
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):
|
||||
with pytest.raises(RuntimeError):
|
||||
data_manager.core_data_path()
|
||||
|
||||
@@ -1,52 +1,7 @@
|
||||
import pytest
|
||||
from redbot.core.rpc import RPC, RPCMixin, get_name
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@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
|
||||
from redbot.pytest.rpc import *
|
||||
from redbot.core.rpc import get_name
|
||||
|
||||
|
||||
def test_get_name(cog):
|
||||
|
||||
Reference in New Issue
Block a user