Compare commits

...

20 Commits

Author SHA1 Message Date
Michael H
2f28df2dd0 Merge branch 'V3/feature/mutes' of https://github.com/Cog-Creators/Red-DiscordBot into V3/feature/mutes 2020-02-06 15:09:40 -05:00
Michael H
677d700363 Merge branch 'V3/develop' into V3/feature/mutes 2020-02-06 15:09:19 -05:00
trundleroo
8d73838d80 Update announcer.py (#3514)
* Update announcer.py

* Update announcer.py
2020-02-06 18:27:32 +01:00
Kowlin
1fc4ece14c Updated readme badges. (#3511) 2020-02-05 17:32:35 -05:00
Michael H
0adc960c60 dev bump (#3512) 2020-02-05 17:32:05 -05:00
Michael H
c426aefd1a Version 3.3.1 (#3510)
* 331

* okay sphinx
2020-02-05 23:21:38 +01:00
Michael H
00cf395483 Handle deprecations in asyncio (#3509)
* passing loop to certain things was deprecated. additionally, `asyncio.get_event_loop()` is being deprecated

* awesome, checks are functioning as intended

* fun with fixtures

* we can just stop misuing that anyhow

* Update redbot/pytest/downloader.py

Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>

Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
2020-02-05 17:16:13 -05:00
Kowlin
61ed864e02 CI ports from Travis CI (#3435)
* Attempt 1, I suppose.

* Add the remaining 2 out of 3 jobs

* Spacing matters T_T

* So does formatting...

* More formatting fixing.

* First attempt at postgres services.

* Postgres attempt 2

* Update tests.yml

Flatten a python version I suppose.

* Update tests.yml

* Update tests.yml

* Update tests.yml

* Update tests.yml

* I wonder if this works lmao

* this is fun™

* let's go back

* add fail-fast

* Added publishing workflows

Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
2020-02-05 16:02:05 -05:00
Lane Babuder
90b099395b Adding CentOS 8 Documentation (#3463)
IUS will not be supporting RHEL 8, so utilizing epel-release and telling the system to use standard git is the best option.
2020-02-03 16:57:09 -05:00
aikaterna
12e6f44135 [Core] No DMing the bot (#3478)
* [Core] No DMing the bot

* Return early if target user is a bot
2020-02-03 16:26:33 -05:00
PredaaA
e44fc69d14 [Core] Add a cli flag for setting a max size of message cache (#3474)
* Add an arg in cli to change message cache size

* Add an arg in cli to change message cache size

* Changelog

* Actually pass None in message_cache_size

* Update cli.py

* Add a cli arg to disable message cache.

* Add a cli arg to disable message cache.

* well go away you useless

* you actually are an int

* Check if message cache is higher than 0 when set it.

* Use sys.maxsize as max cache size.

* Update cli.py

* Add bot.max_messages property.

* typos

* 🤦

* style
2020-02-03 16:14:45 -05:00
jack1142
8454239a98 [Mod] Fix shorthelp for [p]modset dm (#3488)
* Update settings.py

* Update settings.py

* Create 3488.misc.rst

* Update settings.py
2020-02-03 16:14:19 -05:00
jack1142
64106c771a Allow to edit prefixes through redbot --edit (#3486)
* feat: allow to edit prefixes through `redbot --edit`

* enhance: allow to setup multiple prefixes

* fix: gotta break out of the loop

* fix: gotta sort prefixes in reversed order

* fix: editing prefix shouldn't save it as token

* fix: sort prefixes when using flag too

* chore(changelog): add towncrier entry

* docs: update help for `--edit` flag
2020-02-03 16:08:48 -05:00
jack1142
17234ac8fa Add -e flag to journalctl command in systemd guide so that it takes the user to the end of logs automatically. (#3483)
* Make journalctl's pager go to the end of logs automatically

* Aaaaaaaand changelog
2020-02-01 01:26:39 +01:00
Kowlin
b64802b92f Fix for the unknown days argument on hackban. (#3475) 2020-01-30 18:55:11 +01:00
jack1142
6fa02b1a8d [Docs] Trigger update on sudo add-apt-repository (#3464) 2020-01-27 18:41:57 -09:00
Michael H
7420df9598 let's fix this for dev testers (#3458) 2020-01-27 03:35:16 -05:00
Michael H
00bcd480e7 dev bump (#3455) 2020-01-26 20:39:38 -05:00
Michael H
4c77cde249 Merge branch 'V3/develop' into V3/feature/mutes 2020-01-17 20:25:45 -05:00
DiscordLiz
1cb43b11a1 Some old work and some new (#3362)
* Some old work, some new

* c:style

* remove wrong version
2020-01-14 22:17:54 -05:00
31 changed files with 1001 additions and 535 deletions

1
.github/CODEOWNERS vendored
View File

@@ -62,3 +62,4 @@ redbot/setup.py @tekulvw
# Others
.travis.yml @Kowlin
crowdin.yml @Kowlin
.github/workflows/* @Kowlin

28
.github/workflows/publish_crowdin.yml vendored Normal file
View 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
View 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
View 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

View File

@@ -26,8 +26,8 @@
</a>
</p>
<p align="center">
<a href="https://travis-ci.com/Cog-Creators/Red-DiscordBot">
<img src="https://api.travis-ci.com/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
<a href="https://github.com/Cog-Creators/Red-DiscordBot/actions">
<img src="https://github.com/Cog-Creators/Red-DiscordBot/workflows/Tests/badge.svg" alt="GitHub Actions">
</a>
<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">

View File

@@ -71,4 +71,4 @@ type the following command in the terminal, still by adding the instance name af
To view Reds log, you can acccess through journalctl:
:code:`sudo journalctl -u red@instancename`
:code:`sudo journalctl -eu red@instancename`

View File

@@ -1,5 +1,39 @@
.. 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)
=========================

View File

@@ -51,3 +51,9 @@ Common Filters
.. automodule:: redbot.core.utils.common_filters
:members:
Discord Helper Classes
======================
.. automodule:: redbot.core.utils.discord_helpers
:members:

View File

@@ -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:
~~~~~~~~~~~~~~
@@ -231,14 +250,14 @@ We recommend adding the ``git-core`` ppa to install Git 2.11 or greater:
.. code-block:: none
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:git-core/ppa
sudo apt -y install software-properties-common
sudo add-apt-repository -yu ppa:git-core/ppa
We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
.. 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:
@@ -262,8 +281,8 @@ We recommend adding the ``git-core`` ppa to install Git 2.11 or greater:
.. code-block:: none
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:git-core/ppa
sudo apt -y install software-properties-common
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
installing pyenv. To do this, first run the following commands:

View File

@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
__version__ = "3.3.0"
__version__ = "3.3.2.dev1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python
# Discord Version check
import asyncio
import functools
import getpass
@@ -20,7 +18,7 @@ from typing import NoReturn
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,
# return the correct loop object.
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
token = cli_flags.token
owner = cli_flags.owner
prefix = cli_flags.prefix
old_name = cli_flags.instance_name
new_name = cli_flags.edit_instance_name
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:
print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
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(
"No arguments to edit were provided. Available arguments (check help for more "
"information): --edit-instance-name, --edit-data-path, --copy-data, --owner, --token"
"No arguments to edit were provided."
" Available arguments (check help for more information):"
" --edit-instance-name, --edit-data-path, --copy-data, --owner, --token, --prefix"
)
sys.exit(1)
await _edit_token(red, token, no_prompt)
await _edit_prefix(red, prefix, no_prompt)
await _edit_owner(red, owner, no_prompt)
data = deepcopy(data_manager.basic_config)
@@ -152,6 +157,26 @@ async def _edit_token(red, token, no_prompt):
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):
if owner:
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.
"""
loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
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)
try:
@@ -283,6 +309,7 @@ def handle_edit(cli_flags: Namespace):
print("Aborted!")
finally:
loop.run_until_complete(asyncio.sleep(1))
asyncio.set_event_loop(None)
loop.stop()
loop.close()
sys.exit(0)
@@ -433,7 +460,8 @@ def main():
handle_edit(cli_flags)
return
try:
loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if cli_flags.no_instance:
print(
@@ -497,6 +525,7 @@ def main():
# results in a resource warning instead
log.info("Please wait, cleaning up a bit more")
loop.run_until_complete(asyncio.sleep(2))
asyncio.set_event_loop(None)
loop.stop()
loop.close()
exit_code = red._shutdown_mode if red is not None else 1

View File

@@ -70,12 +70,12 @@ class Announcer:
failed.append(str(g.id))
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:
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)))
await self.ctx.bot.send_to_owners(msg)
await self.ctx.bot.send_to_owners(msg)
self.active = False

View File

@@ -462,7 +462,7 @@ class Downloader(commands.Cog):
if not deps:
await ctx.send_help()
return
repo = Repo("", "", "", "", Path.cwd(), loop=ctx.bot.loop)
repo = Repo("", "", "", "", Path.cwd())
async with ctx.typing():
success = await repo.install_raw_requirements(deps, self.LIB_PATH)

View File

@@ -135,7 +135,6 @@ class Repo(RepoJSONMixin):
commit: str,
folder_path: Path,
available_modules: Tuple[Installable, ...] = (),
loop: Optional[asyncio.AbstractEventLoop] = None,
):
self.url = url
self.branch = branch
@@ -154,8 +153,6 @@ class Repo(RepoJSONMixin):
self._repo_lock = asyncio.Lock()
self._loop = loop if loop is not None else asyncio.get_event_loop()
@property
def clean_url(self) -> str:
"""Sanitized repo URL (with removed HTTP Basic Auth)"""
@@ -529,7 +526,7 @@ class Repo(RepoJSONMixin):
env["LANGUAGE"] = "C"
kwargs["env"] = env
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,
functools.partial(sp_run, *args, stdout=PIPE, stderr=PIPE, **kwargs),
)

View File

@@ -7,7 +7,7 @@ from typing import cast, Optional, Union
import discord
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 .abc import MixinMeta
from .converters import RawUserIds
@@ -21,6 +21,48 @@ class KickBanMixin(MixinMeta):
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
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400):
"""Handles the reinvite logic for getting an invite
@@ -308,6 +350,9 @@ class KickBanMixin(MixinMeta):
await ctx.send_help()
return
if days is None:
days = await self.settings.guild(guild).default_days()
if not (0 <= days <= 7):
await ctx.send(_("Invalid days. Must be between 0 and 7."))
return
@@ -329,9 +374,6 @@ class KickBanMixin(MixinMeta):
await show_results()
return
if days is None:
days = await self.settings.guild(guild).default_days()
for user_id in user_ids:
user = guild.get_member(user_id)
if user is not None:

View File

@@ -10,7 +10,6 @@ from .casetypes import CASETYPES
from .events import Events
from .kickban import KickBanMixin
from .movetocore import MoveToCore
from .mutes import MuteMixin
from .names import ModInfo
from .slowmode import Slowmode
from .settings import ModSettings
@@ -35,7 +34,6 @@ class Mod(
Events,
KickBanMixin,
MoveToCore,
MuteMixin,
ModInfo,
Slowmode,
commands.Cog,

View File

@@ -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

View File

@@ -215,8 +215,7 @@ class ModSettings(MixinMeta):
@modset.command()
@commands.guild_only()
async def dm(self, ctx: commands.Context, enabled: bool = None):
"""Toggle whether to send a message to a user when they are
kicked/banned.
"""Toggle whether a message should be sent to a user when they are kicked/banned.
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.

View File

@@ -0,0 +1,5 @@
from .mutes import Mutes
def setup(bot):
bot.add_cog(Mutes(bot))

View 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
View 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)

View 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)"))

View File

@@ -149,6 +149,12 @@ class RedBase(
if "command_not_found" not in kwargs:
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._checked_time_accuracy = None
self._color = discord.Embed.Empty # This is needed or color ends up 0x000000
@@ -271,6 +277,10 @@ class RedBase(
def colour(self) -> NoReturn:
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(
self,
who: Optional[Union[discord.Member, discord.User]] = None,

View File

@@ -74,6 +74,22 @@ async def interactive_config(red, token_set, prefix_set, *, print_header=True):
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):
parser = argparse.ArgumentParser(
description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
@@ -90,7 +106,7 @@ def parse_cli_flags(args):
action="store_true",
help="Edit the instance. This can be done without console interaction "
"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(
"--edit-instance-name",
@@ -212,6 +228,15 @@ def parse_cli_flags(args):
"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)

View File

@@ -1581,12 +1581,12 @@ class Core(commands.Cog, CoreLogic):
settings, 'appearance' tab. Then right click a user
and copy their 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(
_(
"Invalid ID or user not found. You can only "
"send messages to people I share a server "
"with."
"Invalid ID, user not found, or user is a bot. "
"You can only send messages to people I share "
"a server with."
)
)
return

View File

@@ -1,4 +1,5 @@
import asyncio
import warnings
from asyncio import AbstractEventLoop, as_completed, Semaphore
from asyncio.futures import isfuture
from itertools import chain
@@ -177,14 +178,20 @@ def bounded_gather_iter(
TypeError
When invalid parameters are passed
"""
if loop is None:
loop = asyncio.get_event_loop()
if loop is not None:
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 not isinstance(limit, int) or limit <= 0:
raise TypeError("limit must be an int > 0")
semaphore = Semaphore(limit, loop=loop)
semaphore = Semaphore(limit)
pending = []
@@ -195,7 +202,7 @@ def bounded_gather_iter(
cof = _sem_wrapper(semaphore, cof)
pending.append(cof)
return as_completed(pending, loop=loop)
return as_completed(pending)
def bounded_gather(
@@ -228,15 +235,21 @@ def bounded_gather(
TypeError
When invalid parameters are passed
"""
if loop is None:
loop = asyncio.get_event_loop()
if loop is not None:
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 not isinstance(limit, int) or limit <= 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)
return asyncio.gather(*tasks, loop=loop, return_exceptions=return_exceptions)
return asyncio.gather(*tasks, return_exceptions=return_exceptions)

View 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)

View File

@@ -5,6 +5,7 @@
import asyncio
import contextlib
import functools
import warnings
from typing import Union, Iterable, Optional
import discord
@@ -200,7 +201,9 @@ def start_adding_reactions(
await message.add_reaction(emoji)
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())

View File

@@ -76,7 +76,6 @@ def bot_repo(event_loop):
commit="",
url="https://empty.com/something.git",
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):
# we will import repo only once once per session and duplicate the repo folder
repo_path = tmp_path_factory.mktemp("session_git_repo")
repo = Repo(
name="redbot-testrepo",
url="",
branch="master",
commit="",
folder_path=repo_path,
loop=event_loop,
)
repo = Repo(name="redbot-testrepo", url="", branch="master", commit="", folder_path=repo_path)
git_dirparams = _init_test_repo(repo_path)
fast_import = sp.Popen((*git_dirparams, "fast-import", "--quiet"), stdin=sp.PIPE)
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,
commit=_session_git_repo.commit,
folder_path=repo_path,
loop=event_loop,
)
return repo
@@ -208,7 +199,6 @@ async def cloned_git_repo(_session_git_repo, tmp_path, event_loop):
branch=_session_git_repo.branch,
commit=_session_git_repo.commit,
folder_path=repo_path,
loop=event_loop,
)
sp.run(("git", "clone", str(_session_git_repo.folder_path), str(repo_path)), check=True)
return repo
@@ -224,7 +214,6 @@ async def git_repo_with_remote(git_repo, tmp_path, event_loop):
branch=git_repo.branch,
commit=git_repo.commit,
folder_path=repo_path,
loop=event_loop,
)
sp.run(("git", "clone", str(git_repo.folder_path), str(repo_path)), check=True)
return repo

View File

@@ -371,8 +371,7 @@ def delete(
remove_datapath: Optional[bool],
):
"""Removes an instance."""
loop = asyncio.get_event_loop()
loop.run_until_complete(
asyncio.run(
remove_instance(
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["DATA_PATH"] = str(Path(instance_data[instance]["DATA_PATH"]))
loop = asyncio.get_event_loop()
if current_backend == BackendType.MONGOV1:
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
elif current_backend == BackendType.POSTGRES: # TODO: GH-3115
raise RuntimeError("Converting away from postgres isn't currently supported")
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:
default_dirs["STORAGE_TYPE"] = target.value
@@ -422,8 +419,7 @@ def convert(instance, backend):
)
def backup(instance: str, destination_folder: Union[str, Path]) -> None:
"""Backup instance's data."""
loop = asyncio.get_event_loop()
loop.run_until_complete(create_backup(instance, Path(destination_folder)))
asyncio.run(create_backup(instance, Path(destination_folder)))
def run_cli():

View File

@@ -12,8 +12,10 @@ _update_event_loop_policy()
@pytest.fixture(scope="session")
def event_loop(request):
"""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
asyncio.set_event_loop(None)
loop.close()