mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
Compare commits
20 Commits
3.3.0
...
V3/feature
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f28df2dd0 | ||
|
|
677d700363 | ||
|
|
8d73838d80 | ||
|
|
1fc4ece14c | ||
|
|
0adc960c60 | ||
|
|
c426aefd1a | ||
|
|
00cf395483 | ||
|
|
61ed864e02 | ||
|
|
90b099395b | ||
|
|
12e6f44135 | ||
|
|
e44fc69d14 | ||
|
|
8454239a98 | ||
|
|
64106c771a | ||
|
|
17234ac8fa | ||
|
|
b64802b92f | ||
|
|
6fa02b1a8d | ||
|
|
7420df9598 | ||
|
|
00bcd480e7 | ||
|
|
4c77cde249 | ||
|
|
1cb43b11a1 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -62,3 +62,4 @@ redbot/setup.py @tekulvw
|
|||||||
# Others
|
# Others
|
||||||
.travis.yml @Kowlin
|
.travis.yml @Kowlin
|
||||||
crowdin.yml @Kowlin
|
crowdin.yml @Kowlin
|
||||||
|
.github/workflows/* @Kowlin
|
||||||
|
|||||||
28
.github/workflows/publish_crowdin.yml
vendored
Normal file
28
.github/workflows/publish_crowdin.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Publish to Crowdin
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||||
|
echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y crowdin
|
||||||
|
pip install redgettext==3.1
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
CROWDIN_API_KEY: ${{ secrets.crowdin_token}}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.crowdin_identifier }}
|
||||||
|
run: |
|
||||||
|
make upload_translations
|
||||||
26
.github/workflows/publish_pypi.yml
vendored
Normal file
26
.github/workflows/publish_pypi.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Publish to PyPI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install setuptools wheel twine
|
||||||
|
- name: Build and publish
|
||||||
|
env:
|
||||||
|
TWINE_USERNAME: __token__
|
||||||
|
TWINE_PASSWORD: ${{ secrets.pypi_token }}
|
||||||
|
run: |
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
twine upload dist/*
|
||||||
73
.github/workflows/tests.yml
vendored
Normal file
73
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: Tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tox:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python_version:
|
||||||
|
- "3.8"
|
||||||
|
tox_env:
|
||||||
|
- py
|
||||||
|
- style
|
||||||
|
- docs
|
||||||
|
include:
|
||||||
|
- tox_env: py
|
||||||
|
friendly_name: Tests
|
||||||
|
- tox_env: style
|
||||||
|
friendly_name: Style
|
||||||
|
- tox_env: docs
|
||||||
|
friendly_name: Docs
|
||||||
|
fail-fast: false
|
||||||
|
name: Tox - ${{ matrix.friendly_name }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python_version }}
|
||||||
|
- name: Install tox
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- name: Tox test
|
||||||
|
env:
|
||||||
|
TOXENV: ${{ matrix.tox_env }}
|
||||||
|
run: tox
|
||||||
|
|
||||||
|
tox-postgres:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python_version:
|
||||||
|
- "3.8"
|
||||||
|
fail-fast: false
|
||||||
|
name: Tox - Postgres
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: postgres:10
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: red_db
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python_version }}
|
||||||
|
- name: Install tox
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- name: Tox test
|
||||||
|
env:
|
||||||
|
TOXENV: postgres
|
||||||
|
PGDATABASE: red_db
|
||||||
|
PGUSER: postgres
|
||||||
|
PGPASSWORD: postgres
|
||||||
|
PGPORT: 5432
|
||||||
|
run: tox
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://travis-ci.com/Cog-Creators/Red-DiscordBot">
|
<a href="https://github.com/Cog-Creators/Red-DiscordBot/actions">
|
||||||
<img src="https://api.travis-ci.com/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
|
<img src="https://github.com/Cog-Creators/Red-DiscordBot/workflows/Tests/badge.svg" alt="GitHub Actions">
|
||||||
</a>
|
</a>
|
||||||
<a href="http://red-discordbot.readthedocs.io/en/stable/?badge=stable">
|
<a href="http://red-discordbot.readthedocs.io/en/stable/?badge=stable">
|
||||||
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=stable" alt="Red on readthedocs.org">
|
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=stable" alt="Red on readthedocs.org">
|
||||||
|
|||||||
@@ -71,4 +71,4 @@ type the following command in the terminal, still by adding the instance name af
|
|||||||
|
|
||||||
To view Red’s log, you can acccess through journalctl:
|
To view Red’s log, you can acccess through journalctl:
|
||||||
|
|
||||||
:code:`sudo journalctl -u red@instancename`
|
:code:`sudo journalctl -eu red@instancename`
|
||||||
|
|||||||
@@ -1,5 +1,39 @@
|
|||||||
.. 3.3.x Changelogs
|
.. 3.3.x Changelogs
|
||||||
|
|
||||||
|
Redbot 3.3.1 (2020-02-05)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
|
||||||
|
Core Bot
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Add a cli flag for setting a max size of message cache
|
||||||
|
- Allow to edit prefix from command line using ``redbot --edit``.
|
||||||
|
- Some functions have been changed to no longer use deprecated asyncio functions
|
||||||
|
|
||||||
|
Core Commands
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- The short help text for dm has been made more useful
|
||||||
|
- dm no longer allows owners to have the bot attempt to DM itself
|
||||||
|
|
||||||
|
Utils
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Passing the event loop explicitly in utils is deprecated (Removal in 3.4)
|
||||||
|
|
||||||
|
Mod Cog
|
||||||
|
-------
|
||||||
|
|
||||||
|
- Hackban now works properly without being provided a number of days
|
||||||
|
|
||||||
|
Documentation Changes
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- Add ``-e`` flag to ``journalctl`` command in systemd guide so that it takes the user to the end of logs automatically.
|
||||||
|
- Added section to install docs for CentOS 8
|
||||||
|
- Improve usage of apt update in docs
|
||||||
|
|
||||||
Redbot 3.3.0 (2020-01-26)
|
Redbot 3.3.0 (2020-01-26)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,9 @@ Common Filters
|
|||||||
|
|
||||||
.. automodule:: redbot.core.utils.common_filters
|
.. automodule:: redbot.core.utils.common_filters
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Discord Helper Classes
|
||||||
|
======================
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.discord_helpers
|
||||||
|
:members:
|
||||||
|
|||||||
@@ -67,6 +67,25 @@ Complete the rest of the installation by `installing Python 3.8 with pyenv <inst
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
.. _install-centos8:
|
||||||
|
.. _install-rhel8:
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
CentOS and RHEL 8
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
yum -y install epel-release
|
||||||
|
yum update -y
|
||||||
|
yum -y groupinstall development
|
||||||
|
yum -y install git zlib-devel bzip2 bzip2-devel readline-devel sqlite \
|
||||||
|
sqlite-devel openssl-devel xz xz-devel libffi-devel findutils java-11-openjdk
|
||||||
|
|
||||||
|
Complete the rest of the installation by `installing Python 3.8 with pyenv <install-python-pyenv>`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-debian-stretch:
|
.. _install-debian-stretch:
|
||||||
|
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
@@ -231,14 +250,14 @@ We recommend adding the ``git-core`` ppa to install Git 2.11 or greater:
|
|||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install software-properties-common
|
sudo apt -y install software-properties-common
|
||||||
sudo add-apt-repository ppa:git-core/ppa
|
sudo add-apt-repository -yu ppa:git-core/ppa
|
||||||
|
|
||||||
We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
|
We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
sudo add-apt-repository -yu ppa:deadsnakes/ppa
|
||||||
|
|
||||||
Now install the pre-requirements with apt:
|
Now install the pre-requirements with apt:
|
||||||
|
|
||||||
@@ -262,8 +281,8 @@ We recommend adding the ``git-core`` ppa to install Git 2.11 or greater:
|
|||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install software-properties-common
|
sudo apt -y install software-properties-common
|
||||||
sudo add-apt-repository ppa:git-core/ppa
|
sudo add-apt-repository -yu ppa:git-core/ppa
|
||||||
|
|
||||||
Now, to install non-native version of python on non-LTS versions of Ubuntu, we recommend
|
Now, to install non-native version of python on non-LTS versions of Ubuntu, we recommend
|
||||||
installing pyenv. To do this, first run the following commands:
|
installing pyenv. To do this, first run the following commands:
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
|
|||||||
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.3.0"
|
__version__ = "3.3.2.dev1"
|
||||||
version_info = VersionInfo.from_str(__version__)
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Discord Version check
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import getpass
|
import getpass
|
||||||
@@ -20,7 +18,7 @@ from typing import NoReturn
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
# Set the event loop policies here so any subsequent `get_event_loop()`
|
# Set the event loop policies here so any subsequent `new_event_loop()`
|
||||||
# calls, in particular those as a result of the following imports,
|
# calls, in particular those as a result of the following imports,
|
||||||
# return the correct loop object.
|
# return the correct loop object.
|
||||||
from redbot import _update_event_loop_policy, __version__
|
from redbot import _update_event_loop_policy, __version__
|
||||||
@@ -107,6 +105,7 @@ async def edit_instance(red, cli_flags):
|
|||||||
no_prompt = cli_flags.no_prompt
|
no_prompt = cli_flags.no_prompt
|
||||||
token = cli_flags.token
|
token = cli_flags.token
|
||||||
owner = cli_flags.owner
|
owner = cli_flags.owner
|
||||||
|
prefix = cli_flags.prefix
|
||||||
old_name = cli_flags.instance_name
|
old_name = cli_flags.instance_name
|
||||||
new_name = cli_flags.edit_instance_name
|
new_name = cli_flags.edit_instance_name
|
||||||
data_path = cli_flags.edit_data_path
|
data_path = cli_flags.edit_data_path
|
||||||
@@ -119,14 +118,20 @@ async def edit_instance(red, cli_flags):
|
|||||||
if new_name is None and confirm_overwrite:
|
if new_name is None and confirm_overwrite:
|
||||||
print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
|
print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if no_prompt and all(to_change is None for to_change in (token, owner, new_name, data_path)):
|
if (
|
||||||
|
no_prompt
|
||||||
|
and all(to_change is None for to_change in (token, owner, new_name, data_path))
|
||||||
|
and not prefix
|
||||||
|
):
|
||||||
print(
|
print(
|
||||||
"No arguments to edit were provided. Available arguments (check help for more "
|
"No arguments to edit were provided."
|
||||||
"information): --edit-instance-name, --edit-data-path, --copy-data, --owner, --token"
|
" Available arguments (check help for more information):"
|
||||||
|
" --edit-instance-name, --edit-data-path, --copy-data, --owner, --token, --prefix"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
await _edit_token(red, token, no_prompt)
|
await _edit_token(red, token, no_prompt)
|
||||||
|
await _edit_prefix(red, prefix, no_prompt)
|
||||||
await _edit_owner(red, owner, no_prompt)
|
await _edit_owner(red, owner, no_prompt)
|
||||||
|
|
||||||
data = deepcopy(data_manager.basic_config)
|
data = deepcopy(data_manager.basic_config)
|
||||||
@@ -152,6 +157,26 @@ async def _edit_token(red, token, no_prompt):
|
|||||||
print("Token updated.\n")
|
print("Token updated.\n")
|
||||||
|
|
||||||
|
|
||||||
|
async def _edit_prefix(red, prefix, no_prompt):
|
||||||
|
if prefix:
|
||||||
|
prefixes = sorted(prefix, reverse=True)
|
||||||
|
await red._config.prefix.set(prefixes)
|
||||||
|
elif not no_prompt and confirm("Would you like to change instance's prefixes?", default=False):
|
||||||
|
print(
|
||||||
|
"Enter the prefixes, separated by a space (please note "
|
||||||
|
"that prefixes containing a space will need to be added with [p]set prefix)"
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
prefixes = input("> ").strip().split()
|
||||||
|
if not prefixes:
|
||||||
|
print("You need to pass at least one prefix!")
|
||||||
|
continue
|
||||||
|
prefixes = sorted(prefixes, reverse=True)
|
||||||
|
await red._config.prefix.set(prefixes)
|
||||||
|
print("Prefixes updated.\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
async def _edit_owner(red, owner, no_prompt):
|
async def _edit_owner(red, owner, no_prompt):
|
||||||
if owner:
|
if owner:
|
||||||
if not (15 <= len(str(owner)) <= 21):
|
if not (15 <= len(str(owner)) <= 21):
|
||||||
@@ -271,7 +296,8 @@ def handle_edit(cli_flags: Namespace):
|
|||||||
"""
|
"""
|
||||||
This one exists to not log all the things like it's a full run of the bot.
|
This one exists to not log all the things like it's a full run of the bot.
|
||||||
"""
|
"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
data_manager.load_basic_configuration(cli_flags.instance_name)
|
data_manager.load_basic_configuration(cli_flags.instance_name)
|
||||||
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None, fetch_offline_members=True)
|
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None, fetch_offline_members=True)
|
||||||
try:
|
try:
|
||||||
@@ -283,6 +309,7 @@ def handle_edit(cli_flags: Namespace):
|
|||||||
print("Aborted!")
|
print("Aborted!")
|
||||||
finally:
|
finally:
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
loop.run_until_complete(asyncio.sleep(1))
|
||||||
|
asyncio.set_event_loop(None)
|
||||||
loop.stop()
|
loop.stop()
|
||||||
loop.close()
|
loop.close()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -433,7 +460,8 @@ def main():
|
|||||||
handle_edit(cli_flags)
|
handle_edit(cli_flags)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
if cli_flags.no_instance:
|
if cli_flags.no_instance:
|
||||||
print(
|
print(
|
||||||
@@ -497,6 +525,7 @@ def main():
|
|||||||
# results in a resource warning instead
|
# results in a resource warning instead
|
||||||
log.info("Please wait, cleaning up a bit more")
|
log.info("Please wait, cleaning up a bit more")
|
||||||
loop.run_until_complete(asyncio.sleep(2))
|
loop.run_until_complete(asyncio.sleep(2))
|
||||||
|
asyncio.set_event_loop(None)
|
||||||
loop.stop()
|
loop.stop()
|
||||||
loop.close()
|
loop.close()
|
||||||
exit_code = red._shutdown_mode if red is not None else 1
|
exit_code = red._shutdown_mode if red is not None else 1
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ class Announcer:
|
|||||||
failed.append(str(g.id))
|
failed.append(str(g.id))
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
msg = (
|
|
||||||
_("I could not announce to the following server: ")
|
|
||||||
if len(failed) == 1
|
|
||||||
else _("I could not announce to the following servers: ")
|
|
||||||
)
|
|
||||||
if failed:
|
if failed:
|
||||||
|
msg = (
|
||||||
|
_("I could not announce to the following server: ")
|
||||||
|
if len(failed) == 1
|
||||||
|
else _("I could not announce to the following servers: ")
|
||||||
|
)
|
||||||
msg += humanize_list(tuple(map(inline, failed)))
|
msg += humanize_list(tuple(map(inline, failed)))
|
||||||
await self.ctx.bot.send_to_owners(msg)
|
await self.ctx.bot.send_to_owners(msg)
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ class Downloader(commands.Cog):
|
|||||||
if not deps:
|
if not deps:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
repo = Repo("", "", "", "", Path.cwd(), loop=ctx.bot.loop)
|
repo = Repo("", "", "", "", Path.cwd())
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ class Repo(RepoJSONMixin):
|
|||||||
commit: str,
|
commit: str,
|
||||||
folder_path: Path,
|
folder_path: Path,
|
||||||
available_modules: Tuple[Installable, ...] = (),
|
available_modules: Tuple[Installable, ...] = (),
|
||||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
||||||
):
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.branch = branch
|
self.branch = branch
|
||||||
@@ -154,8 +153,6 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
self._repo_lock = asyncio.Lock()
|
self._repo_lock = asyncio.Lock()
|
||||||
|
|
||||||
self._loop = loop if loop is not None else asyncio.get_event_loop()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clean_url(self) -> str:
|
def clean_url(self) -> str:
|
||||||
"""Sanitized repo URL (with removed HTTP Basic Auth)"""
|
"""Sanitized repo URL (with removed HTTP Basic Auth)"""
|
||||||
@@ -529,7 +526,7 @@ class Repo(RepoJSONMixin):
|
|||||||
env["LANGUAGE"] = "C"
|
env["LANGUAGE"] = "C"
|
||||||
kwargs["env"] = env
|
kwargs["env"] = env
|
||||||
async with self._repo_lock:
|
async with self._repo_lock:
|
||||||
p: CompletedProcess = await self._loop.run_in_executor(
|
p: CompletedProcess = await asyncio.get_running_loop().run_in_executor(
|
||||||
self._executor,
|
self._executor,
|
||||||
functools.partial(sp_run, *args, stdout=PIPE, stderr=PIPE, **kwargs),
|
functools.partial(sp_run, *args, stdout=PIPE, stderr=PIPE, **kwargs),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import cast, Optional, Union
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, i18n, checks, modlog
|
from redbot.core import commands, i18n, checks, modlog
|
||||||
from redbot.core.utils.chat_formatting import pagify, humanize_number, bold
|
from redbot.core.utils.chat_formatting import pagify, humanize_number, bold, format_perms_list
|
||||||
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
|
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
|
||||||
from .abc import MixinMeta
|
from .abc import MixinMeta
|
||||||
from .converters import RawUserIds
|
from .converters import RawUserIds
|
||||||
@@ -21,6 +21,48 @@ class KickBanMixin(MixinMeta):
|
|||||||
Kick and ban commands and tasks go here.
|
Kick and ban commands and tasks go here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _voice_perm_check(
|
||||||
|
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
||||||
|
) -> bool:
|
||||||
|
"""Check if the bot and user have sufficient permissions for voicebans.
|
||||||
|
|
||||||
|
This also verifies that the user's voice state and connected
|
||||||
|
channel are not ``None``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if the permissions are sufficient and the user has
|
||||||
|
a valid voice state.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if user_voice_state is None or user_voice_state.channel is None:
|
||||||
|
await ctx.send(_("That user is not in a voice channel."))
|
||||||
|
return False
|
||||||
|
voice_channel: discord.VoiceChannel = user_voice_state.channel
|
||||||
|
required_perms = discord.Permissions()
|
||||||
|
required_perms.update(**perms)
|
||||||
|
if not voice_channel.permissions_for(ctx.me) >= required_perms:
|
||||||
|
await ctx.send(
|
||||||
|
_("I require the {perms} permission(s) in that user's channel to do that.").format(
|
||||||
|
perms=format_perms_list(required_perms)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
if (
|
||||||
|
ctx.permission_state is commands.PermState.NORMAL
|
||||||
|
and not voice_channel.permissions_for(ctx.author) >= required_perms
|
||||||
|
):
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You must have the {perms} permission(s) in that user's channel to use this "
|
||||||
|
"command."
|
||||||
|
).format(perms=format_perms_list(required_perms))
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400):
|
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400):
|
||||||
"""Handles the reinvite logic for getting an invite
|
"""Handles the reinvite logic for getting an invite
|
||||||
@@ -308,6 +350,9 @@ class KickBanMixin(MixinMeta):
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if days is None:
|
||||||
|
days = await self.settings.guild(guild).default_days()
|
||||||
|
|
||||||
if not (0 <= days <= 7):
|
if not (0 <= days <= 7):
|
||||||
await ctx.send(_("Invalid days. Must be between 0 and 7."))
|
await ctx.send(_("Invalid days. Must be between 0 and 7."))
|
||||||
return
|
return
|
||||||
@@ -329,9 +374,6 @@ class KickBanMixin(MixinMeta):
|
|||||||
await show_results()
|
await show_results()
|
||||||
return
|
return
|
||||||
|
|
||||||
if days is None:
|
|
||||||
days = await self.settings.guild(guild).default_days()
|
|
||||||
|
|
||||||
for user_id in user_ids:
|
for user_id in user_ids:
|
||||||
user = guild.get_member(user_id)
|
user = guild.get_member(user_id)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from .casetypes import CASETYPES
|
|||||||
from .events import Events
|
from .events import Events
|
||||||
from .kickban import KickBanMixin
|
from .kickban import KickBanMixin
|
||||||
from .movetocore import MoveToCore
|
from .movetocore import MoveToCore
|
||||||
from .mutes import MuteMixin
|
|
||||||
from .names import ModInfo
|
from .names import ModInfo
|
||||||
from .slowmode import Slowmode
|
from .slowmode import Slowmode
|
||||||
from .settings import ModSettings
|
from .settings import ModSettings
|
||||||
@@ -35,7 +34,6 @@ class Mod(
|
|||||||
Events,
|
Events,
|
||||||
KickBanMixin,
|
KickBanMixin,
|
||||||
MoveToCore,
|
MoveToCore,
|
||||||
MuteMixin,
|
|
||||||
ModInfo,
|
ModInfo,
|
||||||
Slowmode,
|
Slowmode,
|
||||||
commands.Cog,
|
commands.Cog,
|
||||||
|
|||||||
@@ -1,465 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import cast, Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from redbot.core import commands, checks, i18n, modlog
|
|
||||||
from redbot.core.utils.chat_formatting import format_perms_list
|
|
||||||
from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy
|
|
||||||
from .abc import MixinMeta
|
|
||||||
|
|
||||||
T_ = i18n.Translator("Mod", __file__)
|
|
||||||
|
|
||||||
_ = lambda s: s
|
|
||||||
mute_unmute_issues = {
|
|
||||||
"already_muted": _("That user can't send messages in this channel."),
|
|
||||||
"already_unmuted": _("That user isn't muted in this channel."),
|
|
||||||
"hierarchy_problem": _(
|
|
||||||
"I cannot let you do that. You are not higher than the user in the role hierarchy."
|
|
||||||
),
|
|
||||||
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
|
|
||||||
"permissions_issue": _(
|
|
||||||
"Failed to mute user. I need the manage roles "
|
|
||||||
"permission and the user I'm muting must be "
|
|
||||||
"lower than myself in the role hierarchy."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
_ = T_
|
|
||||||
|
|
||||||
|
|
||||||
class MuteMixin(MixinMeta):
|
|
||||||
"""
|
|
||||||
Stuff for mutes goes here
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _voice_perm_check(
|
|
||||||
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
|
||||||
) -> bool:
|
|
||||||
"""Check if the bot and user have sufficient permissions for voicebans.
|
|
||||||
|
|
||||||
This also verifies that the user's voice state and connected
|
|
||||||
channel are not ``None``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bool
|
|
||||||
``True`` if the permissions are sufficient and the user has
|
|
||||||
a valid voice state.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if user_voice_state is None or user_voice_state.channel is None:
|
|
||||||
await ctx.send(_("That user is not in a voice channel."))
|
|
||||||
return False
|
|
||||||
voice_channel: discord.VoiceChannel = user_voice_state.channel
|
|
||||||
required_perms = discord.Permissions()
|
|
||||||
required_perms.update(**perms)
|
|
||||||
if not voice_channel.permissions_for(ctx.me) >= required_perms:
|
|
||||||
await ctx.send(
|
|
||||||
_("I require the {perms} permission(s) in that user's channel to do that.").format(
|
|
||||||
perms=format_perms_list(required_perms)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
if (
|
|
||||||
ctx.permission_state is commands.PermState.NORMAL
|
|
||||||
and not voice_channel.permissions_for(ctx.author) >= required_perms
|
|
||||||
):
|
|
||||||
await ctx.send(
|
|
||||||
_(
|
|
||||||
"You must have the {perms} permission(s) in that user's channel to use this "
|
|
||||||
"command."
|
|
||||||
).format(perms=format_perms_list(required_perms))
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.admin_or_permissions(mute_members=True, deafen_members=True)
|
|
||||||
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
|
||||||
"""Unban a user from speaking and listening in the server's voice channels."""
|
|
||||||
user_voice_state = user.voice
|
|
||||||
if (
|
|
||||||
await self._voice_perm_check(
|
|
||||||
ctx, user_voice_state, deafen_members=True, mute_members=True
|
|
||||||
)
|
|
||||||
is False
|
|
||||||
):
|
|
||||||
return
|
|
||||||
needs_unmute = True if user_voice_state.mute else False
|
|
||||||
needs_undeafen = True if user_voice_state.deaf else False
|
|
||||||
audit_reason = get_audit_reason(ctx.author, reason)
|
|
||||||
if needs_unmute and needs_undeafen:
|
|
||||||
await user.edit(mute=False, deafen=False, reason=audit_reason)
|
|
||||||
elif needs_unmute:
|
|
||||||
await user.edit(mute=False, reason=audit_reason)
|
|
||||||
elif needs_undeafen:
|
|
||||||
await user.edit(deafen=False, reason=audit_reason)
|
|
||||||
else:
|
|
||||||
await ctx.send(_("That user isn't muted or deafened by the server!"))
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = ctx.guild
|
|
||||||
author = ctx.author
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"voiceunban",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=None,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(_("User is now allowed to speak and listen in voice channels"))
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.admin_or_permissions(mute_members=True, deafen_members=True)
|
|
||||||
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
|
||||||
"""Ban a user from speaking and listening in the server's voice channels."""
|
|
||||||
user_voice_state: discord.VoiceState = user.voice
|
|
||||||
if (
|
|
||||||
await self._voice_perm_check(
|
|
||||||
ctx, user_voice_state, deafen_members=True, mute_members=True
|
|
||||||
)
|
|
||||||
is False
|
|
||||||
):
|
|
||||||
return
|
|
||||||
needs_mute = True if user_voice_state.mute is False else False
|
|
||||||
needs_deafen = True if user_voice_state.deaf is False else False
|
|
||||||
audit_reason = get_audit_reason(ctx.author, reason)
|
|
||||||
author = ctx.author
|
|
||||||
guild = ctx.guild
|
|
||||||
if needs_mute and needs_deafen:
|
|
||||||
await user.edit(mute=True, deafen=True, reason=audit_reason)
|
|
||||||
elif needs_mute:
|
|
||||||
await user.edit(mute=True, reason=audit_reason)
|
|
||||||
elif needs_deafen:
|
|
||||||
await user.edit(deafen=True, reason=audit_reason)
|
|
||||||
else:
|
|
||||||
await ctx.send(_("That user is already muted and deafened server-wide!"))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"voiceban",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=None,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(_("User has been banned from speaking or listening in voice channels"))
|
|
||||||
|
|
||||||
@commands.group()
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.mod_or_permissions(manage_channels=True)
|
|
||||||
async def mute(self, ctx: commands.Context):
|
|
||||||
"""Mute users."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@mute.command(name="voice")
|
|
||||||
@commands.guild_only()
|
|
||||||
async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
|
||||||
"""Mute a user in their current voice channel."""
|
|
||||||
user_voice_state = user.voice
|
|
||||||
if (
|
|
||||||
await self._voice_perm_check(
|
|
||||||
ctx, user_voice_state, mute_members=True, manage_channels=True
|
|
||||||
)
|
|
||||||
is False
|
|
||||||
):
|
|
||||||
return
|
|
||||||
guild = ctx.guild
|
|
||||||
author = ctx.author
|
|
||||||
channel = user_voice_state.channel
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"vmute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=channel,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(
|
|
||||||
_("Muted {user} in channel {channel.name}").format(user=user, channel=channel)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.send(issue)
|
|
||||||
|
|
||||||
@mute.command(name="channel")
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
async def channel_mute(
|
|
||||||
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
|
|
||||||
):
|
|
||||||
"""Mute a user in the current text channel."""
|
|
||||||
author = ctx.message.author
|
|
||||||
channel = ctx.message.channel
|
|
||||||
guild = ctx.guild
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"cmute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=channel,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await channel.send(_("User has been muted in this channel."))
|
|
||||||
else:
|
|
||||||
await channel.send(issue)
|
|
||||||
|
|
||||||
@mute.command(name="server", aliases=["guild"])
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
|
||||||
"""Mutes user in the server"""
|
|
||||||
author = ctx.message.author
|
|
||||||
guild = ctx.guild
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
mute_success = []
|
|
||||||
for channel in guild.channels:
|
|
||||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
|
||||||
mute_success.append((success, issue))
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"smute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=None,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(_("User has been muted in this server."))
|
|
||||||
|
|
||||||
@commands.group()
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
|
||||||
@checks.mod_or_permissions(manage_channels=True)
|
|
||||||
async def unmute(self, ctx: commands.Context):
|
|
||||||
"""Unmute users."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@unmute.command(name="voice")
|
|
||||||
@commands.guild_only()
|
|
||||||
async def unmute_voice(
|
|
||||||
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
|
|
||||||
):
|
|
||||||
"""Unmute a user in their current voice channel."""
|
|
||||||
user_voice_state = user.voice
|
|
||||||
if (
|
|
||||||
await self._voice_perm_check(
|
|
||||||
ctx, user_voice_state, mute_members=True, manage_channels=True
|
|
||||||
)
|
|
||||||
is False
|
|
||||||
):
|
|
||||||
return
|
|
||||||
guild = ctx.guild
|
|
||||||
author = ctx.author
|
|
||||||
channel = user_voice_state.channel
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"vunmute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=channel,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(
|
|
||||||
_("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Unmute failed. Reason: {}").format(message))
|
|
||||||
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
@unmute.command(name="channel")
|
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
|
||||||
@commands.guild_only()
|
|
||||||
async def unmute_channel(
|
|
||||||
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
|
|
||||||
):
|
|
||||||
"""Unmute a user in this channel."""
|
|
||||||
channel = ctx.channel
|
|
||||||
author = ctx.author
|
|
||||||
guild = ctx.guild
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"cunmute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
channel=channel,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(_("User unmuted in this channel."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Unmute failed. Reason: {}").format(message))
|
|
||||||
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
@unmute.command(name="server", aliases=["guild"])
|
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
|
||||||
@commands.guild_only()
|
|
||||||
async def unmute_guild(
|
|
||||||
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
|
|
||||||
):
|
|
||||||
"""Unmute a user in this server."""
|
|
||||||
guild = ctx.guild
|
|
||||||
author = ctx.author
|
|
||||||
audit_reason = get_audit_reason(author, reason)
|
|
||||||
|
|
||||||
unmute_success = []
|
|
||||||
for channel in guild.channels:
|
|
||||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
|
||||||
unmute_success.append((success, message))
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
try:
|
|
||||||
await modlog.create_case(
|
|
||||||
self.bot,
|
|
||||||
guild,
|
|
||||||
ctx.message.created_at,
|
|
||||||
"sunmute",
|
|
||||||
user,
|
|
||||||
author,
|
|
||||||
reason,
|
|
||||||
until=None,
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
await ctx.send(e)
|
|
||||||
await ctx.send(_("User has been unmuted in this server."))
|
|
||||||
|
|
||||||
async def mute_user(
|
|
||||||
self,
|
|
||||||
guild: discord.Guild,
|
|
||||||
channel: discord.abc.GuildChannel,
|
|
||||||
author: discord.Member,
|
|
||||||
user: discord.Member,
|
|
||||||
reason: str,
|
|
||||||
) -> (bool, str):
|
|
||||||
"""Mutes the specified user in the specified channel"""
|
|
||||||
overwrites = channel.overwrites_for(user)
|
|
||||||
permissions = channel.permissions_for(user)
|
|
||||||
|
|
||||||
if permissions.administrator:
|
|
||||||
return False, _(mute_unmute_issues["is_admin"])
|
|
||||||
|
|
||||||
new_overs = {}
|
|
||||||
if not isinstance(channel, discord.TextChannel):
|
|
||||||
new_overs.update(speak=False)
|
|
||||||
if not isinstance(channel, discord.VoiceChannel):
|
|
||||||
new_overs.update(send_messages=False, add_reactions=False)
|
|
||||||
|
|
||||||
if all(getattr(permissions, p) is False for p in new_overs.keys()):
|
|
||||||
return False, _(mute_unmute_issues["already_muted"])
|
|
||||||
|
|
||||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
|
||||||
return False, _(mute_unmute_issues["hierarchy_problem"])
|
|
||||||
|
|
||||||
old_overs = {k: getattr(overwrites, k) for k in new_overs}
|
|
||||||
overwrites.update(**new_overs)
|
|
||||||
try:
|
|
||||||
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
|
|
||||||
except discord.Forbidden:
|
|
||||||
return False, _(mute_unmute_issues["permissions_issue"])
|
|
||||||
else:
|
|
||||||
await self.settings.member(user).set_raw(
|
|
||||||
"perms_cache", str(channel.id), value=old_overs
|
|
||||||
)
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
async def unmute_user(
|
|
||||||
self,
|
|
||||||
guild: discord.Guild,
|
|
||||||
channel: discord.abc.GuildChannel,
|
|
||||||
author: discord.Member,
|
|
||||||
user: discord.Member,
|
|
||||||
reason: str,
|
|
||||||
) -> (bool, str):
|
|
||||||
overwrites = channel.overwrites_for(user)
|
|
||||||
perms_cache = await self.settings.member(user).perms_cache()
|
|
||||||
|
|
||||||
if channel.id in perms_cache:
|
|
||||||
old_values = perms_cache[channel.id]
|
|
||||||
else:
|
|
||||||
old_values = {"send_messages": None, "add_reactions": None, "speak": None}
|
|
||||||
|
|
||||||
if all(getattr(overwrites, k) == v for k, v in old_values.items()):
|
|
||||||
return False, _(mute_unmute_issues["already_unmuted"])
|
|
||||||
|
|
||||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
|
||||||
return False, _(mute_unmute_issues["hierarchy_problem"])
|
|
||||||
|
|
||||||
overwrites.update(**old_values)
|
|
||||||
try:
|
|
||||||
if overwrites.is_empty():
|
|
||||||
await channel.set_permissions(
|
|
||||||
user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
|
|
||||||
except discord.Forbidden:
|
|
||||||
return False, _(mute_unmute_issues["permissions_issue"])
|
|
||||||
else:
|
|
||||||
await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
|
|
||||||
return True, None
|
|
||||||
@@ -215,8 +215,7 @@ class ModSettings(MixinMeta):
|
|||||||
@modset.command()
|
@modset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def dm(self, ctx: commands.Context, enabled: bool = None):
|
async def dm(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""Toggle whether to send a message to a user when they are
|
"""Toggle whether a message should be sent to a user when they are kicked/banned.
|
||||||
kicked/banned.
|
|
||||||
|
|
||||||
If this option is enabled, the bot will attempt to DM the user with the guild name
|
If this option is enabled, the bot will attempt to DM the user with the guild name
|
||||||
and reason as to why they were kicked/banned.
|
and reason as to why they were kicked/banned.
|
||||||
|
|||||||
5
redbot/cogs/mutes/__init__.py
Normal file
5
redbot/cogs/mutes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .mutes import Mutes
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Mutes(bot))
|
||||||
21
redbot/cogs/mutes/errors.py
Normal file
21
redbot/cogs/mutes/errors.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class ControlFlowException(Exception):
|
||||||
|
"""
|
||||||
|
The base exception for any exceptions used solely for control flow
|
||||||
|
If this or any subclass of this ever propogates, something has gone wrong.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoChangeError(ControlFlowException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PermError(ControlFlowException):
|
||||||
|
"""
|
||||||
|
An error to be raised when a permission issue is detected prior to an api call being made
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, friendly_error=None, *args):
|
||||||
|
self.friendly_error = friendly_error
|
||||||
|
super().__init__(*args)
|
||||||
410
redbot/cogs/mutes/mutes.py
Normal file
410
redbot/cogs/mutes/mutes.py
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
from typing import Awaitable, Dict, NamedTuple, Optional, Tuple, Union, no_type_check
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from redbot.core import commands, checks, modlog
|
||||||
|
from redbot.core.commands import TimedeltaConverter
|
||||||
|
from redbot.core.config import Config
|
||||||
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.discord_helpers import OverwriteDiff
|
||||||
|
from redbot.core.data_manager import cog_data_path
|
||||||
|
from redbot.core.utils.dbtools import APSWConnectionWrapper as Connection
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
from .errors import NoChangeError, PermError
|
||||||
|
|
||||||
|
TaskDict = Dict[Tuple[int, int], asyncio.Task]
|
||||||
|
|
||||||
|
_ = Translator("Mutes", __file__)
|
||||||
|
log = logging.getLogger("red.mutes")
|
||||||
|
|
||||||
|
|
||||||
|
@cog_i18n(_)
|
||||||
|
class Mutes(commands.Cog):
|
||||||
|
"""
|
||||||
|
A cog to mute users with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.conn = Connection(cog_data_path(self) / "mutes.db")
|
||||||
|
self.config = Config.get_conf(self, identifier=240961564503441410)
|
||||||
|
self.config.register_guild(
|
||||||
|
mute_deny_text=2112, # send, react
|
||||||
|
mute_deny_voice=2097152, # speak
|
||||||
|
excluded_channel_ids=[],
|
||||||
|
)
|
||||||
|
self._unmute_task = asyncio.create_task(self.unmute_loop())
|
||||||
|
self._task_queue = asyncio.Queue()
|
||||||
|
self._server_unmute_tasks: TaskDict = {}
|
||||||
|
self._channel_unmute_tasks: TaskDict = {}
|
||||||
|
self._ready = asyncio.Event()
|
||||||
|
self.bot.loop.create_task(self._cog_init())
|
||||||
|
|
||||||
|
async def _cog_init(self):
|
||||||
|
with self.conn.with_cursor() as cursor:
|
||||||
|
cursor.execute("""PRAGMA journal_mode=wal""")
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS mutes(
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
channel_id INTEGER NOT NULL,
|
||||||
|
guild_id INTEGER NOT NULL,
|
||||||
|
allows_added INTEGER,
|
||||||
|
allows_removed INTEGER,
|
||||||
|
denies_added INTEGER,
|
||||||
|
denies_removed INTEGER,
|
||||||
|
expires_at INTEGER,
|
||||||
|
PRIMARY KEY (user_id, channel_id)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self._ready.set()
|
||||||
|
|
||||||
|
async def cog_before_invoke(self):
|
||||||
|
await self._ready.wait()
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
self.unmute_task.cancel()
|
||||||
|
for task in self._server_unmute_tasks.values():
|
||||||
|
task.cancel()
|
||||||
|
for task in self._channel_unmute_tasks.values():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
def _clean_task_dict(self, task_dict):
|
||||||
|
|
||||||
|
is_debug = log.getEffectiveLevel() <= logging.DEBUG
|
||||||
|
|
||||||
|
for k in list(task_dict.keys()):
|
||||||
|
task = task_dict[k]
|
||||||
|
|
||||||
|
if task.canceled():
|
||||||
|
task_dict.pop(k, None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if task.done():
|
||||||
|
try:
|
||||||
|
r = task.result()
|
||||||
|
except Exception:
|
||||||
|
# Log exception info for dead tasks, but only while debugging.
|
||||||
|
if is_debug:
|
||||||
|
log.exception("Dead server unmute task.")
|
||||||
|
task_dict.pop(k, None)
|
||||||
|
|
||||||
|
async def unmute_loop(self):
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
while True:
|
||||||
|
async with self._task_lock:
|
||||||
|
self._clean_task_dict(self._server_unmute_tasks)
|
||||||
|
self._clean_task_dict(self._channel_unmute_tasks)
|
||||||
|
await self._schedule_unmutes(300)
|
||||||
|
await asyncio.sleep(300)
|
||||||
|
|
||||||
|
async def _schedule_unmutes(self, schedule_by_seconds: int = 300):
|
||||||
|
"""
|
||||||
|
Schedules unmuting.
|
||||||
|
Mutes get scheduled as tasks so that mute extensions or changes to make a mute
|
||||||
|
permanent can have a scheduled mute be canceled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
async def _cancel_channel_mute_delayed(self, *, delay: float, channel_id: int, member_id: int):
|
||||||
|
"""
|
||||||
|
After a delay, attempt to unmute someone
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
async def _cancel_server_mute_delayed(self, *, delay: float, guild_id: int, member_id: int):
|
||||||
|
"""
|
||||||
|
After a delay, attempt to unmute someone.
|
||||||
|
"""
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
guild = self.bot.get_guild(guild_id)
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
member = guild.get_member(member_id)
|
||||||
|
|
||||||
|
if not member: # Still clear this to avoid re-muting on-join after expiration.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def channel_mute_with_diff(
|
||||||
|
*,
|
||||||
|
channel: discord.abc.GuildChannel,
|
||||||
|
target: Union[discord.Role, discord.Member],
|
||||||
|
deny_value: int,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> OverwriteDiff:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
channel : discord.abc.GuildChannel
|
||||||
|
target : Union[discord.Role, discord.Member]
|
||||||
|
deny_value : int
|
||||||
|
The permissions values which should be denied.
|
||||||
|
reason : str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
OverwriteDiff
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
discord.Forbidden
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
discord.NotFound
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
discord.HTTPException
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
NoChangeError
|
||||||
|
the edit was aborted due to no change
|
||||||
|
in permissions between initial and requested
|
||||||
|
"""
|
||||||
|
diff_to_apply = OverwriteDiff(denies_added=deny_value)
|
||||||
|
start = channel.overwrites_for(target)
|
||||||
|
new_overwrite = start + diff_to_apply
|
||||||
|
result_diff = OverwriteDiff.from_overwrites(before=start, after=new_overwrite)
|
||||||
|
|
||||||
|
if not result_diff:
|
||||||
|
raise NoChangeError() from None
|
||||||
|
|
||||||
|
await channel.set_permissions(target, overwrite=new_overwrite, reason=reason)
|
||||||
|
return result_diff
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def channel_unmute_from_diff(
|
||||||
|
*,
|
||||||
|
channel: discord.abc.GuildChannel,
|
||||||
|
target: Union[discord.Role, discord.Member],
|
||||||
|
diff: OverwriteDiff,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
channel : discord.abc.GuildChannel
|
||||||
|
target : Union[discord.Role, discord.Member]
|
||||||
|
diff : OverwriteDiff
|
||||||
|
The recorded difference from a prior mute to undo
|
||||||
|
reason : str
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
discord.Forbidden
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
discord.NotFound
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
discord.HTTPException
|
||||||
|
see `discord.abc.GuildChannel.set_permissions`
|
||||||
|
NoChangeError
|
||||||
|
the edit was aborted due to no change
|
||||||
|
in permissions between initial and requested
|
||||||
|
"""
|
||||||
|
|
||||||
|
start = channel.overwrites_for(target)
|
||||||
|
new_overwrite = start - diff
|
||||||
|
|
||||||
|
if start == new_overwrite:
|
||||||
|
raise NoChangeError()
|
||||||
|
|
||||||
|
await channel.set_permissions(target, overwrite=new_overwrite, reason=reason)
|
||||||
|
|
||||||
|
async def do_command_server_mute(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
ctx: commands.Context,
|
||||||
|
target: discord.Member,
|
||||||
|
duration: Optional[timedelta] = None,
|
||||||
|
reason: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This avoids duplicated logic with the option to use
|
||||||
|
the command group as one of the commands itself.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : commands.Context
|
||||||
|
The context the command was invoked in
|
||||||
|
target : discord.Member
|
||||||
|
The person to mute
|
||||||
|
duration : Optional[timedelta]
|
||||||
|
If provided, the amount of time to mute the user for
|
||||||
|
reason : str
|
||||||
|
The reason for the mute
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
async def apply_server_mute(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
target: Optional[discord.Member] = None,
|
||||||
|
mod: discord.Member,
|
||||||
|
duration: Optional[timedelta],
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
target_id: Optional[int] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Applies a mute server wide
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target : Optional[discord.Member]
|
||||||
|
The member to be muted. This can only be omitted if ``target_id`` is supplied.
|
||||||
|
target_id : Optional[int]
|
||||||
|
The member id to mute. This can only be omitted if ``target`` is supplied.
|
||||||
|
mod : discord.Member
|
||||||
|
The responisble moderator
|
||||||
|
duration : Optional[timedelta]
|
||||||
|
If provided, the mute is considered temporary, and should be scheduled
|
||||||
|
for unmute after this period of time.
|
||||||
|
reason : Optional[str]
|
||||||
|
If provided, the reason for muting a user.
|
||||||
|
|
||||||
|
This should be the reason from the moderator's perspective.
|
||||||
|
All formatting should take place here.
|
||||||
|
This should be less than 900 characters long.
|
||||||
|
Longer reasons will be truncated.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ServerMuteResults
|
||||||
|
A class which contains the mute results
|
||||||
|
and some helpers for providing them to users.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
NoChangeError
|
||||||
|
If the server mute would result in zero changes.
|
||||||
|
ValueError
|
||||||
|
Raised if not given a target or target id, or if the target is not in the guild
|
||||||
|
PermError
|
||||||
|
Raised if we detect an invalid target or bot permissions.
|
||||||
|
This error will contain a user-friendly error message.
|
||||||
|
discord.Forbidden
|
||||||
|
This will only be raised for 2FA related forbiddens,
|
||||||
|
or if the bot's allowed permissions change mid operation.
|
||||||
|
discord.HTTPException
|
||||||
|
Sometimes the API gives these back without a reason.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
async def do_command_server_unmute(
|
||||||
|
self, *, ctx: commands.Context, target: discord.Member, reason: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
All actual command logic.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
async def do_command_channel_mute(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
ctx: commands.Context,
|
||||||
|
target: discord.Member,
|
||||||
|
channel: discord.abc.GuildChannel,
|
||||||
|
duration: Optional[timedelta] = None,
|
||||||
|
reason: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
All actual command logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def do_command_channel_unmute(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
ctx: commands.Context,
|
||||||
|
target: discord.Member,
|
||||||
|
channel: discord.abc.GuildChannel,
|
||||||
|
reason: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
All actual command logic.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
|
@commands.group()
|
||||||
|
async def _muteset(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Allows configuring [botname]'s mute behavior.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@commands.group(name="mute")
|
||||||
|
@no_type_check
|
||||||
|
async def mute_group(self, ctx):
|
||||||
|
"""
|
||||||
|
Mutes users.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@commands.group(name="tempmute")
|
||||||
|
@no_type_check
|
||||||
|
async def tempmute_group(
|
||||||
|
self,
|
||||||
|
ctx,
|
||||||
|
target: discord.Member = None,
|
||||||
|
duration: TimedeltaConverter = None,
|
||||||
|
*,
|
||||||
|
reason: str = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mutes users, for some amount of time.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@mute_group.command(name="channel")
|
||||||
|
@no_type_check
|
||||||
|
async def mute_channel(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""
|
||||||
|
Mutes a user in the current channel.
|
||||||
|
"""
|
||||||
|
await self.do_command_channel_mute(
|
||||||
|
ctx=ctx, target=target, reason=reason, channel=ctx.channel, duration=None
|
||||||
|
)
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@mute_group.command(name="server", aliases=["guild"])
|
||||||
|
@no_type_check
|
||||||
|
async def mute_server(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""
|
||||||
|
Mutes a user in the current server.
|
||||||
|
"""
|
||||||
|
await self.do_command_server_mute(ctx=ctx, target=target, reason=reason, duration=None)
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@tempmute_group.command(name="channel")
|
||||||
|
@no_type_check
|
||||||
|
async def tempmute_channel(
|
||||||
|
self, ctx, target: discord.Member, duration: TimedeltaConverter, *, reason: str = ""
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mutes a user in the current channel.
|
||||||
|
"""
|
||||||
|
await self.do_command_channel_mute(
|
||||||
|
ctx=ctx, target=target, reason=reason, channel=ctx.channel, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
|
@checks.mod()
|
||||||
|
@tempmute_group.command(name="server", aliases=["guild"])
|
||||||
|
@no_type_check
|
||||||
|
async def tempmute_server(
|
||||||
|
self, ctx, target: discord.Member, duration: TimedeltaConverter, *, reason: str = ""
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mutes a user in the current server.
|
||||||
|
"""
|
||||||
|
await self.do_command_server_mute(ctx=ctx, target=target, reason=reason, duration=duration)
|
||||||
53
redbot/cogs/mutes/utils.py
Normal file
53
redbot/cogs/mutes/utils.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
from .errors import PermError
|
||||||
|
|
||||||
|
_ = Translator("Mutes", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
def ngettext(singular: str, plural: str, count: int, **fmt_kwargs) -> str:
|
||||||
|
"""
|
||||||
|
This isn't a full ngettext.
|
||||||
|
|
||||||
|
Replace this with babel when Red can use that.
|
||||||
|
"""
|
||||||
|
return singular.format(**fmt_kwargs) if count == 1 else plural.format(**fmt_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def hierarchy_check(*, mod: discord.Member, target: discord.Member):
|
||||||
|
"""
|
||||||
|
Checks that things are hierarchy safe.
|
||||||
|
|
||||||
|
This does not check the bot can modify permissions.
|
||||||
|
This is assumed to be checked prior to command invocation.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
mod : discord.Member
|
||||||
|
The responsible moderator
|
||||||
|
target : discord.Member
|
||||||
|
The target of a mute
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
PermError
|
||||||
|
Any of:
|
||||||
|
- The target is above either the mod or bot.
|
||||||
|
- The target had the administrator perm
|
||||||
|
- The target is the guild owner
|
||||||
|
This error will contain a user facing error message.
|
||||||
|
"""
|
||||||
|
if target == target.guild.owner:
|
||||||
|
raise PermError(friendly_error=_("You can't mute the owner of a guild."))
|
||||||
|
|
||||||
|
if target.guild_permissions.administrator:
|
||||||
|
raise PermError(
|
||||||
|
friendly_error=_("You can't mute someone with the administrator permission.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if target.top_role >= target.guild.me:
|
||||||
|
raise PermError(friendly_error=_("I can't mute this user. (Discord Hierarchy applies)"))
|
||||||
|
|
||||||
|
if target.top_role >= mod.top_role:
|
||||||
|
raise PermError(friendly_error=_("You can't mute this user. (Discord Hierarchy applies)"))
|
||||||
@@ -149,6 +149,12 @@ class RedBase(
|
|||||||
if "command_not_found" not in kwargs:
|
if "command_not_found" not in kwargs:
|
||||||
kwargs["command_not_found"] = "Command {} not found.\n{}"
|
kwargs["command_not_found"] = "Command {} not found.\n{}"
|
||||||
|
|
||||||
|
message_cache_size = cli_flags.message_cache_size
|
||||||
|
if cli_flags.no_message_cache:
|
||||||
|
message_cache_size = None
|
||||||
|
kwargs["max_messages"] = message_cache_size
|
||||||
|
self._max_messages = message_cache_size
|
||||||
|
|
||||||
self._uptime = None
|
self._uptime = None
|
||||||
self._checked_time_accuracy = None
|
self._checked_time_accuracy = None
|
||||||
self._color = discord.Embed.Empty # This is needed or color ends up 0x000000
|
self._color = discord.Embed.Empty # This is needed or color ends up 0x000000
|
||||||
@@ -271,6 +277,10 @@ class RedBase(
|
|||||||
def colour(self) -> NoReturn:
|
def colour(self) -> NoReturn:
|
||||||
raise AttributeError("Please fetch the embed colour with `get_embed_colour`")
|
raise AttributeError("Please fetch the embed colour with `get_embed_colour`")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_messages(self) -> Optional[int]:
|
||||||
|
return self._max_messages
|
||||||
|
|
||||||
async def allowed_by_whitelist_blacklist(
|
async def allowed_by_whitelist_blacklist(
|
||||||
self,
|
self,
|
||||||
who: Optional[Union[discord.Member, discord.User]] = None,
|
who: Optional[Union[discord.Member, discord.User]] = None,
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ async def interactive_config(red, token_set, prefix_set, *, print_header=True):
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def positive_int(arg: str) -> int:
|
||||||
|
try:
|
||||||
|
x = int(arg)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError("Message cache size has to be a number.")
|
||||||
|
if x < 1000:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"Message cache size has to be greater than or equal to 1000."
|
||||||
|
)
|
||||||
|
if x > sys.maxsize:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Message cache size has to be lower than or equal to {sys.maxsize}."
|
||||||
|
)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
def parse_cli_flags(args):
|
def parse_cli_flags(args):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
|
description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
|
||||||
@@ -90,7 +106,7 @@ def parse_cli_flags(args):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Edit the instance. This can be done without console interaction "
|
help="Edit the instance. This can be done without console interaction "
|
||||||
"by passing --no-prompt and arguments that you want to change (available arguments: "
|
"by passing --no-prompt and arguments that you want to change (available arguments: "
|
||||||
"--edit-instance-name, --edit-data-path, --copy-data, --owner, --token).",
|
"--edit-instance-name, --edit-data-path, --copy-data, --owner, --token, --prefix).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--edit-instance-name",
|
"--edit-instance-name",
|
||||||
@@ -212,6 +228,15 @@ def parse_cli_flags(args):
|
|||||||
"all of the data on the host machine."
|
"all of the data on the host machine."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--message-cache-size",
|
||||||
|
type=positive_int,
|
||||||
|
default=1000,
|
||||||
|
help="Set the maximum number of messages to store in the internal message cache.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-message-cache", action="store_true", help="Disable the internal message cache.",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args(args)
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
|
|||||||
@@ -1581,12 +1581,12 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
settings, 'appearance' tab. Then right click a user
|
settings, 'appearance' tab. Then right click a user
|
||||||
and copy their id"""
|
and copy their id"""
|
||||||
destination = discord.utils.get(ctx.bot.get_all_members(), id=user_id)
|
destination = discord.utils.get(ctx.bot.get_all_members(), id=user_id)
|
||||||
if destination is None:
|
if destination is None or destination.bot:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Invalid ID or user not found. You can only "
|
"Invalid ID, user not found, or user is a bot. "
|
||||||
"send messages to people I share a server "
|
"You can only send messages to people I share "
|
||||||
"with."
|
"a server with."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import warnings
|
||||||
from asyncio import AbstractEventLoop, as_completed, Semaphore
|
from asyncio import AbstractEventLoop, as_completed, Semaphore
|
||||||
from asyncio.futures import isfuture
|
from asyncio.futures import isfuture
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -177,14 +178,20 @@ def bounded_gather_iter(
|
|||||||
TypeError
|
TypeError
|
||||||
When invalid parameters are passed
|
When invalid parameters are passed
|
||||||
"""
|
"""
|
||||||
if loop is None:
|
if loop is not None:
|
||||||
loop = asyncio.get_event_loop()
|
warnings.warn(
|
||||||
|
"Explicitly passing the loop will not work in Red 3.4+ and is currently ignored."
|
||||||
|
"Call this from the related event loop.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
if semaphore is None:
|
if semaphore is None:
|
||||||
if not isinstance(limit, int) or limit <= 0:
|
if not isinstance(limit, int) or limit <= 0:
|
||||||
raise TypeError("limit must be an int > 0")
|
raise TypeError("limit must be an int > 0")
|
||||||
|
|
||||||
semaphore = Semaphore(limit, loop=loop)
|
semaphore = Semaphore(limit)
|
||||||
|
|
||||||
pending = []
|
pending = []
|
||||||
|
|
||||||
@@ -195,7 +202,7 @@ def bounded_gather_iter(
|
|||||||
cof = _sem_wrapper(semaphore, cof)
|
cof = _sem_wrapper(semaphore, cof)
|
||||||
pending.append(cof)
|
pending.append(cof)
|
||||||
|
|
||||||
return as_completed(pending, loop=loop)
|
return as_completed(pending)
|
||||||
|
|
||||||
|
|
||||||
def bounded_gather(
|
def bounded_gather(
|
||||||
@@ -228,15 +235,21 @@ def bounded_gather(
|
|||||||
TypeError
|
TypeError
|
||||||
When invalid parameters are passed
|
When invalid parameters are passed
|
||||||
"""
|
"""
|
||||||
if loop is None:
|
if loop is not None:
|
||||||
loop = asyncio.get_event_loop()
|
warnings.warn(
|
||||||
|
"Explicitly passing the loop will not work in Red 3.4+ and is currently ignored."
|
||||||
|
"Call this from the related event loop.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
if semaphore is None:
|
if semaphore is None:
|
||||||
if not isinstance(limit, int) or limit <= 0:
|
if not isinstance(limit, int) or limit <= 0:
|
||||||
raise TypeError("limit must be an int > 0")
|
raise TypeError("limit must be an int > 0")
|
||||||
|
|
||||||
semaphore = Semaphore(limit, loop=loop)
|
semaphore = Semaphore(limit)
|
||||||
|
|
||||||
tasks = (_sem_wrapper(semaphore, task) for task in coros_or_futures)
|
tasks = (_sem_wrapper(semaphore, task) for task in coros_or_futures)
|
||||||
|
|
||||||
return asyncio.gather(*tasks, loop=loop, return_exceptions=return_exceptions)
|
return asyncio.gather(*tasks, return_exceptions=return_exceptions)
|
||||||
|
|||||||
152
redbot/core/utils/discord_helpers.py
Normal file
152
redbot/core/utils/discord_helpers.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import discord
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
__all__ = ["OverwriteDiff"]
|
||||||
|
|
||||||
|
|
||||||
|
class OverwriteDiff:
|
||||||
|
"""
|
||||||
|
Represents a change in PermissionOverwrites.
|
||||||
|
|
||||||
|
All math operations done with the values contained are bitwise.
|
||||||
|
|
||||||
|
This object is considered False for boolean logic when representing no change.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
allows_added : int
|
||||||
|
allows_removed : int
|
||||||
|
denies_added : int
|
||||||
|
denies_removed : int
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **data: int):
|
||||||
|
self.allows_added = data.pop("allows_added", 0)
|
||||||
|
self.allows_removed = data.pop("allows_removed", 0)
|
||||||
|
self.denies_added = data.pop("denies_added", 0)
|
||||||
|
self.denies_removed = data.pop("denies_removed", 0)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(self.allows_added & self.denies_added)
|
||||||
|
or (self.allows_removed & self.denies_removed)
|
||||||
|
or (self.allows_added & self.allows_removed)
|
||||||
|
or (self.denies_added & self.denies_removed)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"It is impossible for this to be the difference of two valid overwrite objects."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<OverwriteDiff "
|
||||||
|
f"allows_added={self.allows_added} allows_removed={self.allows_removed} "
|
||||||
|
f"denies_added={self.denies_added} denies_removed={self.denies_removed}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.allows_added or self.allows_removed or self.denies_added or self.denies_removed
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, int]:
|
||||||
|
return {
|
||||||
|
"allows_added": self.allows_added,
|
||||||
|
"allows_removed": self.allows_removed,
|
||||||
|
"denies_added": self.denies_added,
|
||||||
|
"denies_removed": self.denies_removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __radd__(self, other: discord.PermissionOverwrite) -> discord.PermissionOverwrite:
|
||||||
|
if not isinstance(other, discord.PermissionOverwrite):
|
||||||
|
return NotImplemented
|
||||||
|
return self.apply_to_overwirte(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other: discord.PermissionOverwrite) -> discord.PermissionOverwrite:
|
||||||
|
if not isinstance(other, discord.PermissionOverwrite):
|
||||||
|
return NotImplemented
|
||||||
|
return self.remove_from_overwrite(other)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, int]):
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_overwrites(
|
||||||
|
cls, before: discord.PermissionOverwrite, after: discord.PermissionOverwrite
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Returns the difference between two permission overwrites.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
before : discord.PermissionOverwrite
|
||||||
|
after : discord.PermissionOverwrite
|
||||||
|
"""
|
||||||
|
|
||||||
|
b_allow, b_deny = before.pair()
|
||||||
|
a_allow, a_deny = after.pair()
|
||||||
|
|
||||||
|
b_allow_val, b_deny_val = b_allow.value, b_deny.value
|
||||||
|
a_allow_val, a_deny_val = a_allow.value, a_deny.value
|
||||||
|
|
||||||
|
allows_added = a_allow_val & ~b_allow_val
|
||||||
|
allows_removed = b_allow_val & ~a_allow_val
|
||||||
|
|
||||||
|
denies_added = a_deny_val & ~b_deny_val
|
||||||
|
denies_removed = b_deny_val & ~a_deny_val
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
allows_added=allows_added,
|
||||||
|
allows_removed=allows_removed,
|
||||||
|
denies_added=denies_added,
|
||||||
|
denies_removed=denies_removed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_to_overwirte(
|
||||||
|
self, overwrite: discord.PermissionOverwrite
|
||||||
|
) -> discord.PermissionOverwrite:
|
||||||
|
"""
|
||||||
|
Creates a new overwrite by applying a diff to existing overwrites.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
overwrite : discord.PermissionOverwrite
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
discord.PermissionOverwrite
|
||||||
|
A new overwrite object with the diff applied to it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_allow, current_deny = overwrite.pair()
|
||||||
|
|
||||||
|
allow_value = (current_allow.value | self.allows_added) & ~self.allows_removed
|
||||||
|
deny_value = (current_deny.value | self.denies_added) & ~self.denies_removed
|
||||||
|
|
||||||
|
na = discord.Permissions(allow_value)
|
||||||
|
nd = discord.Permissions(deny_value)
|
||||||
|
return discord.PermissionOverwrite.from_pair(na, nd)
|
||||||
|
|
||||||
|
def remove_from_overwrite(
|
||||||
|
self, overwrite: discord.PermissionOverwrite
|
||||||
|
) -> discord.PermissionOverwrite:
|
||||||
|
"""
|
||||||
|
If given the after for the current diff object, this should return the before.
|
||||||
|
|
||||||
|
This can be used to roll back changes.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
overwrite : discord.PermissionOverwrite
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
discord.PermissionOverwrite
|
||||||
|
A new overwrite object with the diff removed from it.
|
||||||
|
"""
|
||||||
|
current_allow, current_deny = overwrite.pair()
|
||||||
|
|
||||||
|
allow_value = (current_allow.value | self.allows_removed) & ~self.allows_added
|
||||||
|
deny_value = (current_deny.value | self.denies_removed) & ~self.denies_added
|
||||||
|
|
||||||
|
na = discord.Permissions(allow_value)
|
||||||
|
nd = discord.Permissions(deny_value)
|
||||||
|
return discord.PermissionOverwrite.from_pair(na, nd)
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
|
import warnings
|
||||||
from typing import Union, Iterable, Optional
|
from typing import Union, Iterable, Optional
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
@@ -200,7 +201,9 @@ def start_adding_reactions(
|
|||||||
await message.add_reaction(emoji)
|
await message.add_reaction(emoji)
|
||||||
|
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
else:
|
||||||
|
warnings.warn("Explicitly passing the loop will not work in Red 3.4+", DeprecationWarning)
|
||||||
|
|
||||||
return loop.create_task(task())
|
return loop.create_task(task())
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ def bot_repo(event_loop):
|
|||||||
commit="",
|
commit="",
|
||||||
url="https://empty.com/something.git",
|
url="https://empty.com/something.git",
|
||||||
folder_path=cwd,
|
folder_path=cwd,
|
||||||
loop=event_loop,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -163,14 +162,7 @@ def _init_test_repo(destination: Path):
|
|||||||
async def _session_git_repo(tmp_path_factory, event_loop):
|
async def _session_git_repo(tmp_path_factory, event_loop):
|
||||||
# we will import repo only once once per session and duplicate the repo folder
|
# we will import repo only once once per session and duplicate the repo folder
|
||||||
repo_path = tmp_path_factory.mktemp("session_git_repo")
|
repo_path = tmp_path_factory.mktemp("session_git_repo")
|
||||||
repo = Repo(
|
repo = Repo(name="redbot-testrepo", url="", branch="master", commit="", folder_path=repo_path)
|
||||||
name="redbot-testrepo",
|
|
||||||
url="",
|
|
||||||
branch="master",
|
|
||||||
commit="",
|
|
||||||
folder_path=repo_path,
|
|
||||||
loop=event_loop,
|
|
||||||
)
|
|
||||||
git_dirparams = _init_test_repo(repo_path)
|
git_dirparams = _init_test_repo(repo_path)
|
||||||
fast_import = sp.Popen((*git_dirparams, "fast-import", "--quiet"), stdin=sp.PIPE)
|
fast_import = sp.Popen((*git_dirparams, "fast-import", "--quiet"), stdin=sp.PIPE)
|
||||||
with TEST_REPO_EXPORT_PTH.open(mode="rb") as f:
|
with TEST_REPO_EXPORT_PTH.open(mode="rb") as f:
|
||||||
@@ -193,7 +185,6 @@ async def git_repo(_session_git_repo, tmp_path, event_loop):
|
|||||||
branch=_session_git_repo.branch,
|
branch=_session_git_repo.branch,
|
||||||
commit=_session_git_repo.commit,
|
commit=_session_git_repo.commit,
|
||||||
folder_path=repo_path,
|
folder_path=repo_path,
|
||||||
loop=event_loop,
|
|
||||||
)
|
)
|
||||||
return repo
|
return repo
|
||||||
|
|
||||||
@@ -208,7 +199,6 @@ async def cloned_git_repo(_session_git_repo, tmp_path, event_loop):
|
|||||||
branch=_session_git_repo.branch,
|
branch=_session_git_repo.branch,
|
||||||
commit=_session_git_repo.commit,
|
commit=_session_git_repo.commit,
|
||||||
folder_path=repo_path,
|
folder_path=repo_path,
|
||||||
loop=event_loop,
|
|
||||||
)
|
)
|
||||||
sp.run(("git", "clone", str(_session_git_repo.folder_path), str(repo_path)), check=True)
|
sp.run(("git", "clone", str(_session_git_repo.folder_path), str(repo_path)), check=True)
|
||||||
return repo
|
return repo
|
||||||
@@ -224,7 +214,6 @@ async def git_repo_with_remote(git_repo, tmp_path, event_loop):
|
|||||||
branch=git_repo.branch,
|
branch=git_repo.branch,
|
||||||
commit=git_repo.commit,
|
commit=git_repo.commit,
|
||||||
folder_path=repo_path,
|
folder_path=repo_path,
|
||||||
loop=event_loop,
|
|
||||||
)
|
)
|
||||||
sp.run(("git", "clone", str(git_repo.folder_path), str(repo_path)), check=True)
|
sp.run(("git", "clone", str(git_repo.folder_path), str(repo_path)), check=True)
|
||||||
return repo
|
return repo
|
||||||
|
|||||||
@@ -371,8 +371,7 @@ def delete(
|
|||||||
remove_datapath: Optional[bool],
|
remove_datapath: Optional[bool],
|
||||||
):
|
):
|
||||||
"""Removes an instance."""
|
"""Removes an instance."""
|
||||||
loop = asyncio.get_event_loop()
|
asyncio.run(
|
||||||
loop.run_until_complete(
|
|
||||||
remove_instance(
|
remove_instance(
|
||||||
instance, interactive, delete_data, _create_backup, drop_db, remove_datapath
|
instance, interactive, delete_data, _create_backup, drop_db, remove_datapath
|
||||||
)
|
)
|
||||||
@@ -391,14 +390,12 @@ def convert(instance, backend):
|
|||||||
default_dirs = deepcopy(data_manager.basic_config_default)
|
default_dirs = deepcopy(data_manager.basic_config_default)
|
||||||
default_dirs["DATA_PATH"] = str(Path(instance_data[instance]["DATA_PATH"]))
|
default_dirs["DATA_PATH"] = str(Path(instance_data[instance]["DATA_PATH"]))
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
if current_backend == BackendType.MONGOV1:
|
if current_backend == BackendType.MONGOV1:
|
||||||
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
|
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
|
||||||
elif current_backend == BackendType.POSTGRES: # TODO: GH-3115
|
elif current_backend == BackendType.POSTGRES: # TODO: GH-3115
|
||||||
raise RuntimeError("Converting away from postgres isn't currently supported")
|
raise RuntimeError("Converting away from postgres isn't currently supported")
|
||||||
else:
|
else:
|
||||||
new_storage_details = loop.run_until_complete(do_migration(current_backend, target))
|
new_storage_details = asyncio.run(do_migration(current_backend, target))
|
||||||
|
|
||||||
if new_storage_details is not None:
|
if new_storage_details is not None:
|
||||||
default_dirs["STORAGE_TYPE"] = target.value
|
default_dirs["STORAGE_TYPE"] = target.value
|
||||||
@@ -422,8 +419,7 @@ def convert(instance, backend):
|
|||||||
)
|
)
|
||||||
def backup(instance: str, destination_folder: Union[str, Path]) -> None:
|
def backup(instance: str, destination_folder: Union[str, Path]) -> None:
|
||||||
"""Backup instance's data."""
|
"""Backup instance's data."""
|
||||||
loop = asyncio.get_event_loop()
|
asyncio.run(create_backup(instance, Path(destination_folder)))
|
||||||
loop.run_until_complete(create_backup(instance, Path(destination_folder)))
|
|
||||||
|
|
||||||
|
|
||||||
def run_cli():
|
def run_cli():
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ _update_event_loop_policy()
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def event_loop(request):
|
def event_loop(request):
|
||||||
"""Create an instance of the default event loop for entire session."""
|
"""Create an instance of the default event loop for entire session."""
|
||||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
yield loop
|
yield loop
|
||||||
|
asyncio.set_event_loop(None)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user