From d2b9504c3bce11f07c70270ebe15373e6999819b Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sun, 12 Jan 2020 17:13:58 +0100
Subject: [PATCH 01/91] [Docs] Add "Fork me on GitHub" ribbon (#3306)
* docs: add "Fork me on GitHub" ribbon
* chore(changelog): add towncrier entry
---
changelog.d/3306.docs.rst | 1 +
docs/_templates/layout.html | 9 +++++++++
2 files changed, 10 insertions(+)
create mode 100644 changelog.d/3306.docs.rst
create mode 100644 docs/_templates/layout.html
diff --git a/changelog.d/3306.docs.rst b/changelog.d/3306.docs.rst
new file mode 100644
index 000000000..e52352c5b
--- /dev/null
+++ b/changelog.d/3306.docs.rst
@@ -0,0 +1 @@
+Add "Fork me on GitHub" ribbon in top right corner of the docs page.
\ No newline at end of file
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
new file mode 100644
index 000000000..91fab053e
--- /dev/null
+++ b/docs/_templates/layout.html
@@ -0,0 +1,9 @@
+{% extends '!layout.html' %}
+{% block document %}
+{{ super() }}
+
+
+
+{% endblock %}
\ No newline at end of file
From 475416005541dd914030b03481596a62528359cd Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sun, 12 Jan 2020 22:08:34 +0100
Subject: [PATCH 02/91] [Docs] Update Debian/Raspbian instructions (#3352)
* Update install_linux_mac.rst
* Update install_linux_mac.rst
* Update install_linux_mac.rst
---
docs/install_linux_mac.rst | 33 +++++++++++++++++++++++++++++----
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index 26df6f823..1fe908070 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -51,15 +51,40 @@ CentOS and RHEL 7
Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+.. _install-debian-stretch:
+
+~~~~~~~~~~~~~~
+Debian Stretch
+~~~~~~~~~~~~~~
+
+.. note::
+
+ This guide is only for Debian Stretch users, these instructions won't work with
+ Raspbian Stretch. Raspbian Buster is the only version of Raspbian supported by Red.
+
+We recommend installing pyenv as a method of installing non-native versions of python on
+Debian Stretch. This guide will tell you how. First, run the following commands:
+
+.. code-block:: none
+
+ sudo echo "deb http://deb.debian.org/debian stretch-backports main" >> /etc/apt/sources.list.d/red-sources.list
+ sudo apt update
+ sudo apt -y install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
+ libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev \
+ libxmlsec1-dev libffi-dev liblzma-dev libgdbm-dev uuid-dev python3-openssl git openjdk-11-jre
+ CXX=/usr/bin/g++
+
+Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+
.. _install-debian:
.. _install-raspbian:
-~~~~~~~~~~~~~~~~~~~
-Debian and Raspbian
-~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Debian and Raspbian Buster
+~~~~~~~~~~~~~~~~~~~~~~~~~~
We recommend installing pyenv as a method of installing non-native versions of python on
-Debian/Raspbian. This guide will tell you how. First, run the following commands:
+Debian/Raspbian Buster. This guide will tell you how. First, run the following commands:
.. code-block:: none
From e52c20b9e79d2004e901b4ef31ef42f2ea1444ed Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 21:20:46 +0000
Subject: [PATCH 03/91] [Audio] Hotfix an edge case where an attribute error
can be raised (#3328)
* Limit Playlists
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3328.hotfix.1.rst | 1 +
redbot/cogs/audio/apis.py | 2 ++
redbot/cogs/audio/databases.py | 2 ++
3 files changed, 5 insertions(+)
create mode 100644 changelog.d/audio/3328.hotfix.1.rst
diff --git a/changelog.d/audio/3328.hotfix.1.rst b/changelog.d/audio/3328.hotfix.1.rst
new file mode 100644
index 000000000..08e7ed7b9
--- /dev/null
+++ b/changelog.d/audio/3328.hotfix.1.rst
@@ -0,0 +1 @@
+Fixed an attribute error that can be raised on play commands for spotify tracks.
\ No newline at end of file
diff --git a/redbot/cogs/audio/apis.py b/redbot/cogs/audio/apis.py
index eb6dbd7c0..87745d5ce 100644
--- a/redbot/cogs/audio/apis.py
+++ b/redbot/cogs/audio/apis.py
@@ -750,6 +750,8 @@ class MusicCache:
log.debug(f"Querying Local Database for {query}")
task = ("update", ("lavalink", {"query": query}))
self.append_task(ctx, *task)
+ else:
+ val = None
if val and not forced:
data = val
data["query"] = query
diff --git a/redbot/cogs/audio/databases.py b/redbot/cogs/audio/databases.py
index 6951a91b7..ee136c222 100644
--- a/redbot/cogs/audio/databases.py
+++ b/redbot/cogs/audio/databases.py
@@ -91,6 +91,8 @@ class CacheFetchResult:
k in self.query for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
):
self.query = json.loads(self.query)
+ else:
+ self.query = None
@dataclass
From d6936c87f3422d27d4649534a46fd9852676379f Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 22:37:04 +0000
Subject: [PATCH 04/91] chore (#3348)
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3345.enhance.1.rst | 1 +
redbot/cogs/audio/utils.py | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/audio/3345.enhance.1.rst
diff --git a/changelog.d/audio/3345.enhance.1.rst b/changelog.d/audio/3345.enhance.1.rst
new file mode 100644
index 000000000..1a1767590
--- /dev/null
+++ b/changelog.d/audio/3345.enhance.1.rst
@@ -0,0 +1 @@
+Fix an issues with the formatting of non existing local tracks.
\ No newline at end of file
diff --git a/redbot/cogs/audio/utils.py b/redbot/cogs/audio/utils.py
index b4cffcb02..e44abab2e 100644
--- a/redbot/cogs/audio/utils.py
+++ b/redbot/cogs/audio/utils.py
@@ -213,7 +213,7 @@ async def clear_react(bot: Red, message: discord.Message, emoji: MutableMapping
def get_track_description(track) -> Optional[str]:
if track and getattr(track, "uri", None):
query = Query.process_input(track.uri)
- if query.is_local:
+ if query.is_local or "localtracks/" in track.uri:
if track.title != "Unknown title":
return f'**{escape(f"{track.author} - {track.title}")}**' + escape(
f"\n{query.to_string_user()} "
@@ -229,7 +229,7 @@ def get_track_description(track) -> Optional[str]:
def get_track_description_unformatted(track) -> Optional[str]:
if track and hasattr(track, "uri"):
query = Query.process_input(track.uri)
- if query.is_local:
+ if query.is_local or "localtracks/" in track.uri:
if track.title != "Unknown title":
return escape(f"{track.author} - {track.title}")
else:
From fdfbfe7b601d864cbe9fa3ad299eb149ebce7ef4 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 22:37:50 +0000
Subject: [PATCH 05/91] [3.2.3][Audio] Full fix for #3328 (#3355)
* Limit Playlists
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* [Audio] Hotfix an edge case where an attribute error can be raised (#3328)
* Limit Playlists
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Hotfix
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* flame's review
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Delete 3328.hotfix.1.rst
* lets be extra safe here
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3328.hotfix.2.rst | 1 +
redbot/cogs/audio/apis.py | 40 ++++++++++++++++-------------
redbot/cogs/audio/audio.py | 39 ++++++++++++++++------------
3 files changed, 46 insertions(+), 34 deletions(-)
create mode 100644 changelog.d/audio/3328.hotfix.2.rst
diff --git a/changelog.d/audio/3328.hotfix.2.rst b/changelog.d/audio/3328.hotfix.2.rst
new file mode 100644
index 000000000..039b506e0
--- /dev/null
+++ b/changelog.d/audio/3328.hotfix.2.rst
@@ -0,0 +1 @@
+Check data before it is inserted into the database to avoid corruption.
\ No newline at end of file
diff --git a/redbot/cogs/audio/apis.py b/redbot/cogs/audio/apis.py
index 87745d5ce..5bc359efc 100644
--- a/redbot/cogs/audio/apis.py
+++ b/redbot/cogs/audio/apis.py
@@ -746,13 +746,13 @@ class MusicCache:
(val, update) = await self.database.fetch_one("lavalink", "data", {"query": query})
if update:
val = None
- if val and not isinstance(val, str):
+ if val and isinstance(val, dict):
log.debug(f"Querying Local Database for {query}")
task = ("update", ("lavalink", {"query": query}))
self.append_task(ctx, *task)
else:
val = None
- if val and not forced:
+ if val and not forced and isinstance(val, dict):
data = val
data["query"] = query
results = LoadResult(data)
@@ -780,21 +780,25 @@ class MusicCache:
):
with contextlib.suppress(SQLError):
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
- task = (
- "insert",
- (
- "lavalink",
- [
- {
- "query": query,
- "data": json.dumps(results._raw),
- "last_updated": time_now,
- "last_fetched": time_now,
- }
- ],
- ),
- )
- self.append_task(ctx, *task)
+ data = json.dumps(results._raw)
+ if all(
+ k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
+ ):
+ task = (
+ "insert",
+ (
+ "lavalink",
+ [
+ {
+ "query": query,
+ "data": data,
+ "last_updated": time_now,
+ "last_fetched": time_now,
+ }
+ ],
+ ),
+ )
+ self.append_task(ctx, *task)
return results, called_api
async def run_tasks(self, ctx: Optional[commands.Context] = None, _id=None):
@@ -855,7 +859,7 @@ class MusicCache:
query_data["maxage"] = maxage_int
vals = await self.database.fetch_all("lavalink", "data", query_data)
- recently_played = [r.tracks for r in vals if r]
+ recently_played = [r.tracks for r in vals if r if isinstance(tracks, dict)]
if recently_played:
track = random.choice(recently_played)
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index f29da5c6d..356d23924 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -246,14 +246,19 @@ class Audio(commands.Cog):
uri = t.get("info", {}).get("uri")
if uri:
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
- database_entries.append(
- {
- "query": uri,
- "data": json.dumps(t),
- "last_updated": time_now,
- "last_fetched": time_now,
- }
- )
+ data = json.dumps(t)
+ if all(
+ k in data
+ for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
+ ):
+ database_entries.append(
+ {
+ "query": uri,
+ "data": data,
+ "last_updated": time_now,
+ "last_fetched": time_now,
+ }
+ )
await asyncio.sleep(0)
if guild_playlist:
all_playlist[str(guild_id)] = guild_playlist
@@ -5883,14 +5888,16 @@ class Audio(commands.Cog):
uri = t.get("info", {}).get("uri")
if uri:
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
- database_entries.append(
- {
- "query": uri,
- "data": json.dumps(t),
- "last_updated": time_now,
- "last_fetched": time_now,
- }
- )
+ data = json.dumps(t)
+ if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]):
+ database_entries.append(
+ {
+ "query": uri,
+ "data": data,
+ "last_updated": time_now,
+ "last_fetched": time_now,
+ }
+ )
if database_entries:
await self.music_cache.database.insert("lavalink", database_entries)
From 54711b2054e28e9c7d4334645e6cda8c744c3d04 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sun, 12 Jan 2020 23:41:29 +0100
Subject: [PATCH 06/91] [Docs] Update autostart guides to use `-O` flag (#3354)
* Update autostart_systemd.rst
* Update autostart_pm2.rst
---
docs/autostart_pm2.rst | 12 +++++++-----
docs/autostart_systemd.rst | 4 ++--
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/docs/autostart_pm2.rst b/docs/autostart_pm2.rst
index b7e28e38d..27300ec3b 100644
--- a/docs/autostart_pm2.rst
+++ b/docs/autostart_pm2.rst
@@ -17,18 +17,20 @@ Start by installing Node.JS and NPM via your favorite package distributor. From
After PM2 is installed, run the following command to enable your Red instance to be managed by PM2. Replace the brackets with the required information.
You can add additional Red based arguments after the instance, such as :code:`--dev`.
-:code:`pm2 start redbot --name "" --interpreter "" -- --no-prompt`
+.. code-block:: none
+
+ pm2 start redbot --name "" --interpreter "" --interpreter-args "-O" -- --no-prompt
.. code-block:: none
Arguments to replace.
- --name ""
+
A name to identify the bot within pm2, this is not your Red instance.
- --interpreter ""
- The location of your Python interpreter, to find out where that is use the following command:
- which python3.6
+
+ The location of your Python interpreter, to find out where that is use the following command inside activated venv:
+ which python
The name of your Red instance.
diff --git a/docs/autostart_systemd.rst b/docs/autostart_systemd.rst
index 29d404c44..06d5b141a 100644
--- a/docs/autostart_systemd.rst
+++ b/docs/autostart_systemd.rst
@@ -18,7 +18,7 @@ In order to create the service file, you will first need the location of your :c
# If you are using pyenv
pyenv shell
- which redbot
+ which python
Then create the new service file:
@@ -33,7 +33,7 @@ Paste the following and replace all instances of :code:`username` with the usern
After=multi-user.target
[Service]
- ExecStart=path %I --no-prompt
+ ExecStart=path -O -m redbot %I --no-prompt
User=username
Group=username
Type=idle
From ed76454ddb0ea3e1d0451111f7f5d20164511632 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 22:42:17 +0000
Subject: [PATCH 07/91] ... (#3350)
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3349.bugfix.1.rst | 1 +
redbot/cogs/audio/audio.py | 23 ++++++++++++-----------
2 files changed, 13 insertions(+), 11 deletions(-)
create mode 100644 changelog.d/audio/3349.bugfix.1.rst
diff --git a/changelog.d/audio/3349.bugfix.1.rst b/changelog.d/audio/3349.bugfix.1.rst
new file mode 100644
index 000000000..193d44958
--- /dev/null
+++ b/changelog.d/audio/3349.bugfix.1.rst
@@ -0,0 +1 @@
+Fixed a bug where ``[p]audioset dc`` didn't disconnect the bot.
\ No newline at end of file
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 356d23924..afd34253e 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -535,17 +535,18 @@ class Audio(commands.Cog):
player_check = await self._players_check()
await self._status_check(*player_check)
- if not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
- notify_channel = player.fetch("channel")
- if notify_channel:
- notify_channel = self.bot.get_channel(notify_channel)
- await self._embed_msg(notify_channel, title=_("Queue Ended."))
- elif not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and disconnect:
- self.bot.dispatch("red_audio_audio_disconnect", guild)
- await player.disconnect()
- if event_type == lavalink.LavalinkEvents.QUEUE_END and status:
- player_check = await self._players_check()
- await self._status_check(*player_check)
+ if event_type == lavalink.LavalinkEvents.QUEUE_END:
+ if not autoplay:
+ notify_channel = player.fetch("channel")
+ if notify_channel and notify:
+ notify_channel = self.bot.get_channel(notify_channel)
+ await self._embed_msg(notify_channel, title=_("Queue Ended."))
+ if disconnect:
+ self.bot.dispatch("red_audio_audio_disconnect", guild)
+ await player.disconnect()
+ if status:
+ player_check = await self._players_check()
+ await self._status_check(*player_check)
if event_type in [
lavalink.LavalinkEvents.TRACK_EXCEPTION,
From 8514dbe96ab798d2ec919e0c7b3b651b5bf86c82 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 22:42:59 +0000
Subject: [PATCH 08/91] [3.2.3][Docs]Only support venv and virtualenv users
(#3351)
* Limit Playlists
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* docs
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* jack
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* update pip
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* flame's review
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
docs/install_linux_mac.rst | 20 +++++---------------
docs/install_windows.rst | 24 ++++++++++--------------
docs/venv_guide.rst | 17 ++++++-----------
3 files changed, 21 insertions(+), 40 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index 1fe908070..b767c1592 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -256,7 +256,7 @@ Pyenv is now installed and your system should be configured to run Python 3.8.
Creating a Virtual Environment
------------------------------
-We **strongly** recommend installing Red into a virtual environment. Don't be scared, it's very
+We require installing Red into a virtual environment. Don't be scared, it's very
straightforward. See the section `installing-in-virtual-environment`.
.. _installing-red-linux-mac:
@@ -267,29 +267,19 @@ Installing Red
Choose one of the following commands to install Red.
-.. note::
-
- If you're not inside an activated virtual environment, include the ``--user`` flag with all
- ``python3.8 -m pip install`` commands, like this:
-
- .. code-block:: none
-
- python3.8 -m pip install --user -U setuptools wheel
- python3.8 -m pip install --user -U Red-DiscordBot
-
To install without additional config backend support:
.. code-block:: none
- python3.8 -m pip install -U setuptools wheel
- python3.8 -m pip install -U Red-DiscordBot
+ python -m pip install -U pip setuptools wheel
+ python -m pip install -U Red-DiscordBot
Or, to install with PostgreSQL support:
.. code-block:: none
- python3.8 -m pip install -U setuptools wheel
- python3.8 -m pip install -U Red-DiscordBot[postgres]
+ python -m pip install -U pip setuptools wheel
+ python -m pip install -U Red-DiscordBot[postgres]
--------------------------
diff --git a/docs/install_windows.rst b/docs/install_windows.rst
index 93e390863..36fcc11fa 100644
--- a/docs/install_windows.rst
+++ b/docs/install_windows.rst
@@ -64,6 +64,13 @@ Manually installing dependencies
.. _installing-red-windows:
+------------------------------
+Creating a Virtual Environment
+------------------------------
+
+We require installing Red into a virtual environment. Don't be scared, it's very
+straightforward. See the section `installing-in-virtual-environment`.
+
--------------
Installing Red
--------------
@@ -72,31 +79,20 @@ Installing Red
for the PATH changes to take effect.
1. Open a command prompt (open Start, search for "command prompt", then click it)
-2. Create and activate a virtual environment (strongly recommended), see the section `using-venv`
-3. Run **one** of the following commands, depending on what extras you want installed
-
- .. note::
-
- If you're not inside an activated virtual environment, use ``py -3.8`` in place of
- ``python``, and include the ``--user`` flag with all ``pip install`` commands, like this:
-
- .. code-block:: none
-
- py -3.8 -m pip install --user -U setuptools wheel
- py -3.8 -m pip install --user -U Red-DiscordBot
+2. Run **one** of the following set of commands, depending on what extras you want installed
* Normal installation:
.. code-block:: none
- python -m pip install -U setuptools wheel
+ python -m pip install -U pip setuptools wheel
python -m pip install -U Red-DiscordBot
* With PostgreSQL support:
.. code-block:: none
- python -m pip install -U setuptools wheel
+ python -m pip install -U pip setuptools wheel
python -m pip install -U Red-DiscordBot[postgres]
diff --git a/docs/venv_guide.rst b/docs/venv_guide.rst
index 1aa34b93d..dea83eee6 100644
--- a/docs/venv_guide.rst
+++ b/docs/venv_guide.rst
@@ -9,14 +9,9 @@ problems. Firstly, simply choose how you'd like to create your virtual environme
* :ref:`using-venv` (quick and easy, involves two commands)
* :ref:`using-pyenv-virtualenv` (recommended if you installed Python with pyenv)
-**Why Should I Use a Virtual Environment?**
-
-90% of the installation and setup issues raised in our support channels are resolved when the user
-creates a virtual environment.
-
**What Are Virtual Environments For?**
-Virtual environments allow you to isolate red's library dependencies, cog dependencies and python
+Virtual environments allow you to isolate Red's library dependencies, cog dependencies and python
binaries from the rest of your system. It also makes sure Red and its dependencies are installed to
a predictable location. It makes uninstalling Red as simple as removing a single folder, without
worrying about losing your data or other things on your system becoming broken.
@@ -31,18 +26,18 @@ python.
First, choose a directory where you would like to create your virtual environment. It's a good idea
to keep it in a location which is easy to type out the path to. From now, we'll call it
-``redenv``.
+``redenv`` and it will be located in your home directory.
~~~~~~~~~~~~~~~~~~~~~~~~
``venv`` on Linux or Mac
~~~~~~~~~~~~~~~~~~~~~~~~
Create your virtual environment with the following command::
- python3.8 -m venv redenv
+ python3.8 -m venv ~/redenv
And activate it with the following command::
- source redenv/bin/activate
+ source ~/redenv/bin/activate
.. important::
@@ -56,11 +51,11 @@ Continue reading `below `.
~~~~~~~~~~~~~~~~~~~
Create your virtual environment with the following command::
- py -3.8 -m venv redenv
+ py -3.8 -m venv %userprofile%\redenv
And activate it with the following command::
- redenv\Scripts\activate.bat
+ %userprofile%\redenv\Scripts\activate.bat
.. important::
From fe7770c833e5cf9c67f92daea54b3c97f99e1b93 Mon Sep 17 00:00:00 2001
From: Flame442 <34169552+Flame442@users.noreply.github.com>
Date: Sun, 12 Jan 2020 15:01:45 -0800
Subject: [PATCH 09/91] [Admin] Fixes hierarchy issues in [p]selfrole and
[p]selfroleset (#3331)
* More fixes
* Update admin.py
---
redbot/cogs/admin/admin.py | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py
index ae7412129..e85e302b4 100644
--- a/redbot/cogs/admin/admin.py
+++ b/redbot/cogs/admin/admin.py
@@ -116,12 +116,14 @@ class Admin(commands.Cog):
:param role:
:return:
"""
- return ctx.author.top_role > role
+ return ctx.author.top_role > role or ctx.author == ctx.guild.owner
- async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
+ async def _addrole(
+ self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
+ ):
if member is None:
member = ctx.author
- if not self.pass_user_hierarchy_check(ctx, role):
+ if check_user and not self.pass_user_hierarchy_check(ctx, role):
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
return
if not self.pass_hierarchy_check(ctx, role):
@@ -141,10 +143,12 @@ class Admin(commands.Cog):
)
)
- async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
+ async def _removerole(
+ self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
+ ):
if member is None:
member = ctx.author
- if not self.pass_user_hierarchy_check(ctx, role):
+ if check_user and not self.pass_user_hierarchy_check(ctx, role):
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
return
if not self.pass_hierarchy_check(ctx, role):
@@ -365,7 +369,7 @@ class Admin(commands.Cog):
NOTE: The role is case sensitive!
"""
# noinspection PyTypeChecker
- await self._addrole(ctx, ctx.author, selfrole)
+ await self._addrole(ctx, ctx.author, selfrole, check_user=False)
@selfrole.command(name="remove")
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
@@ -376,7 +380,7 @@ class Admin(commands.Cog):
NOTE: The role is case sensitive!
"""
# noinspection PyTypeChecker
- await self._removerole(ctx, ctx.author, selfrole)
+ await self._removerole(ctx, ctx.author, selfrole, check_user=False)
@selfrole.command(name="list")
async def selfrole_list(self, ctx: commands.Context):
@@ -406,6 +410,13 @@ class Admin(commands.Cog):
NOTE: The role is case sensitive!
"""
+ if not self.pass_user_hierarchy_check(ctx, role):
+ await ctx.send(
+ _(
+ "I cannot let you add {role.name} as a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
+ ).format(role=role)
+ )
+ return
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
if role.id not in curr_selfroles:
curr_selfroles.append(role.id)
@@ -421,6 +432,13 @@ class Admin(commands.Cog):
NOTE: The role is case sensitive!
"""
+ if not self.pass_user_hierarchy_check(ctx, role):
+ await ctx.send(
+ _(
+ "I cannot let you remove {role.name} from being a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
+ ).format(role=role)
+ )
+ return
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
curr_selfroles.remove(role.id)
From 9f027cc3e080e7825a0681f2610fc8588396098a Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 23:19:36 +0000
Subject: [PATCH 10/91] [3.2.3][Audio] Improved Playlist cooldowns (#3342)
* Improved Playlist cooldowns
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Improved Playlist cooldowns
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* formatting
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3342.enhance.1.rst | 2 ++
redbot/cogs/audio/audio.py | 51 ++++++++++++++++++----------
2 files changed, 36 insertions(+), 17 deletions(-)
create mode 100644 changelog.d/audio/3342.enhance.1.rst
diff --git a/changelog.d/audio/3342.enhance.1.rst b/changelog.d/audio/3342.enhance.1.rst
new file mode 100644
index 000000000..38cab120a
--- /dev/null
+++ b/changelog.d/audio/3342.enhance.1.rst
@@ -0,0 +1,2 @@
+Reduce some cooldowns on playlist commands and stop them triggering before command parsing.
+
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index afd34253e..e47317403 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -4150,8 +4150,8 @@ class Audio(commands.Cog):
else None,
)
- @commands.cooldown(1, 300, commands.BucketType.member)
- @playlist.command(name="copy", usage=" [args]")
+ @commands.cooldown(1, 150, commands.BucketType.member)
+ @playlist.command(name="copy", usage=" [args]", cooldown_after_parsing=True)
async def _playlist_copy(
self,
ctx: commands.Context,
@@ -4444,7 +4444,9 @@ class Audio(commands.Cog):
)
@commands.cooldown(1, 30, commands.BucketType.member)
- @playlist.command(name="dedupe", usage=" [args]")
+ @playlist.command(
+ name="dedupe", usage=" [args]", cooldown_after_parsing=True
+ )
async def _playlist_remdupe(
self,
ctx: commands.Context,
@@ -4577,9 +4579,13 @@ class Audio(commands.Cog):
)
@checks.is_owner()
- @playlist.command(name="download", usage=" [v2=False] [args]")
+ @playlist.command(
+ name="download",
+ usage=" [v2=False] [args]",
+ cooldown_after_parsing=True,
+ )
@commands.bot_has_permissions(attach_files=True)
- @commands.cooldown(1, 60, commands.BucketType.guild)
+ @commands.cooldown(1, 30, commands.BucketType.guild)
async def _playlist_download(
self,
ctx: commands.Context,
@@ -4721,8 +4727,10 @@ class Audio(commands.Cog):
await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt"))
to_write.close()
- @commands.cooldown(1, 20, commands.BucketType.member)
- @playlist.command(name="info", usage=" [args]")
+ @commands.cooldown(1, 10, commands.BucketType.member)
+ @playlist.command(
+ name="info", usage=" [args]", cooldown_after_parsing=True
+ )
async def _playlist_info(
self,
ctx: commands.Context,
@@ -4858,8 +4866,8 @@ class Audio(commands.Cog):
page_list.append(embed)
await menu(ctx, page_list, DEFAULT_CONTROLS)
- @commands.cooldown(1, 30, commands.BucketType.guild)
- @playlist.command(name="list", usage="[args]")
+ @commands.cooldown(1, 15, commands.BucketType.guild)
+ @playlist.command(name="list", usage="[args]", cooldown_after_parsing=True)
@commands.bot_has_permissions(add_reactions=True)
async def _playlist_list(self, ctx: commands.Context, *, scope_data: ScopeParser = None):
"""List saved playlists.
@@ -4982,8 +4990,8 @@ class Audio(commands.Cog):
)
return embed
- @playlist.command(name="queue", usage=" [args]")
- @commands.cooldown(1, 600, commands.BucketType.member)
+ @playlist.command(name="queue", usage=" [args]", cooldown_after_parsing=True)
+ @commands.cooldown(1, 300, commands.BucketType.member)
async def _playlist_queue(
self, ctx: commands.Context, playlist_name: str, *, scope_data: ScopeParser = None
):
@@ -5194,8 +5202,8 @@ class Audio(commands.Cog):
).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name),
)
- @playlist.command(name="save", usage=" [args]")
- @commands.cooldown(1, 120, commands.BucketType.member)
+ @playlist.command(name="save", usage=" [args]", cooldown_after_parsing=True)
+ @commands.cooldown(1, 60, commands.BucketType.member)
async def _playlist_save(
self,
ctx: commands.Context,
@@ -5288,8 +5296,13 @@ class Audio(commands.Cog):
else None,
)
- @commands.cooldown(1, 60, commands.BucketType.member)
- @playlist.command(name="start", aliases=["play"], usage=" [args]")
+ @commands.cooldown(1, 30, commands.BucketType.member)
+ @playlist.command(
+ name="start",
+ aliases=["play"],
+ usage=" [args]",
+ cooldown_after_parsing=True,
+ )
async def _playlist_start(
self,
ctx: commands.Context,
@@ -5457,7 +5470,9 @@ class Audio(commands.Cog):
return await ctx.invoke(self.play, query=playlist.url)
@commands.cooldown(1, 60, commands.BucketType.member)
- @playlist.command(name="update", usage=" [args]")
+ @playlist.command(
+ name="update", usage=" [args]", cooldown_after_parsing=True
+ )
async def _playlist_update(
self,
ctx: commands.Context,
@@ -5734,7 +5749,9 @@ class Audio(commands.Cog):
)
@commands.cooldown(1, 60, commands.BucketType.member)
- @playlist.command(name="rename", usage=" [args]")
+ @playlist.command(
+ name="rename", usage=" [args]", cooldown_after_parsing=True
+ )
async def _playlist_rename(
self,
ctx: commands.Context,
From a98497177474288f2a02f78795c3383ad258077e Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 23:20:31 +0000
Subject: [PATCH 11/91] [3.2.3][Audio] Fixes some Playlists strings (#3347)
* chore
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* chore
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3344.enhance.1.rst | 1 +
redbot/cogs/audio/audio.py | 4 ++--
redbot/cogs/audio/utils.py | 6 +++---
3 files changed, 6 insertions(+), 5 deletions(-)
create mode 100644 changelog.d/audio/3344.enhance.1.rst
diff --git a/changelog.d/audio/3344.enhance.1.rst b/changelog.d/audio/3344.enhance.1.rst
new file mode 100644
index 000000000..124a7b894
--- /dev/null
+++ b/changelog.d/audio/3344.enhance.1.rst
@@ -0,0 +1 @@
+Fixes the messages for playlists.
\ No newline at end of file
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index e47317403..67607f090 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -4290,8 +4290,8 @@ class Audio(commands.Cog):
).format(
name=from_playlist.name,
from_id=from_playlist.id,
- from_scope=humanize_scope(from_scope, ctx=from_scope_name, the=True),
- to_scope=humanize_scope(to_scope, ctx=to_scope_name, the=True),
+ from_scope=humanize_scope(from_scope, ctx=from_scope_name),
+ to_scope=humanize_scope(to_scope, ctx=to_scope_name),
to_id=to_playlist.id,
),
)
diff --git a/redbot/cogs/audio/utils.py b/redbot/cogs/audio/utils.py
index e44abab2e..2886f4996 100644
--- a/redbot/cogs/audio/utils.py
+++ b/redbot/cogs/audio/utils.py
@@ -521,8 +521,8 @@ class PlaylistScope(Enum):
def humanize_scope(scope, ctx=None, the=None):
if scope == PlaylistScope.GLOBAL.value:
- return _("the ") if the else "" + _("Global")
+ return (_("the ") if the else "") + _("Global")
elif scope == PlaylistScope.GUILD.value:
- return ctx.name if ctx else _("the ") if the else "" + _("Server")
+ return ctx.name if ctx else (_("the ") if the else "") + _("Server")
elif scope == PlaylistScope.USER.value:
- return str(ctx) if ctx else _("the ") if the else "" + _("User")
+ return str(ctx) if ctx else (_("the ") if the else "") + _("User")
From cb49c5d4200d691c6a66256fb16066eb5fe95547 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 13 Jan 2020 00:21:00 +0100
Subject: [PATCH 12/91] [Downloader] Improve partial-uninstall message in
`[p]cog uninstall` (#3343)
* Update downloader.py
* Let's use more of Flame's suggestions.
Co-authored-by: Flame442 <34169552+Flame442@users.noreply.github.com>
Co-authored-by: Flame442 <34169552+Flame442@users.noreply.github.com>
---
redbot/cogs/downloader/downloader.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index d4da0d09e..f770f292f 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -748,11 +748,15 @@ class Downloader(commands.Cog):
message += _("Successfully uninstalled cogs: ") + humanize_list(uninstalled_cogs)
if failed_cogs:
message += (
- _("\nThese cog were installed but can no longer be located: ")
+ _(
+ "\nDownloader has removed these cogs from the installed cogs list"
+ " but it wasn't able to find their files: "
+ )
+ humanize_list(tuple(map(inline, failed_cogs)))
+ _(
- "\nYou may need to remove their files manually if they are still usable."
- " Also make sure you've unloaded those cogs with `{prefix}unload {cogs}`."
+ "\nThey were most likely removed without using `{prefix}cog uninstall`.\n"
+ "You may need to remove those files manually if the cogs are still usable."
+ " If so, ensure the cogs have been unloaded with `{prefix}unload {cogs}`."
).format(prefix=ctx.prefix, cogs=" ".join(failed_cogs))
)
await ctx.send(message)
From 5eb4bda600badd5f6189588f620fefe34d2ec261 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 13 Jan 2020 00:25:01 +0100
Subject: [PATCH 13/91] Update install_linux_mac.rst (#3336)
---
docs/install_linux_mac.rst | 44 ++++++++++++++++++++++++++------------
1 file changed, 30 insertions(+), 14 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index b767c1592..04abf2bb0 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -188,28 +188,44 @@ with zypper:
.. _install-ubuntu:
-~~~~~~
-Ubuntu
-~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Ubuntu LTS versions (18.04 and 16.04)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. note:: **Ubuntu Python Availability**
-
- We recommend using the deadsnakes ppa to ensure up to date python availability.
-
- .. code-block:: none
-
- sudo apt update
- sudo apt install software-properties-common
- sudo add-apt-repository ppa:deadsnakes/ppa
-
-Install the pre-requirements with apt:
+We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
.. code-block:: none
sudo apt update
+ sudo apt install software-properties-common
+ sudo add-apt-repository ppa:deadsnakes/ppa
+
+Now install the pre-requirements with apt:
+
+.. code-block:: none
+
sudo apt -y install python3.8 python3.8-dev python3.8-venv python3-pip git default-jre-headless \
build-essential
+.. _install-ubuntu-non-lts:
+
+~~~~~~~~~~~~~~~~~~~~~~~
+Ubuntu non-LTS versions
+~~~~~~~~~~~~~~~~~~~~~~~
+
+We recommend installing pyenv as a method of installing non-native versions of python on
+non-LTS versions of Ubuntu. This guide will tell you how. First, run the following commands:
+
+.. code-block:: none
+
+ sudo apt update
+ sudo apt -y install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
+ libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev \
+ libxmlsec1-dev libffi-dev liblzma-dev libgdbm-dev uuid-dev python3-openssl git openjdk-11-jre
+ CXX=/usr/bin/g++
+
+Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+
.. _install-python-pyenv:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 81b6d5bb93d8de611f80f117477fe0bc6d0d84ec Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 23:35:23 +0000
Subject: [PATCH 14/91] why the hell is this here (#3357)
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
redbot/cogs/audio/databases.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/redbot/cogs/audio/databases.py b/redbot/cogs/audio/databases.py
index ee136c222..6951a91b7 100644
--- a/redbot/cogs/audio/databases.py
+++ b/redbot/cogs/audio/databases.py
@@ -91,8 +91,6 @@ class CacheFetchResult:
k in self.query for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
):
self.query = json.loads(self.query)
- else:
- self.query = None
@dataclass
From 7bdd177713a6720cfa21e82a03d4cf8da4477343 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 12 Jan 2020 23:35:49 +0000
Subject: [PATCH 15/91] [3.2.3][Audio] Correct an unsupported LoadType (#3337)
* Limit Playlists
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* logging improvements
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* logging improvements
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* sigh
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* chore
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
changelog.d/audio/3337.misc.1.rst | 1 +
changelog.d/audio/3337.misc.2.rst | 1 +
redbot/cogs/audio/apis.py | 4 ++++
redbot/cogs/audio/audio.py | 4 ++--
redbot/cogs/audio/audio_dataclasses.py | 4 ++++
5 files changed, 12 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/audio/3337.misc.1.rst
create mode 100644 changelog.d/audio/3337.misc.2.rst
diff --git a/changelog.d/audio/3337.misc.1.rst b/changelog.d/audio/3337.misc.1.rst
new file mode 100644
index 000000000..f39071842
--- /dev/null
+++ b/changelog.d/audio/3337.misc.1.rst
@@ -0,0 +1 @@
+Removed a duplication of track search prefixes.
\ No newline at end of file
diff --git a/changelog.d/audio/3337.misc.2.rst b/changelog.d/audio/3337.misc.2.rst
new file mode 100644
index 000000000..25985f91d
--- /dev/null
+++ b/changelog.d/audio/3337.misc.2.rst
@@ -0,0 +1 @@
+Changed and handled the `V2_COMPACT` LoadType to use the correct `V2_COMPAT` type.
\ No newline at end of file
diff --git a/redbot/cogs/audio/apis.py b/redbot/cogs/audio/apis.py
index 5bc359efc..d949be375 100644
--- a/redbot/cogs/audio/apis.py
+++ b/redbot/cogs/audio/apis.py
@@ -755,6 +755,8 @@ class MusicCache:
if val and not forced and isinstance(val, dict):
data = val
data["query"] = query
+ if data.get("loadType") == "V2_COMPACT":
+ data["loadType"] = "V2_COMPAT"
results = LoadResult(data)
called_api = False
if results.has_error:
@@ -863,6 +865,8 @@ class MusicCache:
if recently_played:
track = random.choice(recently_played)
+ if track.get("loadType") == "V2_COMPACT":
+ track["loadType"] = "V2_COMPAT"
results = LoadResult(track)
tracks = list(results.tracks)
except Exception:
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 67607f090..bbbf1852c 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -245,7 +245,7 @@ class Audio(commands.Cog):
for t in tracks_in_playlist:
uri = t.get("info", {}).get("uri")
if uri:
- t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
+ t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
data = json.dumps(t)
if all(
k in data
@@ -5905,7 +5905,7 @@ class Audio(commands.Cog):
for t in track_list:
uri = t.get("info", {}).get("uri")
if uri:
- t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
+ t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
data = json.dumps(t)
if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]):
database_entries.append(
diff --git a/redbot/cogs/audio/audio_dataclasses.py b/redbot/cogs/audio/audio_dataclasses.py
index 7b1ca769e..d31cbbc9b 100644
--- a/redbot/cogs/audio/audio_dataclasses.py
+++ b/redbot/cogs/audio/audio_dataclasses.py
@@ -378,6 +378,10 @@ class Query:
if isinstance(query, str):
query = query.strip("<>")
+ while "ytsearch:" in query:
+ query = query.replace("ytsearch:", "")
+ while "scsearch:" in query:
+ query = query.replace("scsearch:", "")
elif isinstance(query, Query):
for key, val in kwargs.items():
From 088360ec51f05f02fdb19d0ffcef42581c276c56 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 13 Jan 2020 02:26:01 +0100
Subject: [PATCH 16/91] Make Red shutdown when resetting token (#3358)
* Update __main__.py
* Update __main__.py
---
redbot/__main__.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/redbot/__main__.py b/redbot/__main__.py
index 0f0d2dacf..66b64cc57 100644
--- a/redbot/__main__.py
+++ b/redbot/__main__.py
@@ -16,6 +16,7 @@ import sys
from argparse import Namespace
from copy import deepcopy
from pathlib import Path
+from typing import NoReturn
import discord
@@ -287,7 +288,7 @@ def handle_edit(cli_flags: Namespace):
sys.exit(0)
-async def run_bot(red: Red, cli_flags: Namespace):
+async def run_bot(red: Red, cli_flags: Namespace) -> NoReturn:
driver_cls = drivers.get_driver_class()
@@ -334,6 +335,7 @@ async def run_bot(red: Red, cli_flags: Namespace):
sys.exit(0)
try:
await red.start(token, bot=True, cli_flags=cli_flags)
+ # This raises SystemExit in normal use at close
except discord.LoginFailure:
log.critical("This token doesn't seem to be valid.")
db_token = await red._config.token()
@@ -341,6 +343,8 @@ async def run_bot(red: Red, cli_flags: Namespace):
if confirm("\nDo you want to reset the token?"):
await red._config.token.set("")
print("Token has been reset.")
+ sys.exit(0)
+ sys.exit(1)
def handle_early_exit_flags(cli_flags: Namespace):
From ab2e87a8fb44a901a0693c94c2aec6a4fdf1dc5d Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 13 Jan 2020 09:46:05 -0500
Subject: [PATCH 17/91] Start making use of typehints for devs (#3335)
* Start making use of typehints for devs
* changelog
---
changelog.d/3335.enhance.rst | 1 +
redbot/py.typed | 0
setup.cfg | 1 +
3 files changed, 2 insertions(+)
create mode 100644 changelog.d/3335.enhance.rst
create mode 100644 redbot/py.typed
diff --git a/changelog.d/3335.enhance.rst b/changelog.d/3335.enhance.rst
new file mode 100644
index 000000000..316decb0f
--- /dev/null
+++ b/changelog.d/3335.enhance.rst
@@ -0,0 +1 @@
+make typehints accessible to cog developers
diff --git a/redbot/py.typed b/redbot/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/setup.cfg b/setup.cfg
index 5040e8a1c..4fb7d934e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -127,5 +127,6 @@ include =
data/*
data/**/*
*.export
+ py.typed
redbot.core.drivers.postgres =
*.sql
From ef8b57a1d245b3f6ba6ef6ae454347d100625da5 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 13 Jan 2020 10:12:31 -0500
Subject: [PATCH 18/91] Add a command to set the bot description (#3340)
* description-command
* Cap the description length
* mmk
---
redbot/core/bot.py | 2 ++
redbot/core/commands/help.py | 6 +++---
redbot/core/core_commands.py | 26 ++++++++++++++++++++++++++
3 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 4264bd940..4b994a43d 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -76,6 +76,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
help__verify_checks=True,
help__verify_exists=False,
help__tagline="",
+ description="Red V3",
invite_public=False,
invite_perm=0,
disabled_commands=[],
@@ -400,6 +401,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
This should only be run once, prior to connecting to discord.
"""
await self._maybe_update_config()
+ self.description = await self._config.description()
init_global_checks(self)
init_events(self, cli_flags)
diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py
index 5d1a5eb9a..13ce22d6e 100644
--- a/redbot/core/commands/help.py
+++ b/redbot/core/commands/help.py
@@ -198,7 +198,7 @@ class RedHelpFormatter:
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
if description:
- emb["embed"]["title"] = f"*{description[:2044]}*"
+ emb["embed"]["title"] = f"*{description[:250]}*"
emb["footer"]["text"] = tagline
emb["embed"]["description"] = signature
@@ -209,7 +209,7 @@ class RedHelpFormatter:
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
if not value:
value = EMPTY_STRING
- field = EmbedField(name[:252], value[:1024], False)
+ field = EmbedField(name[:250], value[:1024], False)
emb["fields"].append(field)
if subcommands:
@@ -442,7 +442,7 @@ class RedHelpFormatter:
emb["footer"]["text"] = tagline
if description:
- emb["embed"]["title"] = f"*{description[:2044]}*"
+ emb["embed"]["title"] = f"*{description[:250]}*"
for cog_name, data in coms:
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index f89b3473a..cca4af689 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -873,6 +873,32 @@ class Core(commands.Cog, CoreLogic):
for page in pagify(settings):
await ctx.send(box(page))
+ @checks.is_owner()
+ @_set.command(name="description")
+ async def setdescription(self, ctx: commands.Context, *, description: str = ""):
+ """
+ Sets the bot's description.
+ Use without a description to reset.
+ This is shown in a few locations, including the help menu.
+
+ The default is "Red V3"
+ """
+ if not description:
+ await ctx.bot._config.description.clear()
+ ctx.bot.description = "Red V3"
+ await ctx.send(_("Description reset."))
+ elif len(description) > 250: # While the limit is 256, we bold it adding characters.
+ await ctx.send(
+ _(
+ "This description is too long to properly display. "
+ "Please try again with below 250 characters"
+ )
+ )
+ else:
+ await ctx.bot._config.description.set(description)
+ ctx.bot.description = description
+ await ctx.tick()
+
@_set.command()
@checks.guildowner()
@commands.guild_only()
From a7987a83fde3d7f6d519f3802d1af07aed66a00b Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 13 Jan 2020 11:37:49 -0500
Subject: [PATCH 19/91] Exit code handling (#3360)
* Exit code handling
* clear up a docstring
---
redbot/__main__.py | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/redbot/__main__.py b/redbot/__main__.py
index 66b64cc57..77c01ba52 100644
--- a/redbot/__main__.py
+++ b/redbot/__main__.py
@@ -288,7 +288,18 @@ def handle_edit(cli_flags: Namespace):
sys.exit(0)
-async def run_bot(red: Red, cli_flags: Namespace) -> NoReturn:
+async def run_bot(red: Red, cli_flags: Namespace) -> None:
+ """
+ This runs the bot.
+
+ Any shutdown which is a result of not being able to log in needs to raise
+ a SystemExit exception.
+
+ If the bot starts normally, the bot should be left to handle the exit case.
+ It will raise SystemExit in a task, which will reach the event loop and
+ interrupt running forever, then trigger our cleanup process, and does not
+ need additional handling in this function.
+ """
driver_cls = drivers.get_driver_class()
@@ -335,7 +346,6 @@ async def run_bot(red: Red, cli_flags: Namespace) -> NoReturn:
sys.exit(0)
try:
await red.start(token, bot=True, cli_flags=cli_flags)
- # This raises SystemExit in normal use at close
except discord.LoginFailure:
log.critical("This token doesn't seem to be valid.")
db_token = await red._config.token()
@@ -344,7 +354,9 @@ async def run_bot(red: Red, cli_flags: Namespace) -> NoReturn:
await red._config.token.set("")
print("Token has been reset.")
sys.exit(0)
- sys.exit(1)
+ sys.exit(1)
+
+ return None
def handle_early_exit_flags(cli_flags: Namespace):
From 3c53b890405ef7044be80db2975a8a0de93f3466 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 13 Jan 2020 11:50:45 -0500
Subject: [PATCH 20/91] [Help] formatting additions (#3339)
* formatting additions
* I really need to redo this module later
* fix some casing
---
redbot/core/commands/commands.py | 44 +++++++++++++++++++++
redbot/core/commands/help.py | 66 +++++++++++++++++++-------------
2 files changed, 84 insertions(+), 26 deletions(-)
diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py
index 97649747c..fcd5c390e 100644
--- a/redbot/core/commands/commands.py
+++ b/redbot/core/commands/commands.py
@@ -4,6 +4,7 @@ This module contains extended classes and functions which are intended to
replace those from the `discord.ext.commands` module.
"""
import inspect
+import re
import weakref
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
@@ -57,6 +58,49 @@ class CogCommandMixin:
checks=getattr(decorated, "__requires_checks__", []),
)
+ def format_help_for_context(self, ctx: "Context") -> str:
+ """
+ This formats the help string based on values in context
+
+ The steps are (currently, roughly) the following:
+
+ - get the localized help
+ - substitute ``[p]`` with ``ctx.clean_prefix``
+ - substitute ``[botname]`` with ``ctx.me.display_name``
+
+ More steps may be added at a later time.
+
+ Cog creators may override this in their own command classes
+ as long as the method signature stays the same.
+
+ Parameters
+ ----------
+ ctx: Context
+
+ Returns
+ -------
+ str
+ Localized help with some formatting
+ """
+
+ help_str = self.help
+ if not help_str:
+ # Short circuit out on an empty help string
+ return help_str
+
+ formatting_pattern = re.compile(r"\[p\]|\[botname\]")
+
+ def replacement(m: re.Match) -> str:
+ s = m.group(0)
+ if s == "[p]":
+ return ctx.clean_prefix
+ if s == "[botname]":
+ return ctx.me.display_name
+ # We shouldnt get here:
+ return s
+
+ return formatting_pattern.sub(replacement, help_str)
+
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
"""Actively allow this command for the given model.
diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py
index 13ce22d6e..783ef1dc5 100644
--- a/redbot/core/commands/help.py
+++ b/redbot/core/commands/help.py
@@ -162,10 +162,10 @@ class RedHelpFormatter:
@staticmethod
def get_default_tagline(ctx: Context):
- return (
- f"Type {ctx.clean_prefix}help for more info on a command. "
- f"You can also type {ctx.clean_prefix}help for more info on a category."
- )
+ return T_(
+ "Type {ctx.clean_prefix}help for more info on a command. "
+ "You can also type {ctx.clean_prefix}help for more info on a category."
+ ).format(ctx=ctx)
async def format_command_help(self, ctx: Context, obj: commands.Command):
@@ -187,7 +187,9 @@ class RedHelpFormatter:
description = command.description or ""
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
- signature = f"`Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
+ signature = (
+ f"`{T_('Syntax')}: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
+ )
subcommands = None
if hasattr(command, "all_commands"):
@@ -203,10 +205,11 @@ class RedHelpFormatter:
emb["footer"]["text"] = tagline
emb["embed"]["description"] = signature
- if command.help:
- splitted = command.help.split("\n\n")
+ command_help = command.format_help_for_context(ctx)
+ if command_help:
+ splitted = command_help.split("\n\n")
name = splitted[0]
- value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
+ value = "\n\n".join(splitted[1:])
if not value:
value = EMPTY_STRING
field = EmbedField(name[:250], value[:1024], False)
@@ -225,9 +228,9 @@ class RedHelpFormatter:
)
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
if i == 0:
- title = "**__Subcommands:__**"
+ title = T_("**__Subcommands:__**")
else:
- title = "**__Subcommands:__** (continued)"
+ title = T_("**__Subcommands:__** (continued)")
field = EmbedField(title, page, False)
emb["fields"].append(field)
@@ -238,7 +241,7 @@ class RedHelpFormatter:
subtext = None
subtext_header = None
if subcommands:
- subtext_header = "Subcommands:"
+ subtext_header = T_("Subcommands:")
max_width = max(discord.utils._string_width(name) for name in subcommands.keys())
def width_maker(cmds):
@@ -261,7 +264,7 @@ class RedHelpFormatter:
(
description,
signature[1:-1],
- command.help.replace("[p]", ctx.clean_prefix),
+ command.format_help_for_context(ctx),
subtext_header,
subtext,
),
@@ -301,7 +304,10 @@ class RedHelpFormatter:
page_char_limit = await ctx.bot._config.help.page_char_limit()
page_char_limit = min(page_char_limit, 5500) # Just in case someone was manually...
- author_info = {"name": f"{ctx.me.display_name} Help Menu", "icon_url": ctx.me.avatar_url}
+ author_info = {
+ "name": f"{ctx.me.display_name} {T_('Help Menu')}",
+ "icon_url": ctx.me.avatar_url,
+ }
# Offset calculation here is for total embed size limit
# 20 accounts for# *Page {i} of {page_count}*
@@ -346,7 +352,9 @@ class RedHelpFormatter:
embed = discord.Embed(color=color, **embed_dict["embed"])
if page_count > 1:
- description = f"*Page {i} of {page_count}*\n{embed.description}"
+ description = T_(
+ "*Page {page_num} of {page_count}*\n{content_description}"
+ ).format(content_description=embed.description, page_num=i, page_count=page_count)
embed.description = description
embed.set_author(**author_info)
@@ -366,7 +374,7 @@ class RedHelpFormatter:
if not (coms or await ctx.bot._config.help.verify_exists()):
return
- description = obj.help
+ description = obj.format_help_for_context(ctx)
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
if await ctx.embed_requested():
@@ -376,7 +384,7 @@ class RedHelpFormatter:
if description:
splitted = description.split("\n\n")
name = splitted[0]
- value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
+ value = "\n\n".join(splitted[1:])
if not value:
value = EMPTY_STRING
field = EmbedField(name[:252], value[:1024], False)
@@ -395,9 +403,9 @@ class RedHelpFormatter:
)
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
if i == 0:
- title = "**__Commands:__**"
+ title = T_("**__Commands:__**")
else:
- title = "**__Commands:__** (continued)"
+ title = T_("**__Commands:__** (continued)")
field = EmbedField(title, page, False)
emb["fields"].append(field)
@@ -407,7 +415,7 @@ class RedHelpFormatter:
subtext = None
subtext_header = None
if coms:
- subtext_header = "Commands:"
+ subtext_header = T_("Commands:")
max_width = max(discord.utils._string_width(name) for name in coms.keys())
def width_maker(cmds):
@@ -449,7 +457,7 @@ class RedHelpFormatter:
if cog_name:
title = f"**__{cog_name}:__**"
else:
- title = f"**__No Category:__**"
+ title = f"**__{T_('No Category')}:__**"
def shorten_line(a_line: str) -> str:
if len(a_line) < 70: # embed max width needs to be lower
@@ -462,7 +470,7 @@ class RedHelpFormatter:
)
for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)):
- title = title if i < 1 else f"{title} (continued)"
+ title = title if i < 1 else f"{title} {T_('(continued)')}"
field = EmbedField(title, page, False)
emb["fields"].append(field)
@@ -478,7 +486,7 @@ class RedHelpFormatter:
names.extend(list(v.name for v in v.values()))
max_width = max(
- discord.utils._string_width((name or "No Category:")) for name in names
+ discord.utils._string_width((name or T_("No Category:"))) for name in names
)
def width_maker(cmds):
@@ -492,7 +500,7 @@ class RedHelpFormatter:
for cog_name, data in coms:
- title = f"{cog_name}:" if cog_name else "No Category:"
+ title = f"{cog_name}:" if cog_name else T_("No Category:")
to_join.append(title)
for name, doc, width in width_maker(sorted(data.items())):
@@ -543,7 +551,9 @@ class RedHelpFormatter:
if fuzzy_commands:
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
if use_embeds:
- ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
+ ret.set_author(
+ name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
+ )
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
ret.set_footer(text=tagline)
await ctx.send(embed=ret)
@@ -553,7 +563,9 @@ class RedHelpFormatter:
ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for)
if use_embeds:
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
- ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
+ ret.set_author(
+ name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
+ )
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
ret.set_footer(text=tagline)
await ctx.send(embed=ret)
@@ -569,7 +581,9 @@ class RedHelpFormatter:
)
if await ctx.embed_requested():
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
- ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url)
+ ret.set_author(
+ name=f"{ctx.me.display_name} {T_('Help Menu')}", icon_url=ctx.me.avatar_url
+ )
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
ret.set_footer(text=tagline)
await ctx.send(embed=ret)
From 90c0f76ae4fced4f0feae02953171ef6ba8fc0ef Mon Sep 17 00:00:00 2001
From: Dav <57032623+Dav-Git@users.noreply.github.com>
Date: Mon, 13 Jan 2020 23:57:39 +0100
Subject: [PATCH 21/91] [Warnings] Make [p]warnings usable on base of
permissions (#3327)
* new code
Added the admin check to warnings and removed the part where the user could check themselves.
Added new mywarns which replaces part of the old behaviour of warn
* Update warnings.py
* Create 2900.enhance.rst
* Fixed command name
Because appearently I can't remember a command for 10 seconds
* Commands in backticks
Put command names in changelog in double backticks after being advised to do so in discord
* made user not optional, and the other thing sinbad requested
* switched parts. magic
resolves #2900
---
changelog.d/warnings/2900.enhance.rst | 2 +
redbot/cogs/warnings/warnings.py | 62 ++++++++++++++++-----------
2 files changed, 40 insertions(+), 24 deletions(-)
create mode 100644 changelog.d/warnings/2900.enhance.rst
diff --git a/changelog.d/warnings/2900.enhance.rst b/changelog.d/warnings/2900.enhance.rst
new file mode 100644
index 000000000..1c6f7599c
--- /dev/null
+++ b/changelog.d/warnings/2900.enhance.rst
@@ -0,0 +1,2 @@
+``[p]warnings`` can now be used by users that have the permission to use it from the Permissions cog, in order to check another user's warnings.
+``[p]mywarnings`` can now be used by any user (instead of ``[p]warnings`` previously) to check their own warnings.
diff --git a/redbot/cogs/warnings/warnings.py b/redbot/cogs/warnings/warnings.py
index 515c1c13c..458f43ff9 100644
--- a/redbot/cogs/warnings/warnings.py
+++ b/redbot/cogs/warnings/warnings.py
@@ -12,7 +12,6 @@ from redbot.cogs.warnings.helpers import (
from redbot.core import Config, checks, commands, modlog
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
-from redbot.core.utils.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@@ -342,30 +341,16 @@ class Warnings(commands.Cog):
@commands.command()
@commands.guild_only()
- async def warnings(
- self, ctx: commands.Context, user: Optional[Union[discord.Member, int]] = None
- ):
- """List the warnings for the specified user.
+ @checks.admin()
+ async def warnings(self, ctx: commands.Context, user: Union[discord.Member, int]):
+ """List the warnings for the specified user."""
- Omit `` to see your own warnings.
-
- Note that showing warnings for users other than yourself requires
- appropriate permissions.
- """
- if user is None:
- user = ctx.author
- else:
- if not await is_admin_or_superior(self.bot, ctx.author):
- return await ctx.send(
- warning(_("You are not allowed to check warnings for other users!"))
- )
-
- try:
- userid: int = user.id
- except AttributeError:
- userid: int = user
- user = ctx.guild.get_member(userid)
- user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
+ try:
+ userid: int = user.id
+ except AttributeError:
+ userid: int = user
+ user = ctx.guild.get_member(userid)
+ user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
msg = ""
member_settings = self.config.member(user)
@@ -389,6 +374,35 @@ class Warnings(commands.Cog):
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
)
+ @commands.command()
+ @commands.guild_only()
+ async def mywarnings(self, ctx: commands.Context):
+ """List warnings for yourself."""
+
+ user = ctx.author
+
+ msg = ""
+ member_settings = self.config.member(user)
+ async with member_settings.warnings() as user_warnings:
+ if not user_warnings.keys(): # no warnings for the user
+ await ctx.send(_("You have no warnings!"))
+ else:
+ for key in user_warnings.keys():
+ mod_id = user_warnings[key]["mod"]
+ mod = ctx.bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
+ msg += _(
+ "{num_points} point warning {reason_name} issued by {user} for "
+ "{description}\n"
+ ).format(
+ num_points=user_warnings[key]["points"],
+ reason_name=key,
+ user=mod,
+ description=user_warnings[key]["description"],
+ )
+ await ctx.send_interactive(
+ pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
+ )
+
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
From b646c2fd98f80bfdd5a2bdf5abd60fe966bb5f3d Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Tue, 14 Jan 2020 17:54:44 +0100
Subject: [PATCH 22/91] [Docs] Add links to operating systems + minor
readability improvements (#3365)
* add operating systems links + some minor readability improvements
* meh, let's add this too, draper
---
docs/install_linux_mac.rst | 48 ++++++++++++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index 04abf2bb0..45c792f8d 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -25,6 +25,16 @@ The pre-requirements are:
We also recommend installing some basic compiler tools, in case our dependencies don't provide
pre-built "wheels" for your architecture.
+
+*****************
+Operating systems
+*****************
+
+.. contents::
+ :local:
+
+----
+
.. _install-arch:
~~~~~~~~~~
@@ -35,6 +45,10 @@ Arch Linux
sudo pacman -Syu python python-pip git jre-openjdk-headless base-devel
+Continue by `creating-venv-linux`.
+
+----
+
.. _install-centos:
.. _install-rhel:
@@ -51,6 +65,8 @@ CentOS and RHEL 7
Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+----
+
.. _install-debian-stretch:
~~~~~~~~~~~~~~
@@ -76,6 +92,8 @@ Debian Stretch. This guide will tell you how. First, run the following commands:
Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+----
+
.. _install-debian:
.. _install-raspbian:
@@ -96,6 +114,8 @@ Debian/Raspbian Buster. This guide will tell you how. First, run the following c
Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+----
+
.. _install-fedora:
~~~~~~~~~~~~
@@ -109,6 +129,10 @@ them with dnf:
sudo dnf -y install python38 git java-latest-openjdk-headless @development-tools
+Continue by `creating-venv-linux`.
+
+----
+
.. _install-mac:
~~~
@@ -135,6 +159,10 @@ one-by-one:
It's possible you will have network issues. If so, go in your Applications folder, inside it, go in
the Python 3.8 folder then double click ``Install certificates.command``.
+Continue by `creating-venv-linux`.
+
+----
+
.. _install-opensuse:
~~~~~~~~
@@ -175,6 +203,8 @@ Now, install pip with easy_install:
sudo /opt/python/bin/easy_install-3.8 pip
+Continue by `creating-venv-linux`.
+
openSUSE Tumbleweed
*******************
@@ -186,6 +216,10 @@ with zypper:
sudo zypper install python3-base python3-pip git-core java-12-openjdk-headless
sudo zypper install -t pattern devel_basis
+Continue by `creating-venv-linux`.
+
+----
+
.. _install-ubuntu:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -207,6 +241,10 @@ Now install the pre-requirements with apt:
sudo apt -y install python3.8 python3.8-dev python3.8-venv python3-pip git default-jre-headless \
build-essential
+Continue by `creating-venv-linux`.
+
+----
+
.. _install-ubuntu-non-lts:
~~~~~~~~~~~~~~~~~~~~~~~
@@ -226,11 +264,13 @@ non-LTS versions of Ubuntu. This guide will tell you how. First, run the followi
Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+----
+
.. _install-python-pyenv:
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+****************************
Installing Python with pyenv
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+****************************
.. note::
@@ -268,6 +308,10 @@ After that is finished, run:
Pyenv is now installed and your system should be configured to run Python 3.8.
+Continue by `creating-venv-linux`.
+
+.. _creating-venv-linux:
+
------------------------------
Creating a Virtual Environment
------------------------------
From 2be4080bc6c110d8aec6730af82e6acf80cdb73a Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Tue, 14 Jan 2020 18:52:18 +0100
Subject: [PATCH 23/91] stop messing with distutils's internal just to copy
directory (#3364)
---
redbot/cogs/downloader/installable.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/redbot/cogs/downloader/installable.py b/redbot/cogs/downloader/installable.py
index 20997bb25..bfe3d916d 100644
--- a/redbot/cogs/downloader/installable.py
+++ b/redbot/cogs/downloader/installable.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import json
-import distutils.dir_util
+import functools
import shutil
from enum import IntEnum
from pathlib import Path
@@ -127,15 +127,13 @@ class Installable(RepoJSONMixin):
if self._location.is_file():
copy_func = shutil.copy2
else:
- # clear copy_tree's cache to make sure missing directories are created (GH-2690)
- distutils.dir_util._path_created = {}
- copy_func = distutils.dir_util.copy_tree
+ copy_func = functools.partial(shutil.copytree, dirs_exist_ok=True)
# noinspection PyBroadException
try:
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
except: # noqa: E722
- log.exception("Error occurred when copying path: {}".format(self._location))
+ log.exception("Error occurred when copying path: %s", self._location)
return False
return True
From 79dcd22ff644d124319d25e47b8f4a6c1be4493e Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Tue, 14 Jan 2020 18:53:28 +0100
Subject: [PATCH 24/91] Update bank.py (#3366)
---
redbot/core/bank.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/redbot/core/bank.py b/redbot/core/bank.py
index 4379199d8..03dc8b8f6 100644
--- a/redbot/core/bank.py
+++ b/redbot/core/bank.py
@@ -838,9 +838,9 @@ async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
amount = int(amount)
max_bal = await get_max_balance(guild)
- if not (0 < amount <= max_bal):
+ if not (0 <= amount <= max_bal):
raise ValueError(
- "Amount must be greater than zero and less than {max}.".format(
+ "Amount must be greater than or equal zero and less than or equal {max}.".format(
max=humanize_number(max_bal, override_locale="en_US")
)
)
From a7f0e2b7c657a2368e096f90348c697d12a10e72 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Tue, 14 Jan 2020 17:42:40 -0500
Subject: [PATCH 25/91] Globally ensure send_messages for commands (#3361)
* wew
* typo fix, thanks Danny
---
redbot/core/global_checks.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/redbot/core/global_checks.py b/redbot/core/global_checks.py
index 1adbe76a4..47c264a04 100644
--- a/redbot/core/global_checks.py
+++ b/redbot/core/global_checks.py
@@ -4,7 +4,17 @@ from . import commands
def init_global_checks(bot):
@bot.check_once
- def actually_up(ctx):
+ def minimum_bot_perms(ctx) -> bool:
+ """
+ Too many 403, 401, and 429 Errors can cause bots to get global'd
+
+ It's reasonable to assume the below as a minimum amount of perms for
+ commands.
+ """
+ return ctx.channel.permissions_for(ctx.me).send_messages
+
+ @bot.check_once
+ def actually_up(ctx) -> bool:
"""
Uptime is set during the initial startup process.
If this hasn't been set, we should assume the bot isn't ready yet.
@@ -12,10 +22,10 @@ def init_global_checks(bot):
return ctx.bot.uptime is not None
@bot.check_once
- async def whiteblacklist_checks(ctx):
+ async def whiteblacklist_checks(ctx) -> bool:
return await ctx.bot.allowed_by_whitelist_blacklist(ctx.author)
@bot.check_once
- def bots(ctx):
+ def bots(ctx) -> bool:
"""Check the user is not another bot."""
return not ctx.author.bot
From d8199201a5acc9dca7174cf443dab0bfa44180dc Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Wed, 15 Jan 2020 04:14:20 +0100
Subject: [PATCH 26/91] [Changelog] Clarify breaking change related to
`setup()` function (#3367)
* Update changelog_3_2_0.rst
* Update changelog_3_2_0.rst
* Update changelog_3_2_0.rst
---
docs/changelog_3_2_0.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/changelog_3_2_0.rst b/docs/changelog_3_2_0.rst
index 04d69d6f6..43874ad1c 100644
--- a/docs/changelog_3_2_0.rst
+++ b/docs/changelog_3_2_0.rst
@@ -49,7 +49,8 @@ Breaking Changes
- ``bot.get_mod_role_ids`` (`#2967 `_)
- Reserved some command names for internal Red use. These are available programatically as ``redbot.core.commands.RESERVED_COMMAND_NAMES``. (`#2973 `_)
- Removed ``bot._counter``, Made a few more attrs private (``cog_mgr``, ``main_dir``). (`#2976 `_)
-- ``bot.wait_until_ready`` should no longer be used during extension setup. (`#3073 `_)
+- Extension's ``setup()`` function should no longer assume that we are, or even will be connected to Discord.
+ This also means that cog creators should no longer use ``bot.wait_until_ready()`` inside it. (`#3073 `_)
- Removed the mongo driver. (`#3099 `_)
From 27e6f677e83304cb14447eab2a2048e07788367b Mon Sep 17 00:00:00 2001
From: flaree <31554168+flaree@users.noreply.github.com>
Date: Wed, 15 Jan 2020 03:15:55 +0000
Subject: [PATCH 27/91] [Docs] Modlog Example: action -> action_type (#3368)
* action -> action_type
* Changelog.
---
changelog.d/3368.docs.rst | 1 +
docs/framework_modlog.rst | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/3368.docs.rst
diff --git a/changelog.d/3368.docs.rst b/changelog.d/3368.docs.rst
new file mode 100644
index 000000000..d1f384e9b
--- /dev/null
+++ b/changelog.d/3368.docs.rst
@@ -0,0 +1 @@
+Update modlog documentation example to show "action_type" instead of "action".
\ No newline at end of file
diff --git a/docs/framework_modlog.rst b/docs/framework_modlog.rst
index 18a9cf453..39d1b7b48 100644
--- a/docs/framework_modlog.rst
+++ b/docs/framework_modlog.rst
@@ -25,7 +25,7 @@ Basic Usage
async def ban(self, ctx, user: discord.Member, reason: str = None):
await ctx.guild.ban(user)
case = await modlog.create_case(
- ctx.bot, ctx.guild, ctx.message.created_at, action="ban",
+ ctx.bot, ctx.guild, ctx.message.created_at, action_type="ban",
user=user, moderator=ctx.author, reason=reason
)
await ctx.send("Done. It was about time.")
From 60dc54b081c0735773fdeb482c2b76ef1648effa Mon Sep 17 00:00:00 2001
From: Michael H
Date: Wed, 15 Jan 2020 20:44:21 -0500
Subject: [PATCH 28/91] Allow pre_invoke to be used by 3rd party cogs safely.
(#3369)
* Okay, so there's a lot in this diff
* fix docstrings
* meh
* fix misleading var name
* meh...
* useful typehints
* Apply suggestions from code review
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* dep warn in locations suitable
* Fix this...
* Apply suggestions from code review
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
---
redbot/core/bot.py | 75 +++++++++++++++++++++++++++++++++++-
redbot/core/global_checks.py | 8 ----
2 files changed, 74 insertions(+), 9 deletions(-)
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 4b994a43d..32b69f27d 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -10,7 +10,19 @@ from datetime import datetime
from enum import IntEnum
from importlib.machinery import ModuleSpec
from pathlib import Path
-from typing import Optional, Union, List, Dict, NoReturn
+from typing import (
+ Optional,
+ Union,
+ List,
+ Dict,
+ NoReturn,
+ Set,
+ Coroutine,
+ TypeVar,
+ Callable,
+ Awaitable,
+ Any,
+)
from types import MappingProxyType
import discord
@@ -36,6 +48,9 @@ __all__ = ["RedBase", "Red", "ExitCodes"]
NotMessage = namedtuple("NotMessage", "guild")
+PreInvokeCoroutine = Callable[[commands.Context], Awaitable[Any]]
+T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
+
def _is_submodule(parent, child):
return parent == child or child.startswith(parent + ".")
@@ -150,6 +165,64 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
self._permissions_hooks: List[commands.CheckPredicate] = []
self._red_ready = asyncio.Event()
+ self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
+
+ @property
+ def _before_invoke(self): # DEP-WARN
+ return self._red_before_invoke_method
+
+ @_before_invoke.setter
+ def _before_invoke(self, val): # DEP-WARN
+ """Prevent this from being overwritten in super().__init__"""
+ pass
+
+ async def _red_before_invoke_method(self, ctx):
+ await self.wait_until_red_ready()
+ return_exceptions = isinstance(ctx.command, commands.commands._AlwaysAvailableCommand)
+ if self._red_before_invoke_objs:
+ await asyncio.gather(
+ *(coro(ctx) for coro in self._red_before_invoke_objs),
+ return_exceptions=return_exceptions,
+ )
+
+ def remove_before_invoke_hook(self, coro: PreInvokeCoroutine) -> None:
+ """
+ Functional method to remove a `before_invoke` hook.
+ """
+ self._red_before_invoke_objs.discard(coro)
+
+ def before_invoke(self, coro: T_BIC) -> T_BIC:
+ """
+ Overridden decorator method for Red's ``before_invoke`` behavior.
+
+ This can safely be used purely functionally as well.
+
+ 3rd party cogs should remove any hooks which they register at unload
+ using `remove_before_invoke_hook`
+
+ Below behavior shared with discord.py:
+
+ .. note::
+ The ``before_invoke`` hooks are
+ only called if all checks and argument parsing procedures pass
+ without error. If any check or argument parsing procedures fail
+ then the hooks are not called.
+
+ Parameters
+ ----------
+ coro: Callable[[commands.Context], Awaitable[Any]]
+ The coroutine to register as the pre-invoke hook.
+
+ Raises
+ ------
+ TypeError
+ The coroutine passed is not actually a coroutine.
+ """
+ if not asyncio.iscoroutinefunction(coro):
+ raise TypeError("The pre-invoke hook must be a coroutine.")
+
+ self._red_before_invoke_objs.add(coro)
+ return coro
@property
def cog_mgr(self) -> NoReturn:
diff --git a/redbot/core/global_checks.py b/redbot/core/global_checks.py
index 47c264a04..e88405df6 100644
--- a/redbot/core/global_checks.py
+++ b/redbot/core/global_checks.py
@@ -13,14 +13,6 @@ def init_global_checks(bot):
"""
return ctx.channel.permissions_for(ctx.me).send_messages
- @bot.check_once
- def actually_up(ctx) -> bool:
- """
- Uptime is set during the initial startup process.
- If this hasn't been set, we should assume the bot isn't ready yet.
- """
- return ctx.bot.uptime is not None
-
@bot.check_once
async def whiteblacklist_checks(ctx) -> bool:
return await ctx.bot.allowed_by_whitelist_blacklist(ctx.author)
From 29feab638a73ee5c6927e5745fb41c1b39d00bc5 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Thu, 16 Jan 2020 02:45:35 +0100
Subject: [PATCH 29/91] Update install_linux_mac.rst (#3371)
---
docs/install_linux_mac.rst | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index 45c792f8d..1b7dccfea 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -19,7 +19,7 @@ Please install the pre-requirements using the commands listed for your operating
The pre-requirements are:
- Python 3.8.1 or greater
- Pip 18.1 or greater
- - Git
+ - Git 2.11+
- Java Runtime Environment 11 or later (for audio support)
We also recommend installing some basic compiler tools, in case our dependencies don't provide
@@ -226,12 +226,18 @@ Continue by `creating-venv-linux`.
Ubuntu LTS versions (18.04 and 16.04)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
+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
+
+We recommend adding the ``deadsnakes`` ppa to install Python 3.8.1 or greater:
+
+.. code-block:: none
+
sudo add-apt-repository ppa:deadsnakes/ppa
Now install the pre-requirements with apt:
@@ -251,18 +257,25 @@ Continue by `creating-venv-linux`.
Ubuntu non-LTS versions
~~~~~~~~~~~~~~~~~~~~~~~
-We recommend installing pyenv as a method of installing non-native versions of python on
-non-LTS versions of Ubuntu. This guide will tell you how. First, run the following commands:
+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
+
+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:
+
+.. code-block:: none
+
sudo apt -y install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev \
libxmlsec1-dev libffi-dev liblzma-dev libgdbm-dev uuid-dev python3-openssl git openjdk-11-jre
CXX=/usr/bin/g++
-Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+And then complete the rest of the installation by `installing Python 3.8 with pyenv `.
----
From a1b95e5072a49745d1dc4f66d93fe50c1134c9a9 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Thu, 16 Jan 2020 02:54:23 +0100
Subject: [PATCH 30/91] enhance(downloader): log git commands that failed
(#3372)
---
redbot/cogs/downloader/errors.py | 8 +-
redbot/cogs/downloader/repo_manager.py | 162 ++++++++++++-------------
2 files changed, 81 insertions(+), 89 deletions(-)
diff --git a/redbot/cogs/downloader/errors.py b/redbot/cogs/downloader/errors.py
index efd31bedd..ee0c6ab2e 100644
--- a/redbot/cogs/downloader/errors.py
+++ b/redbot/cogs/downloader/errors.py
@@ -38,6 +38,10 @@ class GitException(DownloaderException):
Generic class for git exceptions.
"""
+ def __init__(self, message: str, git_command: str) -> None:
+ self.git_command = git_command
+ super().__init__(f"Git command failed: {git_command}\nError message: {message}")
+
class InvalidRepoName(DownloaderException):
"""
@@ -138,8 +142,8 @@ class AmbiguousRevision(GitException):
Thrown when specified revision is ambiguous.
"""
- def __init__(self, message: str, candidates: List[Candidate]) -> None:
- super().__init__(message)
+ def __init__(self, message: str, git_command: str, candidates: List[Candidate]) -> None:
+ super().__init__(message, git_command)
self.candidates = candidates
diff --git a/redbot/cogs/downloader/repo_manager.py b/redbot/cogs/downloader/repo_manager.py
index 9c28a7a76..fe786ef98 100644
--- a/redbot/cogs/downloader/repo_manager.py
+++ b/redbot/cogs/downloader/repo_manager.py
@@ -203,21 +203,20 @@ class Repo(RepoJSONMixin):
"""
valid_exit_codes = (0, 1)
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_IS_ANCESTOR,
- path=self.folder_path,
- maybe_ancestor_rev=maybe_ancestor_rev,
- descendant_rev=descendant_rev,
- ),
- valid_exit_codes=valid_exit_codes,
+ git_command = ProcessFormatter().format(
+ self.GIT_IS_ANCESTOR,
+ path=self.folder_path,
+ maybe_ancestor_rev=maybe_ancestor_rev,
+ descendant_rev=descendant_rev,
)
+ p = await self._run(git_command, valid_exit_codes=valid_exit_codes)
if p.returncode in valid_exit_codes:
return not bool(p.returncode)
raise errors.GitException(
f"Git failed to determine if commit {maybe_ancestor_rev}"
- f" is ancestor of {descendant_rev} for repo at path: {self.folder_path}"
+ f" is ancestor of {descendant_rev} for repo at path: {self.folder_path}",
+ git_command,
)
async def is_on_branch(self) -> bool:
@@ -253,15 +252,14 @@ class Repo(RepoJSONMixin):
"""
if new_rev is None:
new_rev = self.branch
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_DIFF_FILE_STATUS, path=self.folder_path, old_rev=old_rev, new_rev=new_rev
- )
+ git_command = ProcessFormatter().format(
+ self.GIT_DIFF_FILE_STATUS, path=self.folder_path, old_rev=old_rev, new_rev=new_rev
)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.GitDiffError(
- "Git diff failed for repo at path: {}".format(self.folder_path)
+ f"Git diff failed for repo at path: {self.folder_path}", git_command
)
stdout = p.stdout.strip(b"\t\n\x00 ").decode().split("\x00\t")
@@ -310,18 +308,17 @@ class Repo(RepoJSONMixin):
async with self.checkout(descendant_rev):
return discord.utils.get(self.available_modules, name=module_name)
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
- path=self.folder_path,
- descendant_rev=descendant_rev,
- module_name=module_name,
- )
+ git_command = ProcessFormatter().format(
+ self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
+ path=self.folder_path,
+ descendant_rev=descendant_rev,
+ module_name=module_name,
)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.GitException(
- "Git log failed for repo at path: {}".format(self.folder_path)
+ f"Git log failed for repo at path: {self.folder_path}", git_command
)
commit = p.stdout.decode().strip()
@@ -418,19 +415,18 @@ class Repo(RepoJSONMixin):
to get messages for.
:return: Git commit note log
"""
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_LOG,
- path=self.folder_path,
- old_rev=old_rev,
- relative_file_path=relative_file_path,
- )
+ git_command = ProcessFormatter().format(
+ self.GIT_LOG,
+ path=self.folder_path,
+ old_rev=old_rev,
+ relative_file_path=relative_file_path,
)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.GitException(
- "An exception occurred while executing git log on"
- " this repo: {}".format(self.folder_path)
+ f"An exception occurred while executing git log on this repo: {self.folder_path}",
+ git_command,
)
return p.stdout.decode().strip()
@@ -457,21 +453,24 @@ class Repo(RepoJSONMixin):
Full sha1 object name for provided revision.
"""
- p = await self._run(
- ProcessFormatter().format(self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev)
+ git_command = ProcessFormatter().format(
+ self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev
)
+ p = await self._run(git_command)
if p.returncode != 0:
stderr = p.stderr.decode().strip()
ambiguous_error = f"error: short SHA1 {rev} is ambiguous\nhint: The candidates are:\n"
if not stderr.startswith(ambiguous_error):
- raise errors.UnknownRevision(f"Revision {rev} cannot be found.")
+ raise errors.UnknownRevision(f"Revision {rev} cannot be found.", git_command)
candidates = []
for match in self.AMBIGUOUS_ERROR_REGEX.finditer(stderr, len(ambiguous_error)):
candidates.append(Candidate(match["rev"], match["type"], match["desc"]))
if candidates:
- raise errors.AmbiguousRevision(f"Short SHA1 {rev} is ambiguous.", candidates)
- raise errors.UnknownRevision(f"Revision {rev} cannot be found.")
+ raise errors.AmbiguousRevision(
+ f"Short SHA1 {rev} is ambiguous.", git_command, candidates
+ )
+ raise errors.UnknownRevision(f"Revision {rev} cannot be found.", git_command)
return p.stdout.decode().strip()
@@ -554,17 +553,14 @@ class Repo(RepoJSONMixin):
return
exists, __ = self._existing_git_repo()
if not exists:
- raise errors.MissingGitRepo(
- "A git repo does not exist at path: {}".format(self.folder_path)
- )
+ raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
- p = await self._run(
- ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
- )
+ git_command = ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.UnknownRevision(
- "Could not checkout to {}. This revision may not exist".format(rev)
+ f"Could not checkout to {rev}. This revision may not exist", git_command
)
await self._setup_repo()
@@ -619,25 +615,22 @@ class Repo(RepoJSONMixin):
"""
exists, path = self._existing_git_repo()
if exists:
- raise errors.ExistingGitRepo("A git repo already exists at path: {}".format(path))
+ raise errors.ExistingGitRepo(f"A git repo already exists at path: {path}")
if self.branch is not None:
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
- )
+ git_command = ProcessFormatter().format(
+ self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
)
else:
- p = await self._run(
- ProcessFormatter().format(
- self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
- )
+ git_command = ProcessFormatter().format(
+ self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
)
+ p = await self._run(git_command)
if p.returncode:
# Try cleaning up folder
shutil.rmtree(str(self.folder_path), ignore_errors=True)
- raise errors.CloningError("Error when running git clone.")
+ raise errors.CloningError("Error when running git clone.", git_command)
if self.branch is None:
self.branch = await self.current_branch()
@@ -657,17 +650,14 @@ class Repo(RepoJSONMixin):
"""
exists, __ = self._existing_git_repo()
if not exists:
- raise errors.MissingGitRepo(
- "A git repo does not exist at path: {}".format(self.folder_path)
- )
+ raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
- p = await self._run(
- ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
- )
+ git_command = ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.GitException(
- "Could not determine current branch at path: {}".format(self.folder_path)
+ f"Could not determine current branch at path: {self.folder_path}", git_command
)
return p.stdout.decode().strip()
@@ -683,16 +673,13 @@ class Repo(RepoJSONMixin):
"""
exists, __ = self._existing_git_repo()
if not exists:
- raise errors.MissingGitRepo(
- "A git repo does not exist at path: {}".format(self.folder_path)
- )
+ raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
- p = await self._run(
- ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
- )
+ git_command = ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
+ p = await self._run(git_command)
if p.returncode != 0:
- raise errors.CurrentHashError("Unable to determine commit hash.")
+ raise errors.CurrentHashError("Unable to determine commit hash.", git_command)
return p.stdout.decode().strip()
@@ -715,16 +702,15 @@ class Repo(RepoJSONMixin):
exists, __ = self._existing_git_repo()
if not exists:
- raise errors.MissingGitRepo(
- "A git repo does not exist at path: {}".format(self.folder_path)
- )
+ raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
- p = await self._run(
- ProcessFormatter().format(self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch)
+ git_command = ProcessFormatter().format(
+ self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch
)
+ p = await self._run(git_command)
if p.returncode != 0:
- raise errors.CurrentHashError("Unable to determine latest commit hash.")
+ raise errors.CurrentHashError("Unable to determine latest commit hash.", git_command)
return p.stdout.decode().strip()
@@ -751,10 +737,11 @@ class Repo(RepoJSONMixin):
if folder is None:
folder = self.folder_path
- p = await self._run(ProcessFormatter().format(Repo.GIT_DISCOVER_REMOTE_URL, path=folder))
+ git_command = ProcessFormatter().format(Repo.GIT_DISCOVER_REMOTE_URL, path=folder)
+ p = await self._run(git_command)
if p.returncode != 0:
- raise errors.NoRemoteURL("Unable to discover a repo URL.")
+ raise errors.NoRemoteURL("Unable to discover a repo URL.", git_command)
return p.stdout.decode().strip()
@@ -773,19 +760,18 @@ class Repo(RepoJSONMixin):
await self.checkout(branch)
exists, __ = self._existing_git_repo()
if not exists:
- raise errors.MissingGitRepo(
- "A git repo does not exist at path: {}".format(self.folder_path)
- )
+ raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
- p = await self._run(
- ProcessFormatter().format(self.GIT_HARD_RESET, path=self.folder_path, branch=branch)
+ git_command = ProcessFormatter().format(
+ self.GIT_HARD_RESET, path=self.folder_path, branch=branch
)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.HardResetError(
- "Some error occurred when trying to"
- " execute a hard reset on the repo at"
- " the following path: {}".format(self.folder_path)
+ "Some error occurred when trying to execute a hard reset on the repo at"
+ f" the following path: {self.folder_path}",
+ git_command,
)
async def update(self) -> Tuple[str, str]:
@@ -795,7 +781,7 @@ class Repo(RepoJSONMixin):
-------
`tuple` of `str`
:py:code`(old commit hash, new commit hash)`
-
+
Raises
-------
`UpdateError` - if git pull results with non-zero exit code
@@ -804,12 +790,14 @@ class Repo(RepoJSONMixin):
await self.hard_reset()
- p = await self._run(ProcessFormatter().format(self.GIT_PULL, path=self.folder_path))
+ git_command = ProcessFormatter().format(self.GIT_PULL, path=self.folder_path)
+ p = await self._run(git_command)
if p.returncode != 0:
raise errors.UpdateError(
"Git pull returned a non zero exit code"
- " for the repo located at path: {}".format(self.folder_path)
+ f" for the repo located at path: {self.folder_path}",
+ git_command,
)
await self._setup_repo()
@@ -1114,7 +1102,7 @@ class RepoManager:
"""
repo = self.get_repo(name)
if repo is None:
- raise errors.MissingGitRepo("There is no repo with the name {}".format(name))
+ raise errors.MissingGitRepo(f"There is no repo with the name {name}")
safe_delete(repo.folder_path)
From d6d14617d21b5d24ef95bd2f4a9b23ba014de194 Mon Sep 17 00:00:00 2001
From: Redjumpman
Date: Thu, 16 Jan 2020 13:18:20 -0500
Subject: [PATCH 31/91] Update __init__.py (#3381)
Removed redundant check.
---
redbot/__init__.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 13d14ff05..8b08c74f4 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -181,9 +181,7 @@ class VersionInfo:
def _update_event_loop_policy():
- if _sys.platform == "win32":
- _asyncio.set_event_loop_policy(_asyncio.WindowsProactorEventLoopPolicy())
- elif _sys.implementation.name == "cpython":
+ if _sys.implementation.name == "cpython":
# Let's not force this dependency, uvloop is much faster on cpython
try:
import uvloop as _uvloop
From 85438e7454c2d4b1331ec6efb7750b2a407704c8 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Thu, 16 Jan 2020 19:09:09 -0500
Subject: [PATCH 32/91] [Setup] Fix data deletion. (#3384)
* I'm ready to :knife: some of these entrypoints
* If we're gonna teardown here, may as well do it right
---
redbot/setup.py | 25 ++++++++++++++-----------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/redbot/setup.py b/redbot/setup.py
index c052cb64c..2be440acc 100644
--- a/redbot/setup.py
+++ b/redbot/setup.py
@@ -253,20 +253,23 @@ async def remove_instance(
backend = get_current_backend(instance)
driver_cls = drivers.get_driver_class(backend)
+ await driver_cls.initialize(**data_manager.storage_details())
+ try:
+ if delete_data is True:
+ await driver_cls.delete_all_data(interactive=interactive, drop_db=drop_db)
- if delete_data is True:
- await driver_cls.delete_all_data(interactive=interactive, drop_db=drop_db)
+ if interactive is True and remove_datapath is None:
+ remove_datapath = click.confirm(
+ "Would you like to delete the instance's entire datapath?", default=False
+ )
- if interactive is True and remove_datapath is None:
- remove_datapath = click.confirm(
- "Would you like to delete the instance's entire datapath?", default=False
- )
+ if remove_datapath is True:
+ data_path = data_manager.core_data_path().parent
+ safe_delete(data_path)
- if remove_datapath is True:
- data_path = data_manager.core_data_path().parent
- safe_delete(data_path)
-
- save_config(instance, {}, remove=True)
+ save_config(instance, {}, remove=True)
+ finally:
+ await driver_cls.teardown()
print("The instance {} has been removed\n".format(instance))
From a203fe34cf01115533a07898e1636c2f542d318f Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Fri, 17 Jan 2020 10:43:37 +0100
Subject: [PATCH 33/91] [Typo Fix] Permissions (#3390)
* [Typo Fix] Permissions
* Changelog file
---
changelog.d/3390.misc.rst | 1 +
redbot/core/commands/requires.py | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/3390.misc.rst
diff --git a/changelog.d/3390.misc.rst b/changelog.d/3390.misc.rst
new file mode 100644
index 000000000..9167a63c8
--- /dev/null
+++ b/changelog.d/3390.misc.rst
@@ -0,0 +1 @@
+Fixes a typo in redbot/core/commands/requires.py.
\ No newline at end of file
diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py
index b6f188de1..f3614e0fa 100644
--- a/redbot/core/commands/requires.py
+++ b/redbot/core/commands/requires.py
@@ -95,8 +95,8 @@ class PrivilegeLevel(enum.IntEnum):
"""Enumeration for special privileges."""
# Maintainer Note: do NOT re-order these.
- # Each privelege level also implies access to the ones before it.
- # Inserting new privelege levels at a later point is fine if that is considered.
+ # Each privilege level also implies access to the ones before it.
+ # Inserting new privilege levels at a later point is fine if that is considered.
NONE = enum.auto()
"""No special privilege level."""
From 67fbcb1b4a2709474fc578509ae0e236b183b861 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Fri, 17 Jan 2020 10:44:10 +0100
Subject: [PATCH 34/91] enhance(downloader): pagify any output that might be
too long (#3388)
---
redbot/cogs/downloader/downloader.py | 41 ++++++++++++++++------------
1 file changed, 23 insertions(+), 18 deletions(-)
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index f770f292f..eb063157c 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -418,6 +418,11 @@ class Downloader(commands.Cog):
elif target.is_file():
os.remove(str(target))
+ @staticmethod
+ async def send_pagified(target: discord.abc.Messageable, content: str) -> None:
+ for page in pagify(content):
+ await target.send(page)
+
@commands.command()
@checks.is_owner()
async def pipinstall(self, ctx: commands.Context, *deps: str) -> None:
@@ -550,7 +555,7 @@ class Downloader(commands.Cog):
if failed:
message += "\n" + self.format_failed_repos(failed)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
@commands.group()
@checks.is_owner()
@@ -596,12 +601,13 @@ class Downloader(commands.Cog):
tuple(map(inline, libnames))
)
if message:
- await ctx.send(
+ await self.send_pagified(
+ ctx,
_(
"Cog requirements and shared libraries for all installed cogs"
" have been reinstalled but there were some errors:\n"
)
- + message
+ + message,
)
else:
await ctx.send(
@@ -643,8 +649,7 @@ class Downloader(commands.Cog):
f"**{candidate.object_type} {candidate.rev}**"
f" - {candidate.description}\n"
)
- for page in pagify(msg):
- await ctx.send(msg)
+ await self.send_pagified(ctx, msg)
return
except errors.UnknownRevision:
await ctx.send(
@@ -658,14 +663,14 @@ class Downloader(commands.Cog):
async with repo.checkout(commit, exit_to_rev=repo.branch):
cogs, message = await self._filter_incorrect_cogs_by_names(repo, cog_names)
if not cogs:
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
return
failed_reqs = await self._install_requirements(cogs)
if failed_reqs:
message += _("\nFailed to install requirements: ") + humanize_list(
tuple(map(inline, failed_reqs))
)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
return
installed_cogs, failed_cogs = await self._install_cogs(cogs)
@@ -711,7 +716,7 @@ class Downloader(commands.Cog):
+ message
)
# "---" added to separate cog install messages from Downloader's message
- await ctx.send(f"{message}{deprecation_notice}\n---")
+ await self.send_pagified(ctx, f"{message}{deprecation_notice}\n---")
for cog in installed_cogs:
if cog.install_msg:
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
@@ -759,7 +764,7 @@ class Downloader(commands.Cog):
" If so, ensure the cogs have been unloaded with `{prefix}unload {cogs}`."
).format(prefix=ctx.prefix, cogs=" ".join(failed_cogs))
)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
@cog.command(name="pin", usage="")
async def _cog_pin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
@@ -782,7 +787,7 @@ class Downloader(commands.Cog):
message += _("Pinned cogs: ") + humanize_list(cognames)
if already_pinned:
message += _("\nThese cogs were already pinned: ") + humanize_list(already_pinned)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
@cog.command(name="unpin", usage="")
async def _cog_unpin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
@@ -805,7 +810,7 @@ class Downloader(commands.Cog):
message += _("Unpinned cogs: ") + humanize_list(cognames)
if not_pinned:
message += _("\nThese cogs weren't pinned: ") + humanize_list(not_pinned)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
@cog.command(name="checkforupdates")
async def _cog_checkforupdates(self, ctx: commands.Context) -> None:
@@ -837,7 +842,7 @@ class Downloader(commands.Cog):
if failed:
message += "\n" + self.format_failed_repos(failed)
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
@cog.command(name="update")
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
@@ -873,7 +878,6 @@ class Downloader(commands.Cog):
rev: Optional[str] = None,
cogs: Optional[List[InstalledModule]] = None,
) -> None:
- message = ""
failed_repos = set()
updates_available = set()
@@ -886,7 +890,7 @@ class Downloader(commands.Cog):
await repo.update()
except errors.UpdateError:
message = self.format_failed_repos([repo.name])
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
return
try:
@@ -900,11 +904,10 @@ class Downloader(commands.Cog):
f"**{candidate.object_type} {candidate.rev}**"
f" - {candidate.description}\n"
)
- for page in pagify(msg):
- await ctx.send(msg)
+ await self.send_pagified(ctx, msg)
return
except errors.UnknownRevision:
- message += _(
+ message = _(
"Error: there is no revision `{rev}` in repo `{repo.name}`"
).format(rev=rev, repo=repo)
await ctx.send(message)
@@ -921,6 +924,8 @@ class Downloader(commands.Cog):
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
cogs_to_check -= pinned_cogs
+
+ message = ""
if not cogs_to_check:
cogs_to_update = libs_to_update = ()
message += _("There were no cogs to check.")
@@ -976,7 +981,7 @@ class Downloader(commands.Cog):
if repos_with_libs:
message += DEPRECATION_NOTICE.format(repo_list=humanize_list(list(repos_with_libs)))
- await ctx.send(message)
+ await self.send_pagified(ctx, message)
if updates_available and updated_cognames:
await self._ask_for_cog_reload(ctx, updated_cognames)
From 48ccd9070c85baecdf475f06facb9c663157b8cc Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Fri, 17 Jan 2020 12:08:31 +0100
Subject: [PATCH 35/91] [Core] Adds server IDs to servers command. (#3393)
* [Core] Adds server ID to servers command.
* Changelog
---
changelog.d/3224.enhance.rst | 1 +
redbot/core/core_commands.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/3224.enhance.rst
diff --git a/changelog.d/3224.enhance.rst b/changelog.d/3224.enhance.rst
new file mode 100644
index 000000000..5dcacb085
--- /dev/null
+++ b/changelog.d/3224.enhance.rst
@@ -0,0 +1 @@
+Adds server IDs to servers command.
\ No newline at end of file
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index cca4af689..4a05cea70 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -563,7 +563,7 @@ class Core(commands.Cog, CoreLogic):
msg = ""
responses = []
for i, server in enumerate(guilds, 1):
- msg += "{}: {}\n".format(i, server.name)
+ msg += "{}: {} (`{}`)\n".format(i, server.name, server.id)
responses.append(str(i))
for page in pagify(msg, ["\n"]):
From 3d1e6eab0051b9c425b94ce9f633b1e013f20316 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Fri, 17 Jan 2020 21:30:29 +0000
Subject: [PATCH 36/91] [Audio] Add backticks to commands in docstrings, fix
GH-3140 (#3374)
* docstring change
* remove backticks
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Seems like i cant read
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* Rename 3140.enchance.1.rst to 3140.enhance.1.rst
---
changelog.d/audio/3140.enhance.1.rst | 1 +
redbot/cogs/audio/audio.py | 155 +++++++++++++--------------
2 files changed, 76 insertions(+), 80 deletions(-)
create mode 100644 changelog.d/audio/3140.enhance.1.rst
diff --git a/changelog.d/audio/3140.enhance.1.rst b/changelog.d/audio/3140.enhance.1.rst
new file mode 100644
index 000000000..81570e412
--- /dev/null
+++ b/changelog.d/audio/3140.enhance.1.rst
@@ -0,0 +1 @@
+Update the help strings for ``[p]audioset emptydisconnect``.
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index bbbf1852c..19ad5e744 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -696,7 +696,7 @@ class Audio(commands.Cog):
async def dc(self, ctx: commands.Context):
"""Toggle the bot auto-disconnecting when done playing.
- This setting takes precedence over [p]audioset emptydisconnect.
+ This setting takes precedence over `[p]audioset emptydisconnect`.
"""
disconnect = await self.config.guild(ctx.guild).disconnect()
@@ -1123,7 +1123,7 @@ class Audio(commands.Cog):
"""Set a playlist to auto-play songs from.
**Usage**:
- [p]audioset autoplay playlist_name_OR_id args
+ `[p]audioset autoplay playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -1146,9 +1146,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]audioset autoplay MyGuildPlaylist
- [p]audioset autoplay MyGlobalPlaylist --scope Global
- [p]audioset autoplay PersonalPlaylist --scope User --author Draper
+ `[p]audioset autoplay MyGuildPlaylist`
+ `[p]audioset autoplay MyGlobalPlaylist --scope Global`
+ `[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -1259,7 +1259,10 @@ class Audio(commands.Cog):
@audioset.command()
@checks.mod_or_permissions(administrator=True)
async def emptydisconnect(self, ctx: commands.Context, seconds: int):
- """Auto-disconnect from channel when bot is alone in it for x seconds, 0 to disable."""
+ """Auto-disconnect from channel when bot is alone in it for x seconds, 0 to disable.
+
+ `[p]audioset dc` takes precedence over this setting.
+ """
if seconds < 0:
return await self._embed_msg(
ctx, title=_("Invalid Time"), description=_("Seconds can't be less than zero.")
@@ -4002,7 +4005,7 @@ class Audio(commands.Cog):
The track(s) will be appended to the end of the playlist.
**Usage**:
- [p]playlist append playlist_name_OR_id track_name_OR_url args
+ `[p]playlist append playlist_name_OR_id track_name_OR_url [args]`
**Args**:
The following are all optional:
@@ -4025,10 +4028,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist append MyGuildPlaylist Hello by Adele
- [p]playlist append MyGlobalPlaylist Hello by Adele --scope Global
- [p]playlist append MyGlobalPlaylist Hello by Adele --scope Global
- --Author Draper#6666
+ `[p]playlist append MyGuildPlaylist Hello by Adele`
+ `[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global`
+ `[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4163,7 +4165,7 @@ class Audio(commands.Cog):
"""Copy a playlist from one scope to another.
**Usage**:
- [p]playlist copy playlist_name_OR_id args
+ `[p]playlist copy playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -4190,11 +4192,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global
- [p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666
- --to-scope User
- [p]playlist copy MyPersonalPlaylist --from-scope user --to-author Draper#6666
- --to-scope Guild --to-guild Red - Discord Bot
+ `[p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global`
+ `[p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666 --to-scope User`
+ `[p]playlist copy MyPersonalPlaylist --from-scope user --to-author Draper#6666 --to-scope Guild --to-guild Red - Discord Bot`
"""
if scope_data is None:
@@ -4303,7 +4303,7 @@ class Audio(commands.Cog):
"""Create an empty playlist.
**Usage**:
- [p]playlist create playlist_name args
+ `[p]playlist create playlist_name [args]`
**Args**:
The following are all optional:
@@ -4326,9 +4326,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist create MyGuildPlaylist
- [p]playlist create MyGlobalPlaylist --scope Global
- [p]playlist create MyPersonalPlaylist --scope User
+ `[p]playlist create MyGuildPlaylist`
+ `[p]playlist create MyGlobalPlaylist --scope Global`
+ `[p]playlist create MyPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4370,7 +4370,7 @@ class Audio(commands.Cog):
"""Delete a saved playlist.
**Usage**:
- [p]playlist delete playlist_name_OR_id args
+ `[p]playlist delete playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -4393,9 +4393,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist delete MyGuildPlaylist
- [p]playlist delete MyGlobalPlaylist --scope Global
- [p]playlist delete MyPersonalPlaylist --scope User
+ `[p]playlist delete MyGuildPlaylist`
+ `[p]playlist delete MyGlobalPlaylist --scope Global`
+ `[p]playlist delete MyPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4457,7 +4457,7 @@ class Audio(commands.Cog):
"""Remove duplicate tracks from a saved playlist.
**Usage**:
- [p]playlist dedupe playlist_name_OR_id args
+ `[p]playlist dedupe playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -4480,9 +4480,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist dedupe MyGuildPlaylist
- [p]playlist dedupe MyGlobalPlaylist --scope Global
- [p]playlist dedupe MyPersonalPlaylist --scope User
+ `[p]playlist dedupe MyGuildPlaylist`
+ `[p]playlist dedupe MyGlobalPlaylist --scope Global`
+ `[p]playlist dedupe MyPersonalPlaylist --scope User`
"""
async with ctx.typing():
if scope_data is None:
@@ -4596,12 +4596,12 @@ class Audio(commands.Cog):
):
"""Download a copy of a playlist.
- These files can be used with the [p]playlist upload command.
+ These files can be used with the `[p]playlist upload` command.
Red v2-compatible playlists can be generated by passing True
for the v2 variable.
**Usage**:
- [p]playlist download playlist_name_OR_id [v2=True_OR_False] args
+ `[p]playlist download playlist_name_OR_id [v2=True_OR_False] [args]`
**Args**:
The following are all optional:
@@ -4624,9 +4624,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist download MyGuildPlaylist True
- [p]playlist download MyGlobalPlaylist False --scope Global
- [p]playlist download MyPersonalPlaylist --scope User
+ `[p]playlist download MyGuildPlaylist True`
+ `[p]playlist download MyGlobalPlaylist False --scope Global`
+ `[p]playlist download MyPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4741,7 +4741,7 @@ class Audio(commands.Cog):
"""Retrieve information from a saved playlist.
**Usage**:
- [p]playlist info playlist_name_OR_id args
+ `[p]playlist info playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -4764,9 +4764,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist info MyGuildPlaylist
- [p]playlist info MyGlobalPlaylist --scope Global
- [p]playlist info MyPersonalPlaylist --scope User
+ `[p]playlist info MyGuildPlaylist`
+ `[p]playlist info MyGlobalPlaylist --scope Global`
+ `[p]playlist info MyPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4873,7 +4873,7 @@ class Audio(commands.Cog):
"""List saved playlists.
**Usage**:
- [p]playlist list args
+ `[p]playlist list [args]`
**Args**:
The following are all optional:
@@ -4896,9 +4896,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist list
- [p]playlist list --scope Global
- [p]playlist list --scope User
+ `[p]playlist list`
+ `[p]playlist list --scope Global`
+ `[p]playlist list --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -4998,7 +4998,7 @@ class Audio(commands.Cog):
"""Save the queue to a playlist.
**Usage**:
- [p]playlist queue playlist_name
+ `[p]playlist queue playlist_name [args]`
**Args**:
The following are all optional:
@@ -5021,9 +5021,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist queue MyGuildPlaylist
- [p]playlist queue MyGlobalPlaylist --scope Global
- [p]playlist queue MyPersonalPlaylist --scope User
+ `[p]playlist queue MyGuildPlaylist`
+ `[p]playlist queue MyGlobalPlaylist --scope Global`
+ `[p]playlist queue MyPersonalPlaylist --scope User`
"""
async with ctx.typing():
if scope_data is None:
@@ -5101,7 +5101,7 @@ class Audio(commands.Cog):
"""Remove a track from a playlist by url.
**Usage**:
- [p]playlist remove playlist_name_OR_id url args
+ `[p]playlist remove playlist_name_OR_id url [args]`
**Args**:
The following are all optional:
@@ -5124,11 +5124,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
- [p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
- --scope Global
- [p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
- --scope User
+ `[p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU`
+ `[p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope Global`
+ `[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -5215,7 +5213,7 @@ class Audio(commands.Cog):
"""Save a playlist from a url.
**Usage**:
- [p]playlist save name url args
+ `[p]playlist save name url [args]`
**Args**:
The following are all optional:
@@ -5238,12 +5236,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist save MyGuildPlaylist
- https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM
- [p]playlist save MyGlobalPlaylist
- https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM --scope Global
- [p]playlist save MyPersonalPlaylist
- https://open.spotify.com/playlist/1RyeIbyFeIJVnNzlGr5KkR --scope User
+ `[p]playlist save MyGuildPlaylist https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM`
+ `[p]playlist save MyGlobalPlaylist https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM --scope Global`
+ `[p]playlist save MyPersonalPlaylist https://open.spotify.com/playlist/1RyeIbyFeIJVnNzlGr5KkR --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -5313,7 +5308,7 @@ class Audio(commands.Cog):
"""Load a playlist into the queue.
**Usage**:
- [p]playlist start playlist_name_OR_id args
+ ` [p]playlist start playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -5336,9 +5331,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist start MyGuildPlaylist
- [p]playlist start MyGlobalPlaylist --scope Global
- [p]playlist start MyPersonalPlaylist --scope User
+ `[p]playlist start MyGuildPlaylist`
+ `[p]playlist start MyGlobalPlaylist --scope Global`
+ `[p]playlist start MyPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -5483,7 +5478,7 @@ class Audio(commands.Cog):
"""Updates all tracks in a playlist.
**Usage**:
- [p]playlist update playlist_name_OR_id args
+ `[p]playlist update playlist_name_OR_id [args]`
**Args**:
The following are all optional:
@@ -5506,9 +5501,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist update MyGuildPlaylist
- [p]playlist update MyGlobalPlaylist --scope Global
- [p]playlist update MyPersonalPlaylist --scope User
+ `[p]playlist update MyGuildPlaylist`
+ `[p]playlist update MyGlobalPlaylist --scope Global`
+ `[p]playlist update MyPersonalPlaylist --scope User`
"""
if scope_data is None:
@@ -5631,10 +5626,10 @@ class Audio(commands.Cog):
"""Uploads a playlist file as a playlist for the bot.
V2 and old V3 playlist will be slow.
- V3 Playlist made with [p]playlist download will load a lot faster.
+ V3 Playlist made with `[p]playlist download` will load a lot faster.
**Usage**:
- [p]playlist upload args
+ `[p]playlist upload [args]`
**Args**:
The following are all optional:
@@ -5657,9 +5652,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist upload
- [p]playlist upload --scope Global
- [p]playlist upload --scope User
+ `[p]playlist upload`
+ `[p]playlist upload --scope Global`
+ `[p]playlist upload --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -5763,7 +5758,7 @@ class Audio(commands.Cog):
"""Rename an existing playlist.
**Usage**:
- [p]playlist rename playlist_name_OR_id new_name args
+ `[p]playlist rename playlist_name_OR_id new_name [args]`
**Args**:
The following are all optional:
@@ -5786,9 +5781,9 @@ class Audio(commands.Cog):
Exact guild name
Example use:
- [p]playlist rename MyGuildPlaylist RenamedGuildPlaylist
- [p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global
- [p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User
+ `[p]playlist rename MyGuildPlaylist RenamedGuildPlaylist`
+ `[p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global`
+ `[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
"""
if scope_data is None:
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
@@ -6818,8 +6813,8 @@ class Audio(commands.Cog):
async def search(self, ctx: commands.Context, *, query: str):
"""Pick a track with a search.
- Use `[p]search list ` to queue all tracks found on YouTube. `[p]search sc
- ` will search SoundCloud instead of YouTube.
+ Use `[p]search list ` to queue all tracks found on YouTube.
+ `[p]search sc` will search SoundCloud instead of YouTube.
"""
async def _search_menu(
@@ -7382,8 +7377,8 @@ class Audio(commands.Cog):
async def _shuffle_bumpped(self, ctx: commands.Context):
"""Toggle bumped track shuffle.
- Set this to disabled if you wish to avoid bumped songs being shuffled. This takes priority
- over `[p]shuffle`.
+ Set this to disabled if you wish to avoid bumped songs being shuffled.
+ This takes priority over `[p]shuffle`.
"""
dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
From d1b7f836db4769790de9963c0c89a27167478b0f Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Fri, 17 Jan 2020 22:45:41 +0100
Subject: [PATCH 37/91] Update auto_labeler.yml (#3396)
---
.github/workflows/auto_labeler.yml | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/auto_labeler.yml b/.github/workflows/auto_labeler.yml
index 5440e4e33..e4b3bc9f2 100644
--- a/.github/workflows/auto_labeler.yml
+++ b/.github/workflows/auto_labeler.yml
@@ -13,9 +13,14 @@ jobs:
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
+ const is_status_label = (label) => label.name.startsWith('Status: ');
+ if (context.payload.issue.labels.some(is_status_label)) {
+ console.log('Issue already has Status label, skipping...');
+ return;
+ }
github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Status: Needs Triage']
- })
+ });
From cd7f4681a48c18a52bc825b232d2c1335bc384bb Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 16:49:25 -0500
Subject: [PATCH 38/91] Cache prefixes (#3150)
* Cache prefixes
- This works towards #3148
- Ends up centralizing some logic
- Including that prefixes should be a reverse sorted list
* handle global prefix attempts at none
* fix prefix set for server
* cache using guild id
---
changelog.d/3148.misc.1.rst | 1 +
redbot/core/bot.py | 36 +++++++++--------------
redbot/core/cli.py | 4 ++-
redbot/core/core_commands.py | 15 ++++------
redbot/core/settings_caches.py | 53 ++++++++++++++++++++++++++++++++++
5 files changed, 77 insertions(+), 32 deletions(-)
create mode 100644 changelog.d/3148.misc.1.rst
create mode 100644 redbot/core/settings_caches.py
diff --git a/changelog.d/3148.misc.1.rst b/changelog.d/3148.misc.1.rst
new file mode 100644
index 000000000..6e966e714
--- /dev/null
+++ b/changelog.d/3148.misc.1.rst
@@ -0,0 +1 @@
+Cache prefixes rather than lookup from config each time
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 32b69f27d..9c7e56408 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -36,6 +36,8 @@ from .dev_commands import Dev
from .events import init_events
from .global_checks import init_global_checks
+from .settings_caches import PrefixManager
+
from .rpc import RPCMixin
from .utils import common_filters
@@ -124,23 +126,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
self._config.init_custom(SHARED_API_TOKENS, 2)
self._config.register_custom(SHARED_API_TOKENS)
+ self._prefix_cache = PrefixManager(self._config, cli_flags)
- async def prefix_manager(bot, message):
- if not cli_flags.prefix:
- global_prefix = await bot._config.prefix()
- else:
- global_prefix = cli_flags.prefix
- if message.guild is None:
- return global_prefix
- server_prefix = await bot._config.guild(message.guild).prefix()
+ async def prefix_manager(bot, message) -> List[str]:
+ prefixes = await self._prefix_cache.get_prefixes(message.guild)
if cli_flags.mentionable:
- return (
- when_mentioned_or(*server_prefix)(bot, message)
- if server_prefix
- else when_mentioned_or(*global_prefix)(bot, message)
- )
- else:
- return server_prefix if server_prefix else global_prefix
+ return when_mentioned_or(*prefixes)(bot, message)
+ return prefixes
if "command_prefix" not in kwargs:
kwargs["command_prefix"] = prefix_manager
@@ -273,15 +265,15 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
"""
This checks if a user or member is allowed to run things,
as considered by Red's whitelist and blacklist.
-
+
If given a user object, this function will check the global lists
-
+
If given a member, this will additionally check guild lists
-
+
If omiting a user or member, you must provide a value for ``who_id``
-
+
You may also provide a value for ``guild_id`` in this case
-
+
If providing a member by guild and member ids,
you should supply ``role_ids`` as well
@@ -289,7 +281,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
----------
who : Optional[Union[discord.Member, discord.User]]
The user or member object to check
-
+
Other Parameters
----------------
who_id : Optional[int]
@@ -906,7 +898,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
This should realistically only be used for responding using user provided
input. (unfortunately, including usernames)
Manually crafted messages which dont take any user input have no need of this
-
+
Returns
-------
discord.Message
diff --git a/redbot/core/cli.py b/redbot/core/cli.py
index 4778bb642..9c2575795 100644
--- a/redbot/core/cli.py
+++ b/redbot/core/cli.py
@@ -135,7 +135,9 @@ def parse_cli_flags(args):
"security implications if misused. Can be "
"multiple.",
)
- parser.add_argument("--prefix", "-p", action="append", help="Global prefix. Can be multiple")
+ parser.add_argument(
+ "--prefix", "-p", action="append", help="Global prefix. Can be multiple", default=[]
+ )
parser.add_argument(
"--no-prompt",
action="store_true",
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 4a05cea70..02ed1b738 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -257,10 +257,9 @@ class CoreLogic:
The current (or new) list of prefixes.
"""
if prefixes:
- prefixes = sorted(prefixes, reverse=True)
- await self.bot._config.prefix.set(prefixes)
+ await self.bot._prefix_cache.set_prefixes(guild=None, prefixes=prefixes)
return prefixes
- return await self.bot._config.prefix()
+ return await self.bot._prefix_cache.get_prefixes(guild=None)
@classmethod
async def _version_info(cls) -> Dict[str, str]:
@@ -847,15 +846,13 @@ class Core(commands.Cog, CoreLogic):
mod_role_ids = await ctx.bot._config.guild(ctx.guild).mod_role()
mod_role_names = [r.name for r in guild.roles if r.id in mod_role_ids]
mod_roles_str = humanize_list(mod_role_names) if mod_role_names else "Not Set."
- prefixes = await ctx.bot._config.guild(ctx.guild).prefix()
guild_settings = _("Admin roles: {admin}\nMod roles: {mod}\n").format(
admin=admin_roles_str, mod=mod_roles_str
)
else:
guild_settings = ""
- prefixes = None # This is correct. The below can happen in a guild.
- if not prefixes:
- prefixes = await ctx.bot._config.prefix()
+
+ prefixes = await ctx.bot._prefix_cache.get_prefixes(ctx.guild)
locale = await ctx.bot._config.locale()
prefix_string = " ".join(prefixes)
@@ -1182,11 +1179,11 @@ class Core(commands.Cog, CoreLogic):
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
"""Sets Red's server prefix(es)"""
if not prefixes:
- await ctx.bot._config.guild(ctx.guild).prefix.set([])
+ await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=[])
await ctx.send(_("Guild prefixes have been reset."))
return
prefixes = sorted(prefixes, reverse=True)
- await ctx.bot._config.guild(ctx.guild).prefix.set(prefixes)
+ await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=prefixes)
await ctx.send(_("Prefix set."))
@_set.command()
diff --git a/redbot/core/settings_caches.py b/redbot/core/settings_caches.py
new file mode 100644
index 000000000..32c4c5d52
--- /dev/null
+++ b/redbot/core/settings_caches.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from typing import Dict, List, Optional
+from argparse import Namespace
+
+import discord
+
+from .config import Config
+
+
+class PrefixManager:
+ def __init__(self, config: Config, cli_flags: Namespace):
+ self._config: Config = config
+ self._global_prefix_overide: Optional[List[str]] = sorted(
+ cli_flags.prefix, reverse=True
+ ) or None
+ self._cached: Dict[Optional[int], List[str]] = {}
+
+ async def get_prefixes(self, guild: Optional[discord.Guild] = None) -> List[str]:
+ ret: List[str]
+
+ gid: Optional[int] = guild.id if guild else None
+
+ if gid in self._cached:
+ ret = self._cached[gid].copy()
+ else:
+ if gid is not None:
+ ret = await self._config.guild_from_id(gid).prefix()
+ if not ret:
+ ret = await self.get_prefixes(None)
+ else:
+ ret = self._global_prefix_overide or (await self._config.prefix())
+
+ self._cached[gid] = ret.copy()
+
+ return ret
+
+ async def set_prefixes(
+ self, guild: Optional[discord.Guild] = None, prefixes: Optional[List[str]] = None
+ ):
+ gid: Optional[int] = guild.id if guild else None
+ prefixes = prefixes or []
+ if not isinstance(prefixes, list) and not all(isinstance(pfx, str) for pfx in prefixes):
+ raise TypeError("Prefixes must be a list of strings")
+ prefixes = sorted(prefixes, reverse=True)
+ if gid is None:
+ if not prefixes:
+ raise ValueError("You must have at least one prefix.")
+ self._cached.clear()
+ await self._config.prefix.set(prefixes)
+ else:
+ del self._cached[gid]
+ await self._config.guild_from_id(gid).prefix.set(prefixes)
From 41b283ce5dfe6f515ea377e79971a14170e06cfa Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Fri, 17 Jan 2020 22:00:29 +0000
Subject: [PATCH 39/91] [Audio] Show correct time remaining for bumped tracks
(#3375)
* Fixes Bump play
* *sigh*
---
changelog.d/audio/3373.bugfix.1.rst | 1 +
redbot/cogs/audio/audio.py | 3 +--
redbot/cogs/audio/audio_dataclasses.py | 2 +-
redbot/cogs/audio/utils.py | 15 +++++++++++++++
4 files changed, 18 insertions(+), 3 deletions(-)
create mode 100644 changelog.d/audio/3373.bugfix.1.rst
diff --git a/changelog.d/audio/3373.bugfix.1.rst b/changelog.d/audio/3373.bugfix.1.rst
new file mode 100644
index 000000000..3f421e0c7
--- /dev/null
+++ b/changelog.d/audio/3373.bugfix.1.rst
@@ -0,0 +1 @@
+``[p]bumpplay`` now shows the current remaining time until the bumped track is played.
\ No newline at end of file
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 19ad5e744..259d60e62 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -2957,8 +2957,7 @@ class Audio(commands.Cog):
return await self._embed_msg(ctx, embed=embed)
elif isinstance(tracks, discord.Message):
return
- queue_dur = await queue_duration(ctx)
- lavalink.utils.format_time(queue_dur)
+ queue_dur = await track_remaining_duration(ctx)
index = query.track_index
seek = 0
if query.start_time:
diff --git a/redbot/cogs/audio/audio_dataclasses.py b/redbot/cogs/audio/audio_dataclasses.py
index d31cbbc9b..32eb3ef2b 100644
--- a/redbot/cogs/audio/audio_dataclasses.py
+++ b/redbot/cogs/audio/audio_dataclasses.py
@@ -211,7 +211,7 @@ class LocalPath:
def tracks_in_tree(self):
tracks = []
- for track in self.multirglob(*[f"*{ext}" for ext in self._all_music_ext]):
+ for track in self.multirglob(*[f"{ext}" for ext in self._all_music_ext]):
if track.exists() and track.is_file() and track.parent != self.localtrack_folder:
tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
return sorted(tracks, key=lambda x: x.to_string_user().lower())
diff --git a/redbot/cogs/audio/utils.py b/redbot/cogs/audio/utils.py
index 2886f4996..79a0b00e7 100644
--- a/redbot/cogs/audio/utils.py
+++ b/redbot/cogs/audio/utils.py
@@ -43,6 +43,7 @@ __all__ = [
"CacheLevel",
"format_playlist_picker_data",
"get_track_description_unformatted",
+ "track_remaining_duration",
"Notifier",
"PlaylistScope",
]
@@ -126,6 +127,20 @@ async def queue_duration(ctx) -> int:
return queue_total_duration
+async def track_remaining_duration(ctx) -> int:
+ player = lavalink.get_player(ctx.guild.id)
+ if not player.current:
+ return 0
+ try:
+ if not player.current.is_stream:
+ remain = player.current.length - player.position
+ else:
+ remain = 0
+ except AttributeError:
+ remain = 0
+ return remain
+
+
async def draw_time(ctx) -> str:
player = lavalink.get_player(ctx.guild.id)
paused = player.paused
From b88bd5d44d327426b98fb63fa0888c34768fb352 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 17:07:34 -0500
Subject: [PATCH 40/91] More exit tweaks (#3392)
---
redbot/__main__.py | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/redbot/__main__.py b/redbot/__main__.py
index 77c01ba52..2bf085f7d 100644
--- a/redbot/__main__.py
+++ b/redbot/__main__.py
@@ -490,14 +490,13 @@ def main():
# Allows transports to close properly, and prevent new ones from being opened.
# Transports may still not be closed correcly on windows, see below
loop.run_until_complete(loop.shutdown_asyncgens())
- if os.name == "nt":
- # *we* aren't cleaning up more here, but it prevents
- # a runtime error at the event loop on windows
- # with resources which require longer to clean up.
- # With other event loops, a failure to cleanup prior to here
- # results in a resource warning instead and does not break us.
- log.info("Please wait, cleaning up a bit more")
- loop.run_until_complete(asyncio.sleep(1))
+ # *we* aren't cleaning up more here, but it prevents
+ # a runtime error at the event loop on windows
+ # with resources which require longer to clean up.
+ # With other event loops, a failure to cleanup prior to here
+ # results in a resource warning instead
+ log.info("Please wait, cleaning up a bit more")
+ loop.run_until_complete(asyncio.sleep(2))
loop.stop()
loop.close()
exit_code = red._shutdown_mode if red is not None else 1
From 2c12e4f6bf9c384ad748680a4c77953f52900968 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Fri, 17 Jan 2020 22:07:49 +0000
Subject: [PATCH 41/91] [Audio] Show symbolic link folders (#3376)
* Fixes Bump play
* Fixed #3332
* Revert "Fixed #3332"
This reverts commit d76d3acb
* Revert "Fixes Bump play"
This reverts commit 3839bdaf
* *sigh*
* *sigh*
* *sigh*
* use iglob + async iterator
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* black
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
* + fixes
Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com>
---
redbot/cogs/audio/audio.py | 14 ++-
redbot/cogs/audio/audio_dataclasses.py | 154 +++++++++++++++----------
2 files changed, 105 insertions(+), 63 deletions(-)
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 259d60e62..99714daf6 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -2452,7 +2452,11 @@ class Audio(commands.Cog):
if not await self._localtracks_check(ctx):
return
- return audio_data.subfolders_in_tree() if search_subfolders else audio_data.subfolders()
+ return (
+ await audio_data.subfolders_in_tree()
+ if search_subfolders
+ else await audio_data.subfolders()
+ )
async def _folder_list(
self, ctx: commands.Context, query: audio_dataclasses.Query
@@ -2463,9 +2467,9 @@ class Audio(commands.Cog):
if not query.track.exists():
return
return (
- query.track.tracks_in_tree()
+ await query.track.tracks_in_tree()
if query.search_subfolders
- else query.track.tracks_in_folder()
+ else await query.track.tracks_in_folder()
)
async def _folder_tracks(
@@ -2504,9 +2508,9 @@ class Audio(commands.Cog):
return
return (
- query.track.tracks_in_tree()
+ await query.track.tracks_in_tree()
if query.search_subfolders
- else query.track.tracks_in_folder()
+ else await query.track.tracks_in_folder()
)
async def _localtracks_check(self, ctx: commands.Context) -> bool:
diff --git a/redbot/cogs/audio/audio_dataclasses.py b/redbot/cogs/audio/audio_dataclasses.py
index 32eb3ef2b..21c79e14f 100644
--- a/redbot/cogs/audio/audio_dataclasses.py
+++ b/redbot/cogs/audio/audio_dataclasses.py
@@ -1,9 +1,12 @@
+import asyncio
+import contextlib
+import glob
import ntpath
import os
import posixpath
import re
from pathlib import Path, PosixPath, WindowsPath
-from typing import List, Optional, Union, MutableMapping
+from typing import List, Optional, Union, MutableMapping, Iterator, AsyncIterator
from urllib.parse import urlparse
import lavalink
@@ -167,29 +170,48 @@ class LocalPath:
modified.path = modified.path.joinpath(*args)
return modified
- def multiglob(self, *patterns):
- paths = []
+ def rglob(self, pattern, folder=False) -> Iterator[str]:
+ if folder:
+ return glob.iglob(f"{self.path}{os.sep}**{os.sep}", recursive=True)
+ else:
+ return glob.iglob(f"{self.path}{os.sep}**{os.sep}{pattern}", recursive=True)
+
+ def glob(self, pattern, folder=False) -> Iterator[str]:
+ if folder:
+ return glob.iglob(f"{self.path}{os.sep}*{os.sep}", recursive=False)
+ else:
+ return glob.iglob(f"{self.path}{os.sep}*{pattern}", recursive=False)
+
+ async def multiglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
for p in patterns:
- paths.extend(list(self.path.glob(p)))
- for p in self._filtered(paths):
- yield p
+ for rp in self.glob(p):
+ rp = LocalPath(rp)
+ if folder and rp.is_dir() and rp.exists():
+ yield rp
+ await asyncio.sleep(0)
+ else:
+ if rp.suffix in self._all_music_ext and rp.is_file() and rp.exists():
+ yield rp
+ await asyncio.sleep(0)
- def multirglob(self, *patterns):
- paths = []
+ async def multirglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
for p in patterns:
- paths.extend(list(self.path.rglob(p)))
-
- for p in self._filtered(paths):
- yield p
-
- def _filtered(self, paths: List[Path]):
- for p in paths:
- if p.suffix in self._all_music_ext:
- yield p
+ for rp in self.rglob(p):
+ rp = LocalPath(rp)
+ if folder and rp.is_dir() and rp.exists():
+ yield rp
+ await asyncio.sleep(0)
+ else:
+ if rp.suffix in self._all_music_ext and rp.is_file() and rp.exists():
+ yield rp
+ await asyncio.sleep(0)
def __str__(self):
return self.to_string()
+ def __repr__(self):
+ return str(self)
+
def to_string(self):
try:
return str(self.path.absolute())
@@ -209,48 +231,56 @@ class LocalPath:
string = f"...{os.sep}{string}"
return string
- def tracks_in_tree(self):
+ async def tracks_in_tree(self):
tracks = []
- for track in self.multirglob(*[f"{ext}" for ext in self._all_music_ext]):
- if track.exists() and track.is_file() and track.parent != self.localtrack_folder:
- tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
+ async for track in self.multirglob(*[f"{ext}" for ext in self._all_music_ext]):
+ with contextlib.suppress(ValueError):
+ if track.path.parent != self.localtrack_folder and track.path.relative_to(
+ self.path
+ ):
+ tracks.append(Query.process_input(track))
return sorted(tracks, key=lambda x: x.to_string_user().lower())
- def subfolders_in_tree(self):
- files = list(self.multirglob(*[f"*{ext}" for ext in self._all_music_ext]))
- folders = []
- for f in files:
- if f.exists() and f.parent not in folders and f.parent != self.localtrack_folder:
- folders.append(f.parent)
+ async def subfolders_in_tree(self):
return_folders = []
- for folder in folders:
- if folder.exists() and folder.is_dir():
- return_folders.append(LocalPath(str(folder.absolute())))
+ async for f in self.multirglob("", folder=True):
+ with contextlib.suppress(ValueError):
+ if (
+ f not in return_folders
+ and f.path != self.localtrack_folder
+ and f.path.relative_to(self.path)
+ ):
+ return_folders.append(f)
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
- def tracks_in_folder(self):
+ async def tracks_in_folder(self):
tracks = []
- for track in self.multiglob(*[f"*{ext}" for ext in self._all_music_ext]):
- if track.exists() and track.is_file() and track.parent != self.localtrack_folder:
- tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
+ async for track in self.multiglob(*[f"{ext}" for ext in self._all_music_ext]):
+ with contextlib.suppress(ValueError):
+ if track.path.parent != self.localtrack_folder and track.path.relative_to(
+ self.path
+ ):
+ tracks.append(Query.process_input(track))
return sorted(tracks, key=lambda x: x.to_string_user().lower())
- def subfolders(self):
- files = list(self.multiglob(*[f"*{ext}" for ext in self._all_music_ext]))
- folders = []
- for f in files:
- if f.exists() and f.parent not in folders and f.parent != self.localtrack_folder:
- folders.append(f.parent)
+ async def subfolders(self):
return_folders = []
- for folder in folders:
- if folder.exists() and folder.is_dir():
- return_folders.append(LocalPath(str(folder.absolute())))
+ async for f in self.multiglob("", folder=True):
+ with contextlib.suppress(ValueError):
+ if (
+ f not in return_folders
+ and f.path != self.localtrack_folder
+ and f.path.relative_to(self.path)
+ ):
+ return_folders.append(f)
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
def __eq__(self, other):
- if not isinstance(other, LocalPath):
- return NotImplemented
- return self.path._cparts == other.path._cparts
+ if isinstance(other, LocalPath):
+ return self.path._cparts == other.path._cparts
+ elif isinstance(other, Path):
+ return self.path._cparts == other._cpart
+ return NotImplemented
def __hash__(self):
try:
@@ -260,24 +290,32 @@ class LocalPath:
return self._hash
def __lt__(self, other):
- if not isinstance(other, LocalPath):
- return NotImplemented
- return self.path._cparts < other.path._cparts
+ if isinstance(other, LocalPath):
+ return self.path._cparts < other.path._cparts
+ elif isinstance(other, Path):
+ return self.path._cparts < other._cpart
+ return NotImplemented
def __le__(self, other):
- if not isinstance(other, LocalPath):
- return NotImplemented
- return self.path._cparts <= other.path._cparts
+ if isinstance(other, LocalPath):
+ return self.path._cparts <= other.path._cparts
+ elif isinstance(other, Path):
+ return self.path._cparts <= other._cpart
+ return NotImplemented
def __gt__(self, other):
- if not isinstance(other, LocalPath):
- return NotImplemented
- return self.path._cparts > other.path._cparts
+ if isinstance(other, LocalPath):
+ return self.path._cparts > other.path._cparts
+ elif isinstance(other, Path):
+ return self.path._cparts > other._cpart
+ return NotImplemented
def __ge__(self, other):
- if not isinstance(other, LocalPath):
- return NotImplemented
- return self.path._cparts >= other.path._cparts
+ if isinstance(other, LocalPath):
+ return self.path._cparts >= other.path._cparts
+ elif isinstance(other, Path):
+ return self.path._cparts >= other._cpart
+ return NotImplemented
class Query:
From d52f8974fdf011100d9206537606651147fd9b0e Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 17:59:37 -0500
Subject: [PATCH 42/91] Stop special casing help in bot.embed_requested (#3382)
- However, we are not changing the signature
- This was previously special cased for reasons related to the older
version of the help formatter we used and never re-evaluated for need.
- We should leave the signature as is both for lack of breaking, and
for potential future changes
// actually this was already done once in GH-2966 but got accidentally overwritten
---
redbot/core/bot.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 9c7e56408..5ad380f62 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -614,9 +614,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
bool
:code:`True` if an embed is requested
"""
- if isinstance(channel, discord.abc.PrivateChannel) or (
- command and command == self.get_command("help")
- ):
+ if isinstance(channel, discord.abc.PrivateChannel):
user_setting = await self._config.user(user).embeds()
if user_setting is not None:
return user_setting
@@ -624,6 +622,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
guild_setting = await self._config.guild(channel.guild).embeds()
if guild_setting is not None:
return guild_setting
+
global_setting = await self._config.embeds()
return global_setting
From 7f2e5a0b700bb4f90cfa9e5d6cb0ca1fed2c0287 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 18:00:24 -0500
Subject: [PATCH 43/91] [Docs] Remaining doc improvements for 3.2.3 (#3400)
* double the fun
* double
* pluralize this
---
docs/framework_apikeys.rst | 13 +++++++++++++
docs/install_linux_mac.rst | 4 ++++
docs/install_windows.rst | 4 ++++
3 files changed, 21 insertions(+)
diff --git a/docs/framework_apikeys.rst b/docs/framework_apikeys.rst
index b27d2e713..2a67d9c45 100644
--- a/docs/framework_apikeys.rst
+++ b/docs/framework_apikeys.rst
@@ -60,3 +60,16 @@ Event Reference
:type service_name: :class:`str`
:param api_tokens: New Mapping of token names to tokens. This contains api tokens that weren't changed too.
:type api_tokens: Mapping[:class:`str`, :class:`str`]
+
+
+*********************
+Additional References
+*********************
+
+.. py:currentmodule:: redbot.core.bot
+
+.. automethod:: Red.get_shared_api_tokens
+
+.. automethod:: Red.set_shared_api_tokens
+
+.. automethod:: Red.remove_shared_api_tokens
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index 1b7dccfea..c21e7bfe6 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -355,6 +355,10 @@ Or, to install with PostgreSQL support:
python -m pip install -U Red-DiscordBot[postgres]
+.. note::
+
+ These commands are also used for updating Red
+
--------------------------
Setting Up and Running Red
--------------------------
diff --git a/docs/install_windows.rst b/docs/install_windows.rst
index 36fcc11fa..05e8c60d9 100644
--- a/docs/install_windows.rst
+++ b/docs/install_windows.rst
@@ -96,6 +96,10 @@ Installing Red
python -m pip install -U Red-DiscordBot[postgres]
+.. note::
+
+ These commands are also used for updating Red
+
--------------------------
Setting Up and Running Red
--------------------------
From 6219f0da67c74dd52401d93484605ee7328e66f5 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 18:51:49 -0500
Subject: [PATCH 44/91] [Modlog API] Add resolution for people inpacted by bad
casetypes (#3333)
* add resolution for people inpacted by bad casetypes
* *some* amount of notice on this
* Fine.
* clearer warnings
* actually, unnneded
---
redbot/cogs/modlog/modlog.py | 7 +++++++
redbot/core/modlog.py | 32 +++++++++++++++++++++-----------
2 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/redbot/cogs/modlog/modlog.py b/redbot/cogs/modlog/modlog.py
index 0ab2f85c6..3c9f1ba55 100644
--- a/redbot/cogs/modlog/modlog.py
+++ b/redbot/cogs/modlog/modlog.py
@@ -26,6 +26,13 @@ class ModLog(commands.Cog):
"""Manage modlog settings."""
pass
+ @checks.is_owner()
+ @modlogset.command(hidden=True, name="fixcasetypes")
+ async def reapply_audittype_migration(self, ctx: commands.Context):
+ """Command to fix misbehaving casetypes."""
+ await modlog.handle_auditype_key()
+ await ctx.tick()
+
@modlogset.command()
@commands.guild_only()
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
diff --git a/redbot/core/modlog.py b/redbot/core/modlog.py
index 5758b9b33..700203497 100644
--- a/redbot/core/modlog.py
+++ b/redbot/core/modlog.py
@@ -142,6 +142,18 @@ async def _init(bot: Red):
bot.add_listener(on_member_unban)
+async def handle_auditype_key():
+ all_casetypes = {
+ casetype_name: {
+ inner_key: inner_value
+ for inner_key, inner_value in casetype_data.items()
+ if inner_key != "audit_type"
+ }
+ for casetype_name, casetype_data in (await _conf.custom(_CASETYPES).all()).items()
+ }
+ await _conf.custom(_CASETYPES).set(all_casetypes)
+
+
async def _migrate_config(from_version: int, to_version: int):
if from_version == to_version:
return
@@ -170,16 +182,7 @@ async def _migrate_config(from_version: int, to_version: int):
await _conf.guild(cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("cases")
if from_version < 3 <= to_version:
- all_casetypes = {
- casetype_name: {
- inner_key: inner_value
- for inner_key, inner_value in casetype_data.items()
- if inner_key != "audit_type"
- }
- for casetype_name, casetype_data in (await _conf.custom(_CASETYPES).all()).items()
- }
-
- await _conf.custom(_CASETYPES).set(all_casetypes)
+ await handle_auditype_key()
await _conf.schema_version.set(3)
if from_version < 4 <= to_version:
@@ -507,8 +510,15 @@ class CaseType:
self.image = image
self.case_str = case_str
self.guild = guild
+
+ if "audit_type" in kwargs:
+ kwargs.pop("audit_type", None)
+ log.warning(
+ "Fix this using the hidden command: `modlogset fixcasetypes` in Discord: "
+ "Got outdated key in casetype: audit_type"
+ )
if kwargs:
- log.warning("Got unexpected keys in case %s", ",".join(kwargs.keys()))
+ log.warning("Got unexpected key(s) in casetype: %s", ",".join(kwargs.keys()))
async def to_json(self):
"""Transforms the case type into a dict and saves it"""
From 66cae71d90f64399ee3913ca0ac1cf96c83ffb2d Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Sat, 18 Jan 2020 01:05:39 +0100
Subject: [PATCH 45/91] [Docs] Changed python version references on docs
(#3402)
* [Docs] Changes Python references to Python 3.8
* [Misc] Remove obsolete mention to Zenhub
---
.github/CONTRIBUTING.md | 9 ++++-----
README.md | 2 +-
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 292dcd86f..403bb1eb8 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -30,9 +30,8 @@ Red is an open source project. This means that each and every one of the develop
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
# 2. Ground Rules
-We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
1. Ensure cross compatibility for Windows, Mac OS and Linux.
-2. Ensure all Python features used in contributions exist and work in Python 3.7 and above.
+2. Ensure all Python features used in contributions exist and work in Python 3.8.1 and above.
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
@@ -54,7 +53,7 @@ Red's repository is configured to follow a particular development workflow, usin
### 4.1 Setting up your development environment
The following requirements must be installed prior to setting up:
- - Python 3.7.0 or greater
+ - Python 3.8.1 or greater
- git
- pip
@@ -83,7 +82,7 @@ If you're not on Windows, you should also have GNU make installed, and you can o
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
Currently, tox does the following, creating its own virtual environments for each stage:
-- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.7 (test environment `py37`)
+- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.8 (test environment `py38`)
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
@@ -107,7 +106,7 @@ You may have noticed we have a `Makefile` and a `make.bat` in the top-level dire
The other make recipes are most likely for project maintainers rather than contributors.
-You can specify the Python executable used in the make recipes with the `PYTHON` environment variable, e.g. `make PYTHON=/usr/bin/python3.7 newenv`.
+You can specify the Python executable used in the make recipes with the `PYTHON` environment variable, e.g. `make PYTHON=/usr/bin/python3.8 newenv`.
### 4.5 Keeping your dependencies up to date
Whenever you pull from upstream (V3/develop on the main repository) and you notice either of the files `setup.cfg` or `tools/dev-requirements.txt` have been changed, it can often mean some package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `make syncenv`. You could also simply do `make newenv` to install them to a clean new virtual environment.
diff --git a/README.md b/README.md
index 7d7fd9a6b..d65963747 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
-
+
From 33ea3a1419bb9d7a0dd6265add7a438ea514283c Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 19:47:22 -0500
Subject: [PATCH 46/91] version bump w/changelog (#3403)
---
docs/changelog_3_2_0.rst | 63 ++++++++++++++++++++++++++++++++++++++++
redbot/__init__.py | 2 +-
2 files changed, 64 insertions(+), 1 deletion(-)
diff --git a/docs/changelog_3_2_0.rst b/docs/changelog_3_2_0.rst
index 43874ad1c..3ac5f6cf6 100644
--- a/docs/changelog_3_2_0.rst
+++ b/docs/changelog_3_2_0.rst
@@ -1,5 +1,68 @@
.. 3.2.x Changelogs
+Redbot 3.2.3 (2020-01-17)
+=========================
+
+Core Bot Changes
+----------------
+
+- Further improvements have been made to bot startup and shutdown.
+- Prefixes are now cached for performance.
+- Added a means for cog creators to use a global preinvoke hook.
+- The bot now ensures it has at least the bare neccessary permissions before commands.
+- Deleting instances works as intended again.
+- Sinbad stopped fighting it and embraced the entrypoint madness.
+
+Core Commands
+-------------
+
+- The servers command now also shows the ids.
+
+Admin Cog
+---------
+
+- The selfrole command now has reasonable expectations about hierarchy.
+
+Help Formatter
+--------------
+
+- ``[botname]`` is now replaced with the bot's display name in help text.
+- New features added for cog creators to further customize help behavior.
+
+ - Check out our command reference for details.
+- Embed settings are now consistent.
+
+Downloader
+----------
+
+- Improved a few user facing messages.
+- Added pagination of output on cog update.
+- Added logging of failures.
+
+Docs
+----
+
+There's more detail to the below changes, so go read the docs.
+For some reason, documenting documentation changes is hard.
+
+- Added instructions about git version
+- Clarified instructions for installation and update.
+- Added more detail to the API key reference.
+- Fixed some typos and versioning mistakes.
+
+
+Audio
+-----
+
+Draper did things.
+
+- No seriously, Draper did things.
+- Wait you wanted details? Ok, I guess we can share those.
+- Audio properly disconnects with autodisconnect, even if notify is being used.
+- Symbolic links now work as intended for local tracks.
+- Bump play now shows the correct time till next track.
+- Multiple user facing messages have been made more correct.
+
Redbot 3.2.2 (2020-01-10)
=========================
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 8b08c74f4..0e625eb21 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.2.3.dev1"
+__version__ = "3.2.3"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From b089be7b4975f0b0dc9ffa5f79a88ca9c01ff4cc Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 19:57:27 -0500
Subject: [PATCH 47/91] fixup docs (#3404)
---
changelog.d/3148.misc.1.rst | 1 -
changelog.d/3224.enhance.rst | 1 -
changelog.d/3306.docs.rst | 1 -
changelog.d/3335.enhance.rst | 1 -
changelog.d/3368.docs.rst | 1 -
changelog.d/3390.misc.rst | 1 -
changelog.d/audio/3140.enhance.1.rst | 1 -
changelog.d/audio/3328.hotfix.1.rst | 1 -
changelog.d/audio/3328.hotfix.2.rst | 1 -
changelog.d/audio/3337.misc.1.rst | 1 -
changelog.d/audio/3337.misc.2.rst | 1 -
changelog.d/audio/3342.enhance.1.rst | 2 --
changelog.d/audio/3344.enhance.1.rst | 1 -
changelog.d/audio/3345.enhance.1.rst | 1 -
changelog.d/audio/3349.bugfix.1.rst | 1 -
changelog.d/audio/3373.bugfix.1.rst | 1 -
changelog.d/warnings/2900.enhance.rst | 2 --
docs/framework_commands.rst | 1 +
18 files changed, 1 insertion(+), 19 deletions(-)
delete mode 100644 changelog.d/3148.misc.1.rst
delete mode 100644 changelog.d/3224.enhance.rst
delete mode 100644 changelog.d/3306.docs.rst
delete mode 100644 changelog.d/3335.enhance.rst
delete mode 100644 changelog.d/3368.docs.rst
delete mode 100644 changelog.d/3390.misc.rst
delete mode 100644 changelog.d/audio/3140.enhance.1.rst
delete mode 100644 changelog.d/audio/3328.hotfix.1.rst
delete mode 100644 changelog.d/audio/3328.hotfix.2.rst
delete mode 100644 changelog.d/audio/3337.misc.1.rst
delete mode 100644 changelog.d/audio/3337.misc.2.rst
delete mode 100644 changelog.d/audio/3342.enhance.1.rst
delete mode 100644 changelog.d/audio/3344.enhance.1.rst
delete mode 100644 changelog.d/audio/3345.enhance.1.rst
delete mode 100644 changelog.d/audio/3349.bugfix.1.rst
delete mode 100644 changelog.d/audio/3373.bugfix.1.rst
delete mode 100644 changelog.d/warnings/2900.enhance.rst
diff --git a/changelog.d/3148.misc.1.rst b/changelog.d/3148.misc.1.rst
deleted file mode 100644
index 6e966e714..000000000
--- a/changelog.d/3148.misc.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Cache prefixes rather than lookup from config each time
diff --git a/changelog.d/3224.enhance.rst b/changelog.d/3224.enhance.rst
deleted file mode 100644
index 5dcacb085..000000000
--- a/changelog.d/3224.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-Adds server IDs to servers command.
\ No newline at end of file
diff --git a/changelog.d/3306.docs.rst b/changelog.d/3306.docs.rst
deleted file mode 100644
index e52352c5b..000000000
--- a/changelog.d/3306.docs.rst
+++ /dev/null
@@ -1 +0,0 @@
-Add "Fork me on GitHub" ribbon in top right corner of the docs page.
\ No newline at end of file
diff --git a/changelog.d/3335.enhance.rst b/changelog.d/3335.enhance.rst
deleted file mode 100644
index 316decb0f..000000000
--- a/changelog.d/3335.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-make typehints accessible to cog developers
diff --git a/changelog.d/3368.docs.rst b/changelog.d/3368.docs.rst
deleted file mode 100644
index d1f384e9b..000000000
--- a/changelog.d/3368.docs.rst
+++ /dev/null
@@ -1 +0,0 @@
-Update modlog documentation example to show "action_type" instead of "action".
\ No newline at end of file
diff --git a/changelog.d/3390.misc.rst b/changelog.d/3390.misc.rst
deleted file mode 100644
index 9167a63c8..000000000
--- a/changelog.d/3390.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixes a typo in redbot/core/commands/requires.py.
\ No newline at end of file
diff --git a/changelog.d/audio/3140.enhance.1.rst b/changelog.d/audio/3140.enhance.1.rst
deleted file mode 100644
index 81570e412..000000000
--- a/changelog.d/audio/3140.enhance.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Update the help strings for ``[p]audioset emptydisconnect``.
diff --git a/changelog.d/audio/3328.hotfix.1.rst b/changelog.d/audio/3328.hotfix.1.rst
deleted file mode 100644
index 08e7ed7b9..000000000
--- a/changelog.d/audio/3328.hotfix.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed an attribute error that can be raised on play commands for spotify tracks.
\ No newline at end of file
diff --git a/changelog.d/audio/3328.hotfix.2.rst b/changelog.d/audio/3328.hotfix.2.rst
deleted file mode 100644
index 039b506e0..000000000
--- a/changelog.d/audio/3328.hotfix.2.rst
+++ /dev/null
@@ -1 +0,0 @@
-Check data before it is inserted into the database to avoid corruption.
\ No newline at end of file
diff --git a/changelog.d/audio/3337.misc.1.rst b/changelog.d/audio/3337.misc.1.rst
deleted file mode 100644
index f39071842..000000000
--- a/changelog.d/audio/3337.misc.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Removed a duplication of track search prefixes.
\ No newline at end of file
diff --git a/changelog.d/audio/3337.misc.2.rst b/changelog.d/audio/3337.misc.2.rst
deleted file mode 100644
index 25985f91d..000000000
--- a/changelog.d/audio/3337.misc.2.rst
+++ /dev/null
@@ -1 +0,0 @@
-Changed and handled the `V2_COMPACT` LoadType to use the correct `V2_COMPAT` type.
\ No newline at end of file
diff --git a/changelog.d/audio/3342.enhance.1.rst b/changelog.d/audio/3342.enhance.1.rst
deleted file mode 100644
index 38cab120a..000000000
--- a/changelog.d/audio/3342.enhance.1.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Reduce some cooldowns on playlist commands and stop them triggering before command parsing.
-
diff --git a/changelog.d/audio/3344.enhance.1.rst b/changelog.d/audio/3344.enhance.1.rst
deleted file mode 100644
index 124a7b894..000000000
--- a/changelog.d/audio/3344.enhance.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixes the messages for playlists.
\ No newline at end of file
diff --git a/changelog.d/audio/3345.enhance.1.rst b/changelog.d/audio/3345.enhance.1.rst
deleted file mode 100644
index 1a1767590..000000000
--- a/changelog.d/audio/3345.enhance.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fix an issues with the formatting of non existing local tracks.
\ No newline at end of file
diff --git a/changelog.d/audio/3349.bugfix.1.rst b/changelog.d/audio/3349.bugfix.1.rst
deleted file mode 100644
index 193d44958..000000000
--- a/changelog.d/audio/3349.bugfix.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed a bug where ``[p]audioset dc`` didn't disconnect the bot.
\ No newline at end of file
diff --git a/changelog.d/audio/3373.bugfix.1.rst b/changelog.d/audio/3373.bugfix.1.rst
deleted file mode 100644
index 3f421e0c7..000000000
--- a/changelog.d/audio/3373.bugfix.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-``[p]bumpplay`` now shows the current remaining time until the bumped track is played.
\ No newline at end of file
diff --git a/changelog.d/warnings/2900.enhance.rst b/changelog.d/warnings/2900.enhance.rst
deleted file mode 100644
index 1c6f7599c..000000000
--- a/changelog.d/warnings/2900.enhance.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-``[p]warnings`` can now be used by users that have the permission to use it from the Permissions cog, in order to check another user's warnings.
-``[p]mywarnings`` can now be used by any user (instead of ``[p]warnings`` previously) to check their own warnings.
diff --git a/docs/framework_commands.rst b/docs/framework_commands.rst
index 5501cd03e..63e29f34a 100644
--- a/docs/framework_commands.rst
+++ b/docs/framework_commands.rst
@@ -15,6 +15,7 @@ extend functionlities used throughout the bot, as outlined below.
.. autoclass:: redbot.core.commands.Command
:members:
+ :inherited-members: format_help_for_context
.. autoclass:: redbot.core.commands.Group
:members:
From 12da3bd89e8613e2364186365914352438aa12ae Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sat, 18 Jan 2020 02:06:32 +0100
Subject: [PATCH 48/91] Update changelog_3_2_0.rst (#3405)
---
docs/changelog_3_2_0.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/changelog_3_2_0.rst b/docs/changelog_3_2_0.rst
index 3ac5f6cf6..4eabfb12e 100644
--- a/docs/changelog_3_2_0.rst
+++ b/docs/changelog_3_2_0.rst
@@ -8,8 +8,8 @@ Core Bot Changes
- Further improvements have been made to bot startup and shutdown.
- Prefixes are now cached for performance.
-- Added a means for cog creators to use a global preinvoke hook.
-- The bot now ensures it has at least the bare neccessary permissions before commands.
+- Added the means for cog creators to use a global preinvoke hook.
+- The bot now ensures it has at least the bare neccessary permissions before running commands.
- Deleting instances works as intended again.
- Sinbad stopped fighting it and embraced the entrypoint madness.
@@ -29,7 +29,7 @@ Help Formatter
- ``[botname]`` is now replaced with the bot's display name in help text.
- New features added for cog creators to further customize help behavior.
- - Check out our command reference for details.
+ - Check out our command reference for details on new ``format_help_for_context`` method.
- Embed settings are now consistent.
Downloader
@@ -45,9 +45,9 @@ Docs
There's more detail to the below changes, so go read the docs.
For some reason, documenting documentation changes is hard.
-- Added instructions about git version
+- Added instructions about git version.
- Clarified instructions for installation and update.
-- Added more detail to the API key reference.
+- Added more details to the API key reference.
- Fixed some typos and versioning mistakes.
From 826dae129e447b0bfbe5484831de33e9bafaefb8 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Fri, 17 Jan 2020 20:23:16 -0500
Subject: [PATCH 49/91] dev bump (#3406)
---
redbot/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 0e625eb21..a00782338 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.2.3"
+__version__ = "3.2.4.dev1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From 54e65082bcc94d2bae0d8d3bcc04449d8cf735d3 Mon Sep 17 00:00:00 2001
From: Flame442 <34169552+Flame442@users.noreply.github.com>
Date: Sat, 18 Jan 2020 10:10:25 -0800
Subject: [PATCH 50/91] =?UTF-8?q?[Admin]=20Notify=20when=20the=20user=20ha?=
=?UTF-8?q?s/doesn't=20have=20the=20role=20when=20att=E2=80=A6=20(#3408)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update admin.py
* Create 3010.enhance.rst
---
changelog.d/admin/3010.enhance.rst | 1 +
redbot/cogs/admin/admin.py | 18 ++++++++++++++----
2 files changed, 15 insertions(+), 4 deletions(-)
create mode 100644 changelog.d/admin/3010.enhance.rst
diff --git a/changelog.d/admin/3010.enhance.rst b/changelog.d/admin/3010.enhance.rst
new file mode 100644
index 000000000..5db617388
--- /dev/null
+++ b/changelog.d/admin/3010.enhance.rst
@@ -0,0 +1 @@
+Role granting/removing commands will now notify when the user already has/doesn't have a role when attempting to add/remove it.
diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py
index e85e302b4..245cc242e 100644
--- a/redbot/cogs/admin/admin.py
+++ b/redbot/cogs/admin/admin.py
@@ -121,8 +121,13 @@ class Admin(commands.Cog):
async def _addrole(
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
):
- if member is None:
- member = ctx.author
+ if role in member.roles:
+ await ctx.send(
+ _("{member.display_name} already has the role {role.name}.").format(
+ role=role, member=member
+ )
+ )
+ return
if check_user and not self.pass_user_hierarchy_check(ctx, role):
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
return
@@ -146,8 +151,13 @@ class Admin(commands.Cog):
async def _removerole(
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
):
- if member is None:
- member = ctx.author
+ if role not in member.roles:
+ await ctx.send(
+ _("{member.display_name} does not have the role {role.name}.").format(
+ role=role, member=member
+ )
+ )
+ return
if check_user and not self.pass_user_hierarchy_check(ctx, role):
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
return
From 7f390df8795bca69239d0c8c1381488f6f64e877 Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Sun, 19 Jan 2020 18:08:31 +0100
Subject: [PATCH 51/91] [Customcom] Fix error on exiting customcom interactive
menu. (#3417)
* [Customcom] Fixes error on exit
* Changelog
* Fixed spelling.
* Typehinting
---
changelog.d/customcom/3416.bugfix.rst | 1 +
redbot/cogs/customcom/customcom.py | 3 +++
2 files changed, 4 insertions(+)
create mode 100644 changelog.d/customcom/3416.bugfix.rst
diff --git a/changelog.d/customcom/3416.bugfix.rst b/changelog.d/customcom/3416.bugfix.rst
new file mode 100644
index 000000000..672d5f032
--- /dev/null
+++ b/changelog.d/customcom/3416.bugfix.rst
@@ -0,0 +1 @@
+Fixes error on exiting customcom interactive menu.
\ No newline at end of file
diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py
index 5b163f046..b9c674559 100644
--- a/redbot/cogs/customcom/customcom.py
+++ b/redbot/cogs/customcom/customcom.py
@@ -227,6 +227,9 @@ class CustomCommands(commands.Cog):
await ctx.send(_("There already exists a bot command with the same name."))
return
responses = await self.commandobj.get_responses(ctx=ctx)
+ if not responses:
+ await ctx.send(_("Custom command process cancelled."))
+ return
try:
await self.commandobj.create(ctx=ctx, command=command, response=responses)
await ctx.send(_("Custom command successfully added."))
From b085c1501fa7b4affe649f01eb294ccec0864cb5 Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Mon, 20 Jan 2020 22:49:46 +0100
Subject: [PATCH 52/91] [General] Max amount to roll (#3395)
* [General] Rolls max amount
Adds max amount to roll.
* Removed redundant code.
* QA changes
* Add typehinting.
---
changelog.d/general/3284.bugfix.rst | 1 +
redbot/cogs/general/general.py | 14 ++++++++++++--
2 files changed, 13 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/general/3284.bugfix.rst
diff --git a/changelog.d/general/3284.bugfix.rst b/changelog.d/general/3284.bugfix.rst
new file mode 100644
index 000000000..8b89090ce
--- /dev/null
+++ b/changelog.d/general/3284.bugfix.rst
@@ -0,0 +1 @@
+[General] Adds a maximum amount to roll command.
\ No newline at end of file
diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py
index 0814525a9..1fbd8f578 100644
--- a/redbot/cogs/general/general.py
+++ b/redbot/cogs/general/general.py
@@ -2,6 +2,7 @@ import datetime
import time
from enum import Enum
from random import randint, choice
+from typing import Final
import aiohttp
import discord
from redbot.core import commands
@@ -31,6 +32,9 @@ class RPSParser:
self.choice = None
+MAX_ROLL: Final[int] = 2 ** 64 - 1
+
+
@cog_i18n(_)
class General(commands.Cog):
"""General commands."""
@@ -87,15 +91,21 @@ class General(commands.Cog):
`` defaults to 100.
"""
author = ctx.author
- if number > 1:
+ if 1 < number <= MAX_ROLL:
n = randint(1, number)
await ctx.send(
"{author.mention} :game_die: {n} :game_die:".format(
author=author, n=humanize_number(n)
)
)
- else:
+ elif number <= 1:
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
+ else:
+ await ctx.send(
+ _("{author.mention} Max allowed number is {maxamount}.").format(
+ author=author, maxamount=humanize_number(MAX_ROLL)
+ )
+ )
@commands.command()
async def flip(self, ctx, user: discord.Member = None):
From 8f04fd436f70e591e21a54345885c13137089c2c Mon Sep 17 00:00:00 2001
From: Flame442 <34169552+Flame442@users.noreply.github.com>
Date: Mon, 20 Jan 2020 14:09:17 -0800
Subject: [PATCH 53/91] Catches `discord.NotFound` in `utils.mod.mass_purge`
(#3414)
* Catches `discord.NotFound` in `mass_purge`
* Create 3378.bugfix.rst
---
changelog.d/3378.bugfix.rst | 1 +
redbot/core/utils/mod.py | 11 ++++++-----
2 files changed, 7 insertions(+), 5 deletions(-)
create mode 100644 changelog.d/3378.bugfix.rst
diff --git a/changelog.d/3378.bugfix.rst b/changelog.d/3378.bugfix.rst
new file mode 100644
index 000000000..caff4e2ef
--- /dev/null
+++ b/changelog.d/3378.bugfix.rst
@@ -0,0 +1 @@
+Fixed an error when ``redbot.core.utils.mod.mass_purge`` is passed ``COUNT % 100 == 1`` messages AND the last message in the list does not exist.
diff --git a/redbot/core/utils/mod.py b/redbot/core/utils/mod.py
index 030eaa3ba..2b5b1df10 100644
--- a/redbot/core/utils/mod.py
+++ b/redbot/core/utils/mod.py
@@ -38,12 +38,13 @@ async def mass_purge(messages: List[discord.Message], channel: discord.TextChann
"""
while messages:
- if len(messages) > 1:
+ # discord.NotFound can be raised when `len(messages) == 1` and the message does not exist.
+ # As a result of this obscure behavior, this error needs to be caught just in case.
+ try:
await channel.delete_messages(messages[:100])
- messages = messages[100:]
- else:
- await messages[0].delete()
- messages = []
+ except discord.errors.HTTPException:
+ pass
+ messages = messages[100:]
await asyncio.sleep(1.5)
From c7fd64e0c85f0e22ba8b2afbb286836f677e554a Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 20 Jan 2020 23:09:55 +0100
Subject: [PATCH 54/91] [Downloader] Improve `InstalledCog` converter's error
message (#3409)
* Update converters.py
* Create 3409.misc.rst
---
changelog.d/downloader/3409.misc.rst | 1 +
redbot/cogs/downloader/converters.py | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/downloader/3409.misc.rst
diff --git a/changelog.d/downloader/3409.misc.rst b/changelog.d/downloader/3409.misc.rst
new file mode 100644
index 000000000..d2bc0e365
--- /dev/null
+++ b/changelog.d/downloader/3409.misc.rst
@@ -0,0 +1 @@
+Improve error message when user passes cog that isn't installed to a command that only accepts installed cogs.
diff --git a/redbot/cogs/downloader/converters.py b/redbot/cogs/downloader/converters.py
index 5b7357579..483918d4a 100644
--- a/redbot/cogs/downloader/converters.py
+++ b/redbot/cogs/downloader/converters.py
@@ -15,6 +15,8 @@ class InstalledCog(InstalledModule):
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
if cog is None:
- raise commands.BadArgument(_("That cog is not installed"))
+ raise commands.BadArgument(
+ _("Cog `{cog_name}` is not installed.").format(cog_name=arg)
+ )
return cog
From 77235f77505835d2a5eecb6f5c75caa4505e1db3 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 20 Jan 2020 17:23:15 -0500
Subject: [PATCH 55/91] [commands] Implement `__call__` to `commands.Command`
(#3241)
* This is technically awesome, but let's not document it for public use rn
* changelog
---
changelog.d/3241.misc.rst | 1 +
redbot/core/commands/commands.py | 11 +++++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/3241.misc.rst
diff --git a/changelog.d/3241.misc.rst b/changelog.d/3241.misc.rst
new file mode 100644
index 000000000..ea372bcf7
--- /dev/null
+++ b/changelog.d/3241.misc.rst
@@ -0,0 +1 @@
+implements ``__call__`` for commands
diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py
index fcd5c390e..93c78b3e9 100644
--- a/redbot/core/commands/commands.py
+++ b/redbot/core/commands/commands.py
@@ -201,6 +201,13 @@ class Command(CogCommandMixin, commands.Command):
"""
+ def __call__(self, *args, **kwargs):
+ if self.cog:
+ # We need to inject cog as self here
+ return self.callback(self.cog, *args, **kwargs)
+ else:
+ return self.callback(*args, **kwargs)
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._help_override = kwargs.pop("help_override", None)
@@ -637,7 +644,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
@property
def all_commands(self) -> Dict[str, Command]:
"""
- This does not have identical behavior to
+ This does not have identical behavior to
Group.all_commands but should return what you expect
"""
return {cmd.name: cmd for cmd in self.__cog_commands__}
@@ -653,7 +660,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
"""
This really just exists to allow easy use with other methods using can_run
on commands and groups such as help formatters.
-
+
kwargs used in that won't apply here as they don't make sense to,
but will be swallowed silently for a compatible signature for ease of use.
From e1a110b1bf9a3a3b136a63e0264c79b72707b6cf Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Wed, 22 Jan 2020 11:00:52 +0100
Subject: [PATCH 56/91] [Misc] Typo fixes (#3427)
* [Misc] Typo fixes
* Changelog
* Trivia list
* Update 3427.misc.rst
* Changelog
---
changelog.d/3427.misc.rst | 1 +
changelog.d/trivia/3427.bugfix.rst | 1 +
docs/framework_apikeys.rst | 2 +-
docs/framework_commands.rst | 2 +-
redbot/cogs/alias/alias.py | 2 +-
redbot/cogs/mod/events.py | 2 +-
redbot/cogs/mod/kickban.py | 2 +-
redbot/cogs/trivia/data/lists/clashroyale.yaml | 2 +-
redbot/cogs/trivia/data/lists/entertainment.yaml | 2 +-
redbot/core/drivers/base.py | 2 +-
redbot/core/drivers/json.py | 2 +-
redbot/core/utils/antispam.py | 2 +-
12 files changed, 12 insertions(+), 10 deletions(-)
create mode 100644 changelog.d/3427.misc.rst
create mode 100644 changelog.d/trivia/3427.bugfix.rst
diff --git a/changelog.d/3427.misc.rst b/changelog.d/3427.misc.rst
new file mode 100644
index 000000000..4ff7f6324
--- /dev/null
+++ b/changelog.d/3427.misc.rst
@@ -0,0 +1 @@
+Typo fixes in doc strings.
\ No newline at end of file
diff --git a/changelog.d/trivia/3427.bugfix.rst b/changelog.d/trivia/3427.bugfix.rst
new file mode 100644
index 000000000..7fc6be355
--- /dev/null
+++ b/changelog.d/trivia/3427.bugfix.rst
@@ -0,0 +1 @@
+Trivia list typo fixes.
\ No newline at end of file
diff --git a/docs/framework_apikeys.rst b/docs/framework_apikeys.rst
index 2a67d9c45..adc72a7ee 100644
--- a/docs/framework_apikeys.rst
+++ b/docs/framework_apikeys.rst
@@ -6,7 +6,7 @@ Shared API Keys
Red has a central API key storage utilising the core bots config. This allows cog creators to add a single location to store API keys for their cogs which may be shared between other cogs.
-There needs to be some consistency between cog creators when using shared API keys between cogs. To help make this easier service should be all **lowercase** and the key names should match the naming convetion of the API being accessed.
+There needs to be some consistency between cog creators when using shared API keys between cogs. To help make this easier service should be all **lowercase** and the key names should match the naming convention of the API being accessed.
Example:
diff --git a/docs/framework_commands.rst b/docs/framework_commands.rst
index 63e29f34a..19a124ecd 100644
--- a/docs/framework_commands.rst
+++ b/docs/framework_commands.rst
@@ -7,7 +7,7 @@ Commands Package
This package acts almost identically to :doc:`discord.ext.commands `; i.e.
all of the attributes from discord.py's are also in ours.
Some of these attributes, however, have been slightly modified, while others have been added to
-extend functionlities used throughout the bot, as outlined below.
+extend functionalities used throughout the bot, as outlined below.
.. autofunction:: redbot.core.commands.command
diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py
index d695c7bd0..9c14f9352 100644
--- a/redbot/cogs/alias/alias.py
+++ b/redbot/cogs/alias/alias.py
@@ -90,7 +90,7 @@ class Alias(commands.Cog):
def is_command(self, alias_name: str) -> bool:
"""
- The logic here is that if this returns true, the name shouldnt be used for an alias
+ The logic here is that if this returns true, the name should not be used for an alias
The function name can be changed when alias is reworked
"""
command = self.bot.get_command(alias_name)
diff --git a/redbot/cogs/mod/events.py b/redbot/cogs/mod/events.py
index d63a6c74f..be6f4f2b1 100644
--- a/redbot/cogs/mod/events.py
+++ b/redbot/cogs/mod/events.py
@@ -101,7 +101,7 @@ class Events(MixinMeta):
while None in name_list: # clean out null entries from a bug
name_list.remove(None)
if after.name in name_list:
- # Ensure order is maintained without duplicates occuring
+ # Ensure order is maintained without duplicates occurring
name_list.remove(after.name)
name_list.append(after.name)
while len(name_list) > 20:
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index 37182518c..2b7c57d3f 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -95,7 +95,7 @@ class KickBanMixin(MixinMeta):
except discord.Forbidden:
return _("I'm not allowed to do that.")
except Exception as e:
- return e # TODO: impproper return type? Is this intended to be re-raised?
+ return e # TODO: improper return type? Is this intended to be re-raised?
if create_modlog_case:
try:
diff --git a/redbot/cogs/trivia/data/lists/clashroyale.yaml b/redbot/cogs/trivia/data/lists/clashroyale.yaml
index f353bbdd5..e3a1993ec 100644
--- a/redbot/cogs/trivia/data/lists/clashroyale.yaml
+++ b/redbot/cogs/trivia/data/lists/clashroyale.yaml
@@ -61,7 +61,7 @@ At which Arena can you unlock X-Bow?:
- 6
- Builder's Workshop
At which Arena do you get a chance for Legendary cards to appear in the shop?:
-- Hog Mountian
+- Hog Mountain
- A10
- 10
- Arena 10
diff --git a/redbot/cogs/trivia/data/lists/entertainment.yaml b/redbot/cogs/trivia/data/lists/entertainment.yaml
index b7726b848..4bf1e7c16 100644
--- a/redbot/cogs/trivia/data/lists/entertainment.yaml
+++ b/redbot/cogs/trivia/data/lists/entertainment.yaml
@@ -375,7 +375,7 @@ Porky Pig had a girlfriend named ________?:
Randy Travis said his love was 'deeper than the ______'?:
- Holler
Richard Strauss' majestic overture "Also Sprach Zarathustra" was the theme music for which Stanley Kubrick film?:
-- "2001: A Space Odyessy"
+- "2001: A Space Odyssey"
Rolling Stones first hit was written by what group?:
- The Beatles
Russian modernist Igor _________?:
diff --git a/redbot/core/drivers/base.py b/redbot/core/drivers/base.py
index cc408b131..3d4ac42e3 100644
--- a/redbot/core/drivers/base.py
+++ b/redbot/core/drivers/base.py
@@ -271,7 +271,7 @@ class BaseDriver(abc.ABC):
The driver must be initialized before this operation.
- The BaseDriver provides a generic method which may be overriden
+ The BaseDriver provides a generic method which may be overridden
by subclasses.
Parameters
diff --git a/redbot/core/drivers/json.py b/redbot/core/drivers/json.py
index 60ee86ae5..e1196ff4a 100644
--- a/redbot/core/drivers/json.py
+++ b/redbot/core/drivers/json.py
@@ -217,7 +217,7 @@ class JsonDriver(BaseDriver):
def _save_json(path: Path, data: Dict[str, Any]) -> None:
"""
- This fsync stuff here is entirely neccessary.
+ This fsync stuff here is entirely necessary.
On windows, it is not available in entirety.
If a windows user ends up with tons of temp files, they should consider hosting on
diff --git a/redbot/core/utils/antispam.py b/redbot/core/utils/antispam.py
index 8135b8330..b2b6c9b33 100644
--- a/redbot/core/utils/antispam.py
+++ b/redbot/core/utils/antispam.py
@@ -21,7 +21,7 @@ class AntiSpam:
# TODO : Decorator interface for command check using `spammy`
# with insertion of the antispam element into context
- # for manual stamping on succesful command completion
+ # for manual stamping on successful command completion
default_intervals = [
(timedelta(seconds=5), 3),
From 8570971f68111c7f8f8257087d066221164ab3b7 Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Wed, 22 Jan 2020 18:52:06 +0100
Subject: [PATCH 57/91] [Core] Make bot name adjustable in bot. (#3429)
* First draft
* Up for discussion
* Revert "Up for discussion"
This reverts commit 2f00b7ded8c0d03a7a06b7b973438cbd87d1f447.
---
redbot/core/core_commands.py | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 02ed1b738..5c1cac0d8 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -358,7 +358,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
async def uptime(self, ctx: commands.Context):
- """Shows Red's uptime"""
+ """Shows [botname]'s uptime"""
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
delta = datetime.datetime.utcnow() - self.bot.uptime
uptime_str = humanize_timedelta(timedelta=delta) or _("Less than one second")
@@ -471,7 +471,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@commands.check(CoreLogic._can_get_invite_url)
async def invite(self, ctx):
- """Show's Red's invite url"""
+ """Show's [botname]'s invite url"""
try:
await ctx.author.send(await self._invite_url())
except discord.errors.Forbidden:
@@ -834,7 +834,7 @@ class Core(commands.Cog, CoreLogic):
@commands.group(name="set")
async def _set(self, ctx: commands.Context):
- """Changes Red's settings"""
+ """Changes [botname]'s settings"""
if ctx.invoked_subcommand is None:
if ctx.guild:
guild = ctx.guild
@@ -1020,7 +1020,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.is_owner()
async def avatar(self, ctx: commands.Context, url: str):
- """Sets Red's avatar"""
+ """Sets [botname]'s avatar"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
data = await r.read()
@@ -1044,7 +1044,7 @@ class Core(commands.Cog, CoreLogic):
@checks.bot_in_a_guild()
@checks.is_owner()
async def _game(self, ctx: commands.Context, *, game: str = None):
- """Sets Red's playing status"""
+ """Sets [botname]'s playing status"""
if game:
game = discord.Game(name=game)
@@ -1058,7 +1058,7 @@ class Core(commands.Cog, CoreLogic):
@checks.bot_in_a_guild()
@checks.is_owner()
async def _listening(self, ctx: commands.Context, *, listening: str = None):
- """Sets Red's listening status"""
+ """Sets [botname]'s listening status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
if listening:
@@ -1072,7 +1072,7 @@ class Core(commands.Cog, CoreLogic):
@checks.bot_in_a_guild()
@checks.is_owner()
async def _watching(self, ctx: commands.Context, *, watching: str = None):
- """Sets Red's watching status"""
+ """Sets [botname]'s watching status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
if watching:
@@ -1086,7 +1086,7 @@ class Core(commands.Cog, CoreLogic):
@checks.bot_in_a_guild()
@checks.is_owner()
async def status(self, ctx: commands.Context, *, status: str):
- """Sets Red's status
+ """Sets [botname]'s status
Available statuses:
online
@@ -1115,7 +1115,7 @@ class Core(commands.Cog, CoreLogic):
@checks.bot_in_a_guild()
@checks.is_owner()
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
- """Sets Red's streaming status
+ """Sets [botname]'s streaming status
Leaving both streamer and stream_title empty will clear it."""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None
@@ -1136,7 +1136,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="username", aliases=["name"])
@checks.is_owner()
async def _username(self, ctx: commands.Context, *, username: str):
- """Sets Red's username"""
+ """Sets [botname]'s username"""
try:
await self._name(name=username)
except discord.HTTPException:
@@ -1155,7 +1155,7 @@ class Core(commands.Cog, CoreLogic):
@checks.admin()
@commands.guild_only()
async def _nickname(self, ctx: commands.Context, *, nickname: str = None):
- """Sets Red's nickname"""
+ """Sets [botname]'s nickname"""
try:
await ctx.guild.me.edit(nick=nickname)
except discord.Forbidden:
@@ -1166,7 +1166,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["prefixes"])
@checks.is_owner()
async def prefix(self, ctx: commands.Context, *prefixes: str):
- """Sets Red's global prefix(es)"""
+ """Sets [botname]'s global prefix(es)"""
if not prefixes:
await ctx.send_help()
return
@@ -1177,7 +1177,7 @@ class Core(commands.Cog, CoreLogic):
@checks.admin()
@commands.guild_only()
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
- """Sets Red's server prefix(es)"""
+ """Sets [botname]'s server prefix(es)"""
if not prefixes:
await ctx.bot._prefix_cache.set_prefixes(guild=ctx.guild, prefixes=[])
await ctx.send(_("Guild prefixes have been reset."))
From ee53d50c3aec2c2bbbe3a55df603635bb4a8b80d Mon Sep 17 00:00:00 2001
From: DevilXD
Date: Wed, 22 Jan 2020 23:15:51 +0100
Subject: [PATCH 58/91] Help delete pages delay feature (#3433)
* Added 'deletedelay' feature for help
* Fixes
* More fixes
* Use better message when disabling
* Added changelog entry
* Addressed feedback
* Improved the pages check
* Added additional command check
* Improved command description
* Final feedback improvements
---
changelog.d/3433.feature.rst | 1 +
redbot/core/bot.py | 11 +++---
redbot/core/commands/help.py | 71 ++++++++++++++++++++++--------------
redbot/core/core_commands.py | 24 ++++++++++++
4 files changed, 75 insertions(+), 32 deletions(-)
create mode 100644 changelog.d/3433.feature.rst
diff --git a/changelog.d/3433.feature.rst b/changelog.d/3433.feature.rst
new file mode 100644
index 000000000..7a7d6446f
--- /dev/null
+++ b/changelog.d/3433.feature.rst
@@ -0,0 +1 @@
+Added a ``[p]helpset deletedelay`` command, that lets you set a delay (in seconds) after which help messages / pages will be deleted.
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 5ad380f62..c3d609e19 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -88,6 +88,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
custom_info=None,
help__page_char_limit=1000,
help__max_pages_in_guild=2,
+ help__delete_delay=0,
help__use_menus=False,
help__show_hidden=False,
help__verify_checks=True,
@@ -186,9 +187,9 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
def before_invoke(self, coro: T_BIC) -> T_BIC:
"""
Overridden decorator method for Red's ``before_invoke`` behavior.
-
+
This can safely be used purely functionally as well.
-
+
3rd party cogs should remove any hooks which they register at unload
using `remove_before_invoke_hook`
@@ -199,12 +200,12 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
only called if all checks and argument parsing procedures pass
without error. If any check or argument parsing procedures fail
then the hooks are not called.
-
+
Parameters
----------
coro: Callable[[commands.Context], Awaitable[Any]]
The coroutine to register as the pre-invoke hook.
-
+
Raises
------
TypeError
@@ -298,7 +299,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
------
TypeError
Did not provide ``who`` or ``who_id``
-
+
Returns
-------
bool
diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py
index 783ef1dc5..acb5d6d61 100644
--- a/redbot/core/commands/help.py
+++ b/redbot/core/commands/help.py
@@ -44,6 +44,7 @@ from . import commands
from .context import Context
from ..i18n import Translator
from ..utils import menus
+from ..utils.mod import mass_purge
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
from ..utils.chat_formatting import box, pagify
@@ -627,36 +628,52 @@ class RedHelpFormatter:
Sends pages based on settings.
"""
- if not (
- ctx.channel.permissions_for(ctx.me).add_reactions
- and await ctx.bot._config.help.use_menus()
- ):
+ # save on config calls
+ config_help = await ctx.bot._config.help()
+ channel_permissions = ctx.channel.permissions_for(ctx.me)
- max_pages_in_guild = await ctx.bot._config.help.max_pages_in_guild()
- destination = ctx.author if len(pages) > max_pages_in_guild else ctx
+ if not (channel_permissions.add_reactions and config_help["use_menus"]):
- if embed:
- for page in pages:
- try:
- await destination.send(embed=page)
- except discord.Forbidden:
- return await ctx.send(
- T_(
- "I couldn't send the help message to you in DM. "
- "Either you blocked me or you disabled DMs in this server."
- )
- )
- else:
- for page in pages:
- try:
- await destination.send(page)
- except discord.Forbidden:
- return await ctx.send(
- T_(
- "I couldn't send the help message to you in DM. "
- "Either you blocked me or you disabled DMs in this server."
- )
+ max_pages_in_guild = config_help["max_pages_in_guild"]
+ use_DMs = len(pages) > max_pages_in_guild
+ destination = ctx.author if use_DMs else ctx.channel
+ delete_delay = config_help["delete_delay"]
+
+ messages: List[discord.Message] = []
+ for page in pages:
+ try:
+ if embed:
+ msg = await destination.send(embed=page)
+ else:
+ msg = await destination.send(page)
+ except discord.Forbidden:
+ return await ctx.send(
+ T_(
+ "I couldn't send the help message to you in DM. "
+ "Either you blocked me or you disabled DMs in this server."
)
+ )
+ else:
+ messages.append(msg)
+
+ # The if statement takes into account that 'destination' will be
+ # the context channel in non-DM context, reusing 'channel_permissions' to avoid
+ # computing the permissions twice.
+ if (
+ not use_DMs # we're not in DMs
+ and delete_delay > 0 # delete delay is enabled
+ and channel_permissions.manage_messages # we can manage messages here
+ ):
+
+ # We need to wrap this in a task to not block after-sending-help interactions.
+ # The channel has to be TextChannel as we can't bulk-delete from DMs
+ async def _delete_delay_help(
+ channel: discord.TextChannel, messages: List[discord.Message], delay: int
+ ):
+ await asyncio.sleep(delay)
+ await mass_purge(messages, channel)
+
+ asyncio.create_task(_delete_delay_help(destination, messages, delete_delay))
else:
# Specifically ensuring the menu's message is sent prior to returning
m = await (ctx.send(embed=pages[0]) if embed else ctx.send(pages[0]))
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 5c1cac0d8..31872df37 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -1368,6 +1368,30 @@ class Core(commands.Cog, CoreLogic):
await ctx.bot._config.help.max_pages_in_guild.set(pages)
await ctx.send(_("Done. The page limit has been set to {}.").format(pages))
+ @helpset.command(name="deletedelay")
+ @commands.bot_has_permissions(manage_messages=True)
+ async def helpset_deletedelay(self, ctx: commands.Context, seconds: int):
+ """Set the delay after which help pages will be deleted.
+
+ The setting is disabled by default, and only applies to non-menu help,
+ sent in server text channels.
+ Setting the delay to 0 disables this feature.
+
+ The bot has to have MANAGE_MESSAGES permission for this to work.
+ """
+ if seconds < 0:
+ await ctx.send(_("You must give a value of zero or greater!"))
+ return
+ if seconds > 60 * 60 * 24 * 14: # 14 days
+ await ctx.send(_("The delay cannot be longer than 14 days!"))
+ return
+
+ await ctx.bot._config.help.delete_delay.set(seconds)
+ if seconds == 0:
+ await ctx.send(_("Done. Help messages will not be deleted now."))
+ else:
+ await ctx.send(_("Done. The delete delay has been set to {} seconds.").format(seconds))
+
@helpset.command(name="tagline")
async def helpset_tagline(self, ctx: commands.Context, *, tagline: str = None):
"""
From 40c0d8d83b1a9b133f1e9aa367f9789d56f98fc4 Mon Sep 17 00:00:00 2001
From: zephyrkul
Date: Wed, 22 Jan 2020 21:20:35 -0700
Subject: [PATCH 59/91] [systemd] fix `which` cmd for pyenv (#3434)
---
docs/autostart_systemd.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/autostart_systemd.rst b/docs/autostart_systemd.rst
index 06d5b141a..fcf8111e9 100644
--- a/docs/autostart_systemd.rst
+++ b/docs/autostart_systemd.rst
@@ -14,11 +14,11 @@ In order to create the service file, you will first need the location of your :c
# If redbot is installed in a virtualenv
source redenv/bin/activate
+ which python
# If you are using pyenv
pyenv shell
-
- which python
+ pyenv which python
Then create the new service file:
From 17553341248d5c8dcfb8b2a18707040281b03abd Mon Sep 17 00:00:00 2001
From: Ianardo DiCaprio <43935737+Ianardo-DiCaprio@users.noreply.github.com>
Date: Thu, 23 Jan 2020 19:37:11 +0000
Subject: [PATCH 60/91] [Mod] Default days in `[p]ban` command are now
configurable (#2930)
* Initial Commit
* Added changelog
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Rename 2930.enhance.rst.txt to 2930.enhance.rst
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
---
changelog.d/mod/2930.enhance.rst | 1 +
redbot/cogs/mod/kickban.py | 8 +++++---
redbot/cogs/mod/mod.py | 1 +
redbot/cogs/mod/settings.py | 35 +++++++++++++++++++++++++++-----
4 files changed, 37 insertions(+), 8 deletions(-)
create mode 100644 changelog.d/mod/2930.enhance.rst
diff --git a/changelog.d/mod/2930.enhance.rst b/changelog.d/mod/2930.enhance.rst
new file mode 100644
index 000000000..c85c0238d
--- /dev/null
+++ b/changelog.d/mod/2930.enhance.rst
@@ -0,0 +1 @@
+Added a defaultdays command to set the amount of days for the ban commands to use for days of messages deleted when days isn't used in the command itself.
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index 2b7c57d3f..2a2816227 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -218,15 +218,17 @@ class KickBanMixin(MixinMeta):
self,
ctx: commands.Context,
user: discord.Member,
- days: Optional[int] = 0,
+ days: Optional[int] = None,
*,
reason: str = None,
):
"""Ban a user from this server and optionally delete days of messages.
If days is not a number, it's treated as the first word of the reason.
- Minimum 0 days, maximum 7. Defaults to 0."""
-
+ Minimum 0 days, maximum 7. If not specified, defaultdays setting will be used instead."""
+ guild = ctx.guild
+ if days is None:
+ days = await self.settings.guild(guild).default_days()
result = await self.ban_user(
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
)
diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py
index 95ad52baa..37527d2b6 100644
--- a/redbot/cogs/mod/mod.py
+++ b/redbot/cogs/mod/mod.py
@@ -53,6 +53,7 @@ class Mod(
"delete_delay": -1,
"reinvite_on_unban": False,
"current_tempbans": [],
+ "default_days": 0,
}
default_channel_settings = {"ignored": False}
diff --git a/redbot/cogs/mod/settings.py b/redbot/cogs/mod/settings.py
index e7923c57e..5de256056 100644
--- a/redbot/cogs/mod/settings.py
+++ b/redbot/cogs/mod/settings.py
@@ -21,11 +21,13 @@ class ModSettings(MixinMeta):
if ctx.invoked_subcommand is None:
guild = ctx.guild
# Display current settings
- delete_repeats = await self.settings.guild(guild).delete_repeats()
- ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
- respect_hierarchy = await self.settings.guild(guild).respect_hierarchy()
- delete_delay = await self.settings.guild(guild).delete_delay()
- reinvite_on_unban = await self.settings.guild(guild).reinvite_on_unban()
+ data = await self.settings.guild(guild).all()
+ delete_repeats = data["delete_repeats"]
+ ban_mention_spam = data["ban_mention_spam"]
+ respect_hierarchy = data["respect_hierarchy"]
+ delete_delay = data["delete_delay"]
+ reinvite_on_unban = data["reinvite_on_unban"]
+ default_days = data["default_days"]
msg = ""
msg += _("Delete repeats: {num_repeats}\n").format(
num_repeats=_("after {num} repeats").format(num=delete_repeats)
@@ -48,6 +50,12 @@ class ModSettings(MixinMeta):
msg += _("Reinvite on unban: {yes_or_no}\n").format(
yes_or_no=_("Yes") if reinvite_on_unban else _("No")
)
+ if default_days:
+ msg += _(
+ "Default message history delete on ban: Previous {num_days} days\n"
+ ).format(num_days=default_days)
+ else:
+ msg += _("Default message history delete on ban: Don't delete any\n")
await ctx.send(box(msg))
@modset.command()
@@ -199,3 +207,20 @@ class ModSettings(MixinMeta):
command=f"{ctx.prefix}unban"
)
)
+
+ @modset.command()
+ @commands.guild_only()
+ async def defaultdays(self, ctx: commands.Context, days: int = 0):
+ """Set the default number of days worth of messages to be deleted when a user is banned.
+
+ The number of days must be between 0 and 7.
+ """
+ guild = ctx.guild
+ if not (0 <= days <= 7):
+ return await ctx.send(_("Invalid number of days. Must be between 0 and 7."))
+ await self.settings.guild(guild).default_days.set(days)
+ await ctx.send(
+ _("{days} days worth of messages will be deleted when a user is banned.").format(
+ days=days
+ )
+ )
From 0a8e7f56634669316d252f2e52240bed3758d31c Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Thu, 23 Jan 2020 22:05:50 +0000
Subject: [PATCH 61/91] stop dc interacting with repeat (#3426)
---
redbot/cogs/audio/audio.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 99714daf6..5bc2d3a8b 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -705,7 +705,6 @@ class Audio(commands.Cog):
msg += _("Auto-disconnection at queue end: {true_or_false}.").format(
true_or_false=_("Enabled") if not disconnect else _("Disabled")
)
- await self.config.guild(ctx.guild).repeat.set(not disconnect)
if disconnect is not True and autoplay is True:
msg += _("\nAuto-play has been disabled.")
await self.config.guild(ctx.guild).auto_play.set(False)
From 01c1fdfd16e3cef1927fa1d656a06f7c51b18ac0 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Fri, 24 Jan 2020 11:30:32 +0100
Subject: [PATCH 62/91] [Mod] Make `[p]hackban` use default days setting too.
(#3437)
* Update kickban.py
* freaking whitespace
---
redbot/cogs/mod/kickban.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index 2a2816227..4ee1560ae 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -246,7 +246,7 @@ class KickBanMixin(MixinMeta):
self,
ctx: commands.Context,
user_ids: commands.Greedy[RawUserIds],
- days: Optional[int] = 0,
+ days: Optional[int] = None,
*,
reason: str = None,
):
@@ -254,7 +254,6 @@ class KickBanMixin(MixinMeta):
User IDs need to be provided in order to ban
using this command"""
- days = cast(int, days)
banned = []
errors = {}
@@ -302,6 +301,9 @@ 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:
From 2a387773793e6fe8fe1418d8d99ac5ca7f87645c Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Fri, 24 Jan 2020 18:38:42 +0100
Subject: [PATCH 63/91] [Downloader] Do the initialization in task to avoid
timeout on bot startup (#3440)
* enhance(downloader): run init in task
* chore(changelog): add towncrier entry
* fix: address review
---
changelog.d/downloader/3415.bugfix.rst | 1 +
redbot/cogs/downloader/__init__.py | 2 +-
redbot/cogs/downloader/downloader.py | 14 ++++++++++++++
3 files changed, 16 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/downloader/3415.bugfix.rst
diff --git a/changelog.d/downloader/3415.bugfix.rst b/changelog.d/downloader/3415.bugfix.rst
new file mode 100644
index 000000000..305377c9c
--- /dev/null
+++ b/changelog.d/downloader/3415.bugfix.rst
@@ -0,0 +1 @@
+Downloader will now do the initialization in background to avoid timeout issues during bot startup.
\ No newline at end of file
diff --git a/redbot/cogs/downloader/__init__.py b/redbot/cogs/downloader/__init__.py
index 7c50e3395..1c8d1b710 100644
--- a/redbot/cogs/downloader/__init__.py
+++ b/redbot/cogs/downloader/__init__.py
@@ -3,5 +3,5 @@ from .downloader import Downloader
async def setup(bot):
cog = Downloader(bot)
- await cog.initialize()
bot.add_cog(cog)
+ cog.create_init_task()
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index eb063157c..c0099bf14 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -53,6 +53,8 @@ class Downloader(commands.Cog):
self._create_lib_folder()
self._repo_manager = RepoManager()
+ self._ready = asyncio.Event()
+ self._init_task = None
def _create_lib_folder(self, *, remove_first: bool = False) -> None:
if remove_first:
@@ -62,9 +64,21 @@ class Downloader(commands.Cog):
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
pass
+ async def cog_before_invoke(self, ctx: commands.Context) -> None:
+ async with ctx.typing():
+ await self._ready.wait()
+
+ def cog_unload(self):
+ if self._init_task is not None:
+ self._init_task.cancel()
+
+ def create_init_task(self):
+ self._init_task = asyncio.create_task(self.initialize())
+
async def initialize(self) -> None:
await self._repo_manager.initialize()
await self._maybe_update_config()
+ self._ready.set()
async def _maybe_update_config(self) -> None:
schema_version = await self.conf.schema_version()
From 498d0d22fbaf686195c03e808328f8f5b980dca7 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sat, 25 Jan 2020 18:57:07 -0500
Subject: [PATCH 64/91] resolves #3443 (#3447)
---
redbot/core/modlog.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/redbot/core/modlog.py b/redbot/core/modlog.py
index 700203497..6c3158f98 100644
--- a/redbot/core/modlog.py
+++ b/redbot/core/modlog.py
@@ -324,9 +324,7 @@ class Case:
if embed:
emb = discord.Embed(title=title, description=reason)
-
- if avatar_url is not None:
- emb.set_author(name=user, icon_url=avatar_url)
+ emb.set_author(name=user)
emb.add_field(name=_("Moderator"), value=moderator, inline=False)
if until and duration:
emb.add_field(name=_("Until"), value=until)
From 2ac4dde729b06d14c27a8f723abe98e104fdbac2 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sat, 25 Jan 2020 18:59:08 -0500
Subject: [PATCH 65/91] update for d.py 1.3 (#3445)
* update for d.py 1.3
* Update redbot/core/commands/commands.py
Co-Authored-By: Danny
* a few more places we use owner info
* add the cli flag + handling
* set fix
* Handle MaxConcurrencyReached.
* Bump `aiohttp-json-rpc`
Co-authored-by: Danny
Co-authored-by: Kowlin
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
---
redbot/core/bot.py | 45 ++++++++++++++++++++++++++++----
redbot/core/cli.py | 12 +++++++++
redbot/core/commands/commands.py | 23 +++++++++++-----
redbot/core/commands/requires.py | 12 +++------
redbot/core/core_commands.py | 5 +++-
redbot/core/events.py | 15 +++++++++--
setup.cfg | 6 ++---
7 files changed, 91 insertions(+), 27 deletions(-)
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index c3d609e19..5eb2b027b 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -27,6 +27,7 @@ from types import MappingProxyType
import discord
from discord.ext.commands import when_mentioned_or
+from discord.ext.commands.bot import BotBase
from . import Config, i18n, commands, errors, drivers, modlog, bank
from .cog_manager import CogManager, CogManagerUI
@@ -59,7 +60,7 @@ def _is_submodule(parent, child):
# barely spurious warning caused by our intentional shadowing
-class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: disable=no-member
+class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-member
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
@@ -150,6 +151,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
self._main_dir = bot_dir
self._cog_mgr = CogManager()
+ self._use_team_features = cli_flags.use_team_features
super().__init__(*args, help_command=None, **kwargs)
# Do not manually use the help formatter attribute here, see `send_help_for`,
# for a documented API. The internals of this object are still subject to change.
@@ -627,10 +629,42 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
global_setting = await self._config.embeds()
return global_setting
- async def is_owner(self, user) -> bool:
+ async def is_owner(self, user: Union[discord.User, discord.Member]) -> bool:
+ """
+ Determines if the user should be considered a bot owner.
+
+ This takes into account CLI flags and application ownership.
+
+ By default,
+ application team members are not considered owners,
+ while individual application owners are.
+
+ Parameters
+ ----------
+ user: Union[discord.User, discord.Member]
+
+ Returns
+ -------
+ bool
+ """
if user.id in self._co_owners:
return True
- return await super().is_owner(user)
+
+ if self.owner_id:
+ return self.owner_id == user.id
+ elif self.owner_ids:
+ return user.id in self.owner_ids
+ else:
+ app = await self.application_info()
+ if app.team:
+ if self._use_team_features:
+ self.owner_ids = ids = {m.id for m in app.team.members}
+ return user.id in ids
+ else:
+ self.owner_id = owner_id = app.owner.id
+ return user.id == owner_id
+
+ return False
async def is_admin(self, member: discord.Member) -> bool:
"""Checks if a member is an admin of their guild."""
@@ -1069,10 +1103,11 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
await self.wait_until_red_ready()
destinations = []
opt_outs = await self._config.owner_opt_out_list()
- for user_id in (self.owner_id, *self._co_owners):
+ team_ids = () if not self._use_team_features else self.owner_ids
+ for user_id in set((self.owner_id, *self._co_owners, *team_ids)):
if user_id not in opt_outs:
user = self.get_user(user_id)
- if user:
+ if user and not user.bot: # user.bot is possible with flags and teams
destinations.append(user)
else:
log.warning(
diff --git a/redbot/core/cli.py b/redbot/core/cli.py
index 9c2575795..f638530d1 100644
--- a/redbot/core/cli.py
+++ b/redbot/core/cli.py
@@ -200,6 +200,18 @@ def parse_cli_flags(args):
parser.add_argument(
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
)
+ parser.add_argument(
+ "--team-members-are-owners",
+ action="store_true",
+ dest="use_team_features",
+ default=False,
+ help=(
+ "Treat application team members as owners. "
+ "This is off by default. Owners can load and run arbitrary code. "
+ "Do not enable if you would not trust all of your team members with "
+ "all of the data on the host machine."
+ ),
+ )
args = parser.parse_args(args)
diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py
index 93c78b3e9..313f72f2f 100644
--- a/redbot/core/commands/commands.py
+++ b/redbot/core/commands/commands.py
@@ -330,15 +330,27 @@ class Command(CogCommandMixin, commands.Command):
if not change_permission_state:
ctx.permission_state = original_state
- async def _verify_checks(self, ctx):
+ async def prepare(self, ctx):
+ ctx.command = self
+
if not self.enabled:
raise commands.DisabledCommand(f"{self.name} command is disabled")
- if not (await self.can_run(ctx, change_permission_state=True)):
+ if not await self.can_run(ctx, change_permission_state=True):
raise commands.CheckFailure(
f"The check functions for command {self.qualified_name} failed."
)
+ if self.cooldown_after_parsing:
+ await self._parse_arguments(ctx)
+ self._prepare_cooldowns(ctx)
+ else:
+ self._prepare_cooldowns(ctx)
+ await self._parse_arguments(ctx)
+ if self._max_concurrency is not None:
+ await self._max_concurrency.acquire(ctx)
+ await self.call_before_hooks(ctx)
+
async def do_conversion(
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
):
@@ -625,14 +637,14 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
if self.autohelp and not self.invoke_without_command:
- await self._verify_checks(ctx)
+ await self.can_run(ctx, change_permission_state=True)
await ctx.send_help()
elif self.invoke_without_command:
# So invoke_without_command when a subcommand of this group is invoked
# will skip the the invokation of *this* command. However, because of
# how our permissions system works, we don't want it to skip the checks
# as well.
- await self._verify_checks(ctx)
+ await self.can_run(ctx, change_permission_state=True)
# this is actually why we don't prepare earlier.
await super().invoke(ctx)
@@ -778,6 +790,3 @@ class _AlwaysAvailableCommand(Command):
async def can_run(self, ctx, *args, **kwargs) -> bool:
return not ctx.author.bot
-
- async def _verify_checks(self, ctx) -> bool:
- return not ctx.author.bot
diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py
index f3614e0fa..88b348209 100644
--- a/redbot/core/commands/requires.py
+++ b/redbot/core/commands/requires.py
@@ -757,16 +757,10 @@ class _RulesDict(Dict[Union[int, str], PermState]):
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
+ invalid_keys = set(perms.keys()) - set(discord.Permissions.VALID_FLAGS)
+ if invalid_keys:
+ raise TypeError(f"Invalid perm name(s): {', '.join(invalid_keys)}")
for perm, value in perms.items():
- try:
- attr = getattr(discord.Permissions, perm)
- except AttributeError:
- attr = None
-
- if attr is None or not isinstance(attr, property):
- # We reject invalid permissions
- raise TypeError(f"Unknown permission name '{perm}'")
-
if value is not True:
# We reject any permission not specified as 'True', since this is the only value which
# makes practical sense.
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 31872df37..0cee0ce2c 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -319,7 +319,10 @@ class Core(commands.Cog, CoreLogic):
python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url)
red_version = "[{}]({})".format(__version__, red_pypi)
app_info = await self.bot.application_info()
- owner = app_info.owner
+ if app_info.team:
+ owner = app_info.team.name
+ else:
+ owner = app_info.owner
custom_info = await self.bot._config.custom_info()
async with aiohttp.ClientSession() as session:
diff --git a/redbot/core/events.py b/redbot/core/events.py
index e72c505a4..c36874a6c 100644
--- a/redbot/core/events.py
+++ b/redbot/core/events.py
@@ -49,8 +49,13 @@ def init_events(bot, cli_flags):
users = len(set([m for m in bot.get_all_members()]))
app_info = await bot.application_info()
- if bot.owner_id is None:
- bot.owner_id = app_info.owner.id
+
+ if app_info.team:
+ if bot._use_team_features:
+ bot.owner_ids = {m.id for m in app_info.team.members}
+ else:
+ if bot.owner_id is None:
+ bot.owner_id = app_info.owner.id
try:
invite_url = discord.utils.oauth_url(app_info.id)
@@ -213,6 +218,12 @@ def init_events(bot, cli_flags):
),
delete_after=error.retry_after,
)
+ elif isinstance(error, commands.MaxConcurrencyReached):
+ await ctx.send(
+ "Too many people using this command. It can only be used {} time(s) per {} concurrently.".format(
+ error.number, error.per.name
+ )
+ )
else:
log.exception(type(error).__name__, exc_info=error)
diff --git a/setup.cfg b/setup.cfg
index 4fb7d934e..6e43c7d6b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,7 +27,7 @@ packages = find_namespace:
python_requires = >=3.8.1
install_requires =
aiohttp==3.6.2
- aiohttp-json-rpc==0.12.1
+ aiohttp-json-rpc==0.12.2
aiosqlite==0.11.0
appdirs==1.4.3
apsw-wheels==3.30.1.post3
@@ -38,7 +38,7 @@ install_requires =
Click==7.0
colorama==0.4.3
contextlib2==0.5.5
- discord.py==1.2.5
+ discord.py==1.3.0
distro==1.4.0; sys_platform == "linux"
fuzzywuzzy==0.17.0
idna==2.8
@@ -46,7 +46,7 @@ install_requires =
python-Levenshtein-wheels==0.13.1
pytz==2019.3
PyYAML==5.3
- Red-Lavalink==0.4.1
+ Red-Lavalink==0.4.2
schema==0.7.1
tqdm==4.41.1
uvloop==0.14.0; sys_platform != "win32" and platform_python_implementation == "CPython"
From de4804863a77bb1d08b36f5c325681c19fe3af98 Mon Sep 17 00:00:00 2001
From: Ianardo DiCaprio <43935737+Ianardo-DiCaprio@users.noreply.github.com>
Date: Sun, 26 Jan 2020 03:18:13 +0000
Subject: [PATCH 66/91] [Mod] Option to DM user with kick/ban reason. (#2990)
* FUCK
* FUCK
* FUCK
* Update kickban.py
* Update settings.py
* Update kickban.py
* Update kickban.py
* Add files via upload
* black
* Update kickban.py
* Update kickban.py
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update settings.py
* Update kickban.py
* Update and rename 2990.enhance.rst.txt.txt to 2990.enhance.rst.txt
* Update settings.py
* Rename 2990.enhance.rst.txt to 2990.enhance.rst
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: DevilXD
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: DevilXD
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update changelog.d/mod/2990.enhance.rst
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/settings.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update kickban.py
* Update settings.py
* Update kickban.py
* Update kickban.py
* Update redbot/cogs/mod/kickban.py
Co-Authored-By: jack1142 <6032823+jack1142@users.noreply.github.com>
* Update kickban.py
* Update kickban.py
* Update mod.py
* Update settings.py
* Fix SyntaxError
* Don't pass "No reason was given." to modlog case
* Update settings.py
* Update 2990.enhance.rst
* black
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
Co-authored-by: DevilXD
---
changelog.d/mod/2990.enhance.rst | 1 +
redbot/cogs/mod/kickban.py | 30 +++++++++++++++++++++++++++++-
redbot/cogs/mod/mod.py | 1 +
redbot/cogs/mod/settings.py | 28 ++++++++++++++++++++++++++++
4 files changed, 59 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/mod/2990.enhance.rst
diff --git a/changelog.d/mod/2990.enhance.rst b/changelog.d/mod/2990.enhance.rst
new file mode 100644
index 000000000..e27d9caa3
--- /dev/null
+++ b/changelog.d/mod/2990.enhance.rst
@@ -0,0 +1 @@
+Added a ``[p]modset dm`` to toggle kick/bans sending an embed to the user detailing the reason for the kick/ban, the server they were kicked/banned from and if it was a kick or a ban.
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index 4ee1560ae..b9ce3532a 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -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
+from redbot.core.utils.chat_formatting import pagify, humanize_number, bold
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
from .abc import MixinMeta
from .converters import RawUserIds
@@ -82,6 +82,19 @@ class KickBanMixin(MixinMeta):
elif not (0 <= days <= 7):
return _("Invalid days. Must be between 0 and 7.")
+ toggle = await self.settings.guild(guild).dm_on_kickban()
+ if toggle:
+ with contextlib.suppress(discord.HTTPException):
+ em = discord.Embed(
+ title=bold(_("You have been banned from {guild}.").format(guild=guild))
+ )
+ em.add_field(
+ name=_("**Reason**"),
+ value=reason if reason is not None else _("No reason was given."),
+ inline=False,
+ )
+ await user.send(embed=em)
+
audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user.id)
@@ -186,6 +199,18 @@ class KickBanMixin(MixinMeta):
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
audit_reason = get_audit_reason(author, reason)
+ toggle = await self.settings.guild(guild).dm_on_kickban()
+ if toggle:
+ with contextlib.suppress(discord.HTTPException):
+ em = discord.Embed(
+ title=bold(_("You have been kicked from {guild}.").format(guild=guild))
+ )
+ em.add_field(
+ name=_("**Reason**"),
+ value=reason if reason is not None else _("No reason was given."),
+ inline=False,
+ )
+ await user.send(embed=em)
try:
await guild.kick(user, reason=audit_reason)
log.info("{}({}) kicked {}({})".format(author.name, author.id, user.name, user.id))
@@ -225,10 +250,13 @@ class KickBanMixin(MixinMeta):
"""Ban a user from this server and optionally delete days of messages.
If days is not a number, it's treated as the first word of the reason.
+
Minimum 0 days, maximum 7. If not specified, defaultdays setting will be used instead."""
+ author = ctx.author
guild = ctx.guild
if days is None:
days = await self.settings.guild(guild).default_days()
+
result = await self.ban_user(
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
)
diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py
index 37527d2b6..9eaa372c5 100644
--- a/redbot/cogs/mod/mod.py
+++ b/redbot/cogs/mod/mod.py
@@ -53,6 +53,7 @@ class Mod(
"delete_delay": -1,
"reinvite_on_unban": False,
"current_tempbans": [],
+ "dm_on_kickban": False,
"default_days": 0,
}
diff --git a/redbot/cogs/mod/settings.py b/redbot/cogs/mod/settings.py
index 5de256056..3da4833db 100644
--- a/redbot/cogs/mod/settings.py
+++ b/redbot/cogs/mod/settings.py
@@ -27,6 +27,7 @@ class ModSettings(MixinMeta):
respect_hierarchy = data["respect_hierarchy"]
delete_delay = data["delete_delay"]
reinvite_on_unban = data["reinvite_on_unban"]
+ dm_on_kickban = data["dm_on_kickban"]
default_days = data["default_days"]
msg = ""
msg += _("Delete repeats: {num_repeats}\n").format(
@@ -50,6 +51,9 @@ class ModSettings(MixinMeta):
msg += _("Reinvite on unban: {yes_or_no}\n").format(
yes_or_no=_("Yes") if reinvite_on_unban else _("No")
)
+ msg += _("Send message to users on kick/ban: {yes_or_no}\n").format(
+ yes_or_no=_("Yes") if dm_on_kickban else _("No")
+ )
if default_days:
msg += _(
"Default message history delete on ban: Previous {num_days} days\n"
@@ -208,6 +212,30 @@ 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.
+
+ 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.
+ """
+ guild = ctx.guild
+ if enabled is None:
+ setting = await self.settings.guild(guild).dm_on_kickban()
+ await ctx.send(
+ _("DM when kicked/banned is currently set to: {setting}").format(setting=setting)
+ )
+ return
+ await self.settings.guild(guild).dm_on_kickban.set(enabled)
+ if enabled:
+ await ctx.send(_("Bot will now attempt to send a DM to user before kick and ban."))
+ else:
+ await ctx.send(
+ _("Bot will no longer attempt to send a DM to user before kick and ban.")
+ )
+
@modset.command()
@commands.guild_only()
async def defaultdays(self, ctx: commands.Context, days: int = 0):
From 41fdcb2ae8c4d4edb83ff61ce962343c26486c7f Mon Sep 17 00:00:00 2001
From: Stonedestroyer <1307729+Stonedestroyer@users.noreply.github.com>
Date: Sun, 26 Jan 2020 18:15:22 +0100
Subject: [PATCH 67/91] [Core] Embeds toggle for channels. (#3418)
* [Core] Embedset toggle for channels.
* Typo fix
* Add to contact as well
Thanks Jack.
* Add guild only and check.
---
changelog.d/3152.feature.rst | 1 +
redbot/core/bot.py | 4 ++++
redbot/core/core_commands.py | 32 +++++++++++++++++++++++++++++++-
3 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/3152.feature.rst
diff --git a/changelog.d/3152.feature.rst b/changelog.d/3152.feature.rst
new file mode 100644
index 000000000..ae12fe46f
--- /dev/null
+++ b/changelog.d/3152.feature.rst
@@ -0,0 +1 @@
+Adds toggle for channels for embedset.
\ No newline at end of file
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 5eb2b027b..d113c9475 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -121,6 +121,7 @@ class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-mem
autoimmune_ids=[],
)
+ self._config.register_channel(embeds=None)
self._config.register_user(embeds=None)
self._config.init_custom(CUSTOM_GROUPS, 2)
@@ -622,6 +623,9 @@ class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-mem
if user_setting is not None:
return user_setting
else:
+ channel_setting = await self._config.channel(channel).embeds()
+ if channel_setting is not None:
+ return channel_setting
guild_setting = await self._config.guild(channel.guild).embeds()
if guild_setting is not None:
return guild_setting
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 0cee0ce2c..5eb7d6091 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -388,6 +388,9 @@ class Core(commands.Cog, CoreLogic):
if ctx.guild:
guild_setting = await self.bot._config.guild(ctx.guild).embeds()
text += _("Guild setting: {}\n").format(guild_setting)
+ if ctx.channel:
+ channel_setting = await self.bot._config.channel(ctx.channel).embeds()
+ text += _("Channel setting: {}\n").format(channel_setting)
user_setting = await self.bot._config.user(ctx.author).embeds()
text += _("User setting: {}").format(user_setting)
await ctx.send(box(text))
@@ -433,6 +436,31 @@ class Core(commands.Cog, CoreLogic):
)
)
+ @embedset.command(name="channel")
+ @checks.guildowner_or_permissions(administrator=True)
+ @commands.guild_only()
+ async def embedset_channel(self, ctx: commands.Context, enabled: bool = None):
+ """
+ Toggle the channel's embed setting.
+
+ If enabled is None, the setting will be unset and
+ the guild default will be used instead.
+
+ If set, this is used instead of the guild default
+ to determine whether or not to use embeds. This is
+ used for all commands done in a channel except
+ for help commands.
+ """
+ await self.bot._config.channel(ctx.channel).embeds.set(enabled)
+ if enabled is None:
+ await ctx.send(_("Embeds will now fall back to the global setting."))
+ else:
+ await ctx.send(
+ _("Embeds are now {} for this channel.").format(
+ _("enabled") if enabled else _("disabled")
+ )
+ )
+
@embedset.command(name="user")
async def embedset_user(self, ctx: commands.Context, enabled: bool = None):
"""
@@ -1484,7 +1512,9 @@ class Core(commands.Cog, CoreLogic):
if not destination.permissions_for(destination.guild.me).send_messages:
continue
if destination.permissions_for(destination.guild.me).embed_links:
- send_embed = await ctx.bot._config.guild(destination.guild).embeds()
+ send_embed = await ctx.bot._config.channel(destination).embeds()
+ if send_embed is None:
+ send_embed = await ctx.bot._config.guild(destination.guild).embeds()
else:
send_embed = False
From fc5fc08962651039305b93ef0731ba0672a9203e Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sun, 26 Jan 2020 18:16:13 +0100
Subject: [PATCH 68/91] [Downloader] Log errors from initialization task
(#3444)
* Update downloader.py
* Create 3444.misc.rst
* enhance(downloader): don't type infinitely on init error
* fix(downloader): unindent `_ready_raised` check
* Update downloader.py
---
changelog.d/downloader/3444.misc.rst | 1 +
redbot/cogs/downloader/downloader.py | 18 ++++++++++++++++++
2 files changed, 19 insertions(+)
create mode 100644 changelog.d/downloader/3444.misc.rst
diff --git a/changelog.d/downloader/3444.misc.rst b/changelog.d/downloader/3444.misc.rst
new file mode 100644
index 000000000..9a3798bfb
--- /dev/null
+++ b/changelog.d/downloader/3444.misc.rst
@@ -0,0 +1 @@
+ Log errors that may happen in initialization task.
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index c0099bf14..52dc73a1d 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -55,6 +55,7 @@ class Downloader(commands.Cog):
self._repo_manager = RepoManager()
self._ready = asyncio.Event()
self._init_task = None
+ self._ready_raised = False
def _create_lib_folder(self, *, remove_first: bool = False) -> None:
if remove_first:
@@ -67,13 +68,30 @@ class Downloader(commands.Cog):
async def cog_before_invoke(self, ctx: commands.Context) -> None:
async with ctx.typing():
await self._ready.wait()
+ if self._ready_raised:
+ await ctx.send(
+ "There was an error during Downloader's initialization."
+ " Check logs for more information."
+ )
+ raise commands.CheckFailure()
def cog_unload(self):
if self._init_task is not None:
self._init_task.cancel()
def create_init_task(self):
+ def _done_callback(task: asyncio.Task) -> None:
+ exc = task.exception()
+ if exc is not None:
+ log.error(
+ "An unexpected error occurred during Downloader's initialization.",
+ exc_info=exc,
+ )
+ self._ready_raised = True
+ self._ready.set()
+
self._init_task = asyncio.create_task(self.initialize())
+ self._init_task.add_done_callback(_done_callback)
async def initialize(self) -> None:
await self._repo_manager.initialize()
From 068585379a583a9c848e44243fab3b2a9074bef2 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sun, 26 Jan 2020 18:16:44 +0100
Subject: [PATCH 69/91] docs: deprecation of shared libraries has been
postponed to 3.4 (#3449)
---
docs/changelog_3_2_0.rst | 2 +-
docs/guide_publish_cogs.rst | 2 +-
redbot/cogs/downloader/downloader.py | 4 ++--
redbot/core/_sharedlibdeprecation.py | 2 +-
redbot/core/core_commands.py | 10 +++++-----
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/docs/changelog_3_2_0.rst b/docs/changelog_3_2_0.rst
index 4eabfb12e..ce594cde5 100644
--- a/docs/changelog_3_2_0.rst
+++ b/docs/changelog_3_2_0.rst
@@ -238,7 +238,7 @@ Removals
~~~~~~~~
- ``[p]set owner`` and ``[p]set token`` have been removed in favor of managing server side. (`#2928 `_)
-- Shared libraries are marked for removal in Red 3.3. (`#3106 `_)
+- Shared libraries are marked for removal in Red 3.4. (`#3106 `_)
- Removed ``[p]backup``. Use the cli command ``redbot-setup backup`` instead. (`#3235 `_)
- Removed the functions ``safe_delete``, ``fuzzy_command_search``, ``format_fuzzy_results`` and ``create_backup`` from ``redbot.core.utils``. (`#3240 `_)
- Removed a lot of the launcher's handled behavior. (`#3289 `_)
diff --git a/docs/guide_publish_cogs.rst b/docs/guide_publish_cogs.rst
index c5073c885..64be6b1b7 100644
--- a/docs/guide_publish_cogs.rst
+++ b/docs/guide_publish_cogs.rst
@@ -81,5 +81,5 @@ Keys specific to the cog info.json (case sensitive)
``SHARED_LIBRARY``. If ``SHARED_LIBRARY`` then ``hidden`` will be ``True``.
.. warning::
- Shared libraries are deprecated since version 3.2 and are marked for removal in version 3.3.
+ Shared libraries are deprecated since version 3.2 and are marked for removal in version 3.4.
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index 52dc73a1d..ed48f8cfd 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -29,7 +29,7 @@ _ = Translator("Downloader", __file__)
DEPRECATION_NOTICE = _(
"\n**WARNING:** The following repos are using shared libraries"
- " which are marked for removal in Red 3.3: {repo_list}.\n"
+ " which are marked for removal in Red 3.4: {repo_list}.\n"
" You should inform maintainers of these repos about this message."
)
@@ -237,7 +237,7 @@ class Downloader(commands.Cog):
await self.conf.installed_libraries.set(installed_libraries)
async def _shared_lib_load_check(self, cog_name: str) -> Optional[Repo]:
- # remove in Red 3.3
+ # remove in Red 3.4
is_installed, cog = await self.is_installed(cog_name)
# it's not gonna be None when `is_installed` is True
# if we'll use typing_extensions in future, `Literal` can solve this
diff --git a/redbot/core/_sharedlibdeprecation.py b/redbot/core/_sharedlibdeprecation.py
index c4c9d0147..89c0e3136 100644
--- a/redbot/core/_sharedlibdeprecation.py
+++ b/redbot/core/_sharedlibdeprecation.py
@@ -22,7 +22,7 @@ class SharedLibImportWarner(MetaPathFinder):
return None
msg = (
"One of cogs uses shared libraries which are"
- " deprecated and scheduled for removal in Red 3.3.\n"
+ " deprecated and scheduled for removal in Red 3.4.\n"
"You should inform author of the cog about this message."
)
warnings.warn(msg, SharedLibDeprecationWarning, stacklevel=2)
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index 5eb7d6091..f17ab6cce 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -126,7 +126,7 @@ class CoreLogic:
else:
await bot.add_loaded_package(name)
loaded_packages.append(name)
- # remove in Red 3.3
+ # remove in Red 3.4
downloader = bot.get_cog("Downloader")
if downloader is None:
continue
@@ -705,13 +705,13 @@ class Core(commands.Cog, CoreLogic):
if len(repos_with_shared_libs) == 1:
formed = _(
"**WARNING**: The following repo is using shared libs"
- " which are marked for removal in Red 3.3: {repo}.\n"
+ " which are marked for removal in Red 3.4: {repo}.\n"
"You should inform maintainer of the repo about this message."
).format(repo=inline(repos_with_shared_libs.pop()))
else:
formed = _(
"**WARNING**: The following repos are using shared libs"
- " which are marked for removal in Red 3.3: {repos}.\n"
+ " which are marked for removal in Red 3.4: {repos}.\n"
"You should inform maintainers of these repos about this message."
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
output.append(formed)
@@ -823,13 +823,13 @@ class Core(commands.Cog, CoreLogic):
if len(repos_with_shared_libs) == 1:
formed = _(
"**WARNING**: The following repo is using shared libs"
- " which are marked for removal in Red 3.3: {repo}.\n"
+ " which are marked for removal in Red 3.4: {repo}.\n"
"You should inform maintainers of these repos about this message."
).format(repo=inline(repos_with_shared_libs.pop()))
else:
formed = _(
"**WARNING**: The following repos are using shared libs"
- " which are marked for removal in Red 3.3: {repos}.\n"
+ " which are marked for removal in Red 3.4: {repos}.\n"
"You should inform maintainers of these repos about this message."
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
output.append(formed)
From 86549248699d1bcd5294b57805d49b1fb3d1a660 Mon Sep 17 00:00:00 2001
From: Draper <27962761+Drapersniper@users.noreply.github.com>
Date: Sun, 26 Jan 2020 21:38:49 +0000
Subject: [PATCH 70/91] [Audio] Allow lazy searching for playlist across scopes
(#3430)
* Allow lazy searching for playlist cross scope
* Chore
---
changelog.d/audio/3431.enhance.rst | 1 +
redbot/cogs/audio/audio.py | 131 +++++++++++++++++------------
redbot/cogs/audio/converters.py | 5 +-
3 files changed, 83 insertions(+), 54 deletions(-)
create mode 100644 changelog.d/audio/3431.enhance.rst
diff --git a/changelog.d/audio/3431.enhance.rst b/changelog.d/audio/3431.enhance.rst
new file mode 100644
index 000000000..e2ed19a08
--- /dev/null
+++ b/changelog.d/audio/3431.enhance.rst
@@ -0,0 +1 @@
+Playlist no longer default to the Server scope, and will not return matches across multiple scopes.
\ No newline at end of file
diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py
index 5bc2d3a8b..091bc9c23 100644
--- a/redbot/cogs/audio/audio.py
+++ b/redbot/cogs/audio/audio.py
@@ -67,7 +67,7 @@ from .utils import *
_ = Translator("Audio", __file__)
-__version__ = "1.1.0"
+__version__ = "1.1.1"
__author__ = ["aikaterna", "Draper"]
log = logging.getLogger("red.audio")
@@ -1150,11 +1150,11 @@ class Audio(commands.Cog):
`[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -3833,7 +3833,7 @@ class Audio(commands.Cog):
author: discord.User,
guild: discord.Guild,
specified_user: bool = False,
- ) -> Tuple[Optional[int], str]:
+ ) -> Tuple[Optional[int], str, str]:
"""
Parameters
----------
@@ -3862,34 +3862,57 @@ class Audio(commands.Cog):
"""
correct_scope_matches: List[Playlist]
original_input = matches.get("arg")
- correct_scope_matches_temp: MutableMapping = matches.get(scope)
+ lazy_match = False
+ if scope is None:
+ correct_scope_matches_temp: MutableMapping = matches.get("all")
+ lazy_match = True
+ else:
+ correct_scope_matches_temp: MutableMapping = matches.get(scope)
+
guild_to_query = guild.id
user_to_query = author.id
+ correct_scope_matches_user = []
+ correct_scope_matches_guild = []
+ correct_scope_matches_global = []
+
if not correct_scope_matches_temp:
- return None, original_input
- if scope == PlaylistScope.USER.value:
- correct_scope_matches = [
- p for p in correct_scope_matches_temp if user_to_query == p.scope_id
+ return None, original_input, scope or PlaylistScope.GUILD.value
+ if lazy_match or (scope == PlaylistScope.USER.value):
+ correct_scope_matches_user = [
+ p for p in matches.get(PlaylistScope.USER.value) if user_to_query == p.scope_id
]
- elif scope == PlaylistScope.GUILD.value:
+ if lazy_match or (scope == PlaylistScope.GUILD.value and not correct_scope_matches_user):
if specified_user:
- correct_scope_matches = [
+ correct_scope_matches_guild = [
p
- for p in correct_scope_matches_temp
+ for p in matches.get(PlaylistScope.GUILD.value)
if guild_to_query == p.scope_id and p.author == user_to_query
]
else:
- correct_scope_matches = [
- p for p in correct_scope_matches_temp if guild_to_query == p.scope_id
+ correct_scope_matches_guild = [
+ p
+ for p in matches.get(PlaylistScope.GUILD.value)
+ if guild_to_query == p.scope_id
]
- else:
+ if lazy_match or (
+ scope == PlaylistScope.GLOBAL.value
+ and not correct_scope_matches_user
+ and not correct_scope_matches_guild
+ ):
if specified_user:
- correct_scope_matches = [
- p for p in correct_scope_matches_temp if p.author == user_to_query
+ correct_scope_matches_global = [
+ p
+ for p in matches.get(PlaylistScope.USGLOBALER.value)
+ if p.author == user_to_query
]
else:
- correct_scope_matches = [p for p in correct_scope_matches_temp]
+ correct_scope_matches_global = [p for p in matches.get(PlaylistScope.GLOBAL.value)]
+ correct_scope_matches = [
+ *correct_scope_matches_global,
+ *correct_scope_matches_guild,
+ *correct_scope_matches_user,
+ ]
match_count = len(correct_scope_matches)
if match_count > 1:
correct_scope_matches2 = [
@@ -3916,14 +3939,15 @@ class Audio(commands.Cog):
).format(match_count=match_count, original_input=original_input)
)
elif match_count == 1:
- return correct_scope_matches[0].id, original_input
+ return correct_scope_matches[0].id, original_input, correct_scope_matches[0].scope
elif match_count == 0:
- return None, original_input
+ return None, original_input, scope
# TODO : Convert this section to a new paged reaction menu when Toby Menus are Merged
pos_len = 3
playlists = f"{'#':{pos_len}}\n"
number = 0
+ correct_scope_matches = sorted(correct_scope_matches, key=lambda x: x.name.lower())
for number, playlist in enumerate(correct_scope_matches, 1):
author = self.bot.get_user(playlist.author) or playlist.author or _("Unknown")
line = _(
@@ -3936,7 +3960,7 @@ class Audio(commands.Cog):
).format(
number=number,
playlist=playlist,
- scope=humanize_scope(scope),
+ scope=humanize_scope(playlist.scope),
tracks=len(playlist.tracks),
author=author,
)
@@ -3972,7 +3996,11 @@ class Audio(commands.Cog):
)
with contextlib.suppress(discord.HTTPException):
await msg.delete()
- return correct_scope_matches[pred.result].id, original_input
+ return (
+ correct_scope_matches[pred.result].id,
+ original_input,
+ correct_scope_matches[pred.result].scope,
+ )
@commands.group()
@commands.guild_only()
@@ -4035,12 +4063,12 @@ class Audio(commands.Cog):
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
(scope, author, guild, specified_user) = scope_data
if not await self._playlist_check(ctx):
return
try:
- (playlist_id, playlist_arg) = await self._get_correct_playlist_id(
+ (playlist_id, playlist_arg, scope) = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -4222,7 +4250,7 @@ class Audio(commands.Cog):
) = scope_data
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, from_scope, from_author, from_guild, specified_from_user
)
except TooManyMatches as e:
@@ -4400,11 +4428,11 @@ class Audio(commands.Cog):
`[p]playlist delete MyPersonalPlaylist --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -4488,19 +4516,18 @@ class Audio(commands.Cog):
"""
async with ctx.typing():
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
- scope_name = humanize_scope(
- scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
- )
-
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
ctx.command.reset_cooldown(ctx)
return await self._embed_msg(ctx, title=str(e))
+ scope_name = humanize_scope(
+ scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
+ )
if playlist_id is None:
ctx.command.reset_cooldown(ctx)
return await self._embed_msg(
@@ -4631,11 +4658,11 @@ class Audio(commands.Cog):
`[p]playlist download MyPersonalPlaylist --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -4771,19 +4798,19 @@ class Audio(commands.Cog):
`[p]playlist info MyPersonalPlaylist --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
- scope_name = humanize_scope(
- scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
- )
-
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
ctx.command.reset_cooldown(ctx)
return await self._embed_msg(ctx, title=str(e))
+ scope_name = humanize_scope(
+ scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
+ )
+
if playlist_id is None:
ctx.command.reset_cooldown(ctx)
return await self._embed_msg(
@@ -5131,18 +5158,18 @@ class Audio(commands.Cog):
`[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
- scope_name = humanize_scope(
- scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
- )
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
return await self._embed_msg(ctx, title=str(e))
+ scope_name = humanize_scope(
+ scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
+ )
if playlist_id is None:
return await self._embed_msg(
ctx,
@@ -5338,7 +5365,7 @@ class Audio(commands.Cog):
`[p]playlist start MyPersonalPlaylist --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
@@ -5354,7 +5381,7 @@ class Audio(commands.Cog):
return False
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -5509,10 +5536,10 @@ class Audio(commands.Cog):
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
@@ -5788,7 +5815,7 @@ class Audio(commands.Cog):
`[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
"""
if scope_data is None:
- scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
+ scope_data = [None, ctx.author, ctx.guild, False]
scope, author, guild, specified_user = scope_data
new_name = new_name.split(" ")[0].strip('"')[:32]
@@ -5804,7 +5831,7 @@ class Audio(commands.Cog):
)
try:
- playlist_id, playlist_arg = await self._get_correct_playlist_id(
+ playlist_id, playlist_arg, scope = await self._get_correct_playlist_id(
ctx, playlist_matches, scope, author, guild, specified_user
)
except TooManyMatches as e:
diff --git a/redbot/cogs/audio/converters.py b/redbot/cogs/audio/converters.py
index 8a928a323..7e8d2c36e 100644
--- a/redbot/cogs/audio/converters.py
+++ b/redbot/cogs/audio/converters.py
@@ -158,6 +158,7 @@ class PlaylistConverter(commands.Converter):
PlaylistScope.GLOBAL.value: global_matches,
PlaylistScope.GUILD.value: guild_matches,
PlaylistScope.USER.value: user_matches,
+ "all": [*global_matches, *guild_matches, *user_matches],
"arg": arg,
}
@@ -170,7 +171,7 @@ class NoExitParser(argparse.ArgumentParser):
class ScopeParser(commands.Converter):
async def convert(
self, ctx: commands.Context, argument: str
- ) -> Tuple[str, discord.User, Optional[discord.Guild], bool]:
+ ) -> Tuple[Optional[str], discord.User, Optional[discord.Guild], bool]:
target_scope: Optional[str] = None
target_user: Optional[Union[discord.Member, discord.User]] = None
@@ -261,7 +262,7 @@ class ScopeParser(commands.Converter):
elif any(x in argument for x in ["--author", "--user", "--member"]):
raise commands.ArgParserFailure("--scope", "Nothing", custom_help=_USER_HELP)
- target_scope: str = target_scope or PlaylistScope.GUILD.value
+ target_scope: str = target_scope or None
target_user: Union[discord.Member, discord.User] = target_user or ctx.author
target_guild: discord.Guild = target_guild or ctx.guild
From a8450580e807f32e2216f718f916da8bf3ce2b16 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 17:54:39 -0500
Subject: [PATCH 71/91] [Commands Module] Improve usability of type hints
(#3410)
* [Commands Module] Better Typehint Support
We now do a lot more with type hints
- No more rexporting d.py commands submodules
- New type aliases for GuildContext & DMContext
- More things are typehinted
Note: Some things are still not typed, others are still incorrectly
typed, This is progress.
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
---
docs/framework_commands.rst | 9 +
redbot/core/bot.py | 15 +-
redbot/core/commands/__init__.py | 152 ++++++++-
redbot/core/commands/_dpy_reimplements.py | 126 +++++++
redbot/core/commands/commands.py | 171 +++++++---
redbot/core/commands/context.py | 81 ++++-
redbot/core/commands/converter.py | 389 +++++++++++++++-------
redbot/core/commands/requires.py | 45 ++-
redbot/core/config.py | 2 +-
redbot/core/utils/predicates.py | 2 +
10 files changed, 807 insertions(+), 185 deletions(-)
create mode 100644 redbot/core/commands/_dpy_reimplements.py
diff --git a/docs/framework_commands.rst b/docs/framework_commands.rst
index 19a124ecd..694894806 100644
--- a/docs/framework_commands.rst
+++ b/docs/framework_commands.rst
@@ -23,5 +23,14 @@ extend functionalities used throughout the bot, as outlined below.
.. autoclass:: redbot.core.commands.Context
:members:
+.. autoclass:: redbot.core.commands.GuildContext
+
+.. autoclass:: redbot.core.commands.DMContext
+
.. automodule:: redbot.core.commands.requires
:members: PrivilegeLevel, PermState, Requires
+
+.. automodule:: redbot.core.commands.converter
+ :members:
+ :exclude-members: convert
+ :no-undoc-members:
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index d113c9475..852c6281e 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -26,6 +26,7 @@ from typing import (
from types import MappingProxyType
import discord
+from discord.ext import commands as dpy_commands
from discord.ext.commands import when_mentioned_or
from discord.ext.commands.bot import BotBase
@@ -60,7 +61,9 @@ def _is_submodule(parent, child):
# barely spurious warning caused by our intentional shadowing
-class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-member
+class RedBase(
+ commands.GroupMixin, dpy_commands.bot.BotBase, RPCMixin
+): # pylint: disable=no-member
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
@@ -163,6 +166,16 @@ class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-mem
self._red_ready = asyncio.Event()
self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
+ def get_command(self, name: str) -> Optional[commands.Command]:
+ com = super().get_command(name)
+ assert com is None or isinstance(com, commands.Command)
+ return com
+
+ def get_cog(self, name: str) -> Optional[commands.Cog]:
+ cog = super().get_cog(name)
+ assert cog is None or isinstance(cog, commands.Cog)
+ return cog
+
@property
def _before_invoke(self): # DEP-WARN
return self._red_before_invoke_method
diff --git a/redbot/core/commands/__init__.py b/redbot/core/commands/__init__.py
index b00abe1fe..20ac5dbeb 100644
--- a/redbot/core/commands/__init__.py
+++ b/redbot/core/commands/__init__.py
@@ -1,7 +1,145 @@
-from discord.ext.commands import *
-from .commands import *
-from .context import *
-from .converter import *
-from .errors import *
-from .requires import *
-from .help import *
+########## SENSITIVE SECTION WARNING ###########
+################################################
+# Any edits of any of the exported names #
+# may result in a breaking change. #
+# Ensure no names are removed without warning. #
+################################################
+
+from .commands import (
+ Cog as Cog,
+ CogMixin as CogMixin,
+ CogCommandMixin as CogCommandMixin,
+ CogGroupMixin as CogGroupMixin,
+ Command as Command,
+ Group as Group,
+ GroupMixin as GroupMixin,
+ command as command,
+ group as group,
+ RESERVED_COMMAND_NAMES as RESERVED_COMMAND_NAMES,
+)
+from .context import Context as Context, GuildContext as GuildContext, DMContext as DMContext
+from .converter import (
+ APIToken as APIToken,
+ DictConverter as DictConverter,
+ GuildConverter as GuildConverter,
+ TimedeltaConverter as TimedeltaConverter,
+ get_dict_converter as get_dict_converter,
+ get_timedelta_converter as get_timedelta_converter,
+ parse_timedelta as parse_timedelta,
+ NoParseOptional as NoParseOptional,
+ UserInputOptional as UserInputOptional,
+ Literal as Literal,
+)
+from .errors import (
+ ConversionFailure as ConversionFailure,
+ BotMissingPermissions as BotMissingPermissions,
+ UserFeedbackCheckFailure as UserFeedbackCheckFailure,
+ ArgParserFailure as ArgParserFailure,
+)
+from .help import (
+ red_help as red_help,
+ RedHelpFormatter as RedHelpFormatter,
+ HelpSettings as HelpSettings,
+)
+from .requires import (
+ CheckPredicate as CheckPredicate,
+ DM_PERMS as DM_PERMS,
+ GlobalPermissionModel as GlobalPermissionModel,
+ GuildPermissionModel as GuildPermissionModel,
+ PermissionModel as PermissionModel,
+ PrivilegeLevel as PrivilegeLevel,
+ PermState as PermState,
+ Requires as Requires,
+ permissions_check as permissions_check,
+ bot_has_permissions as bot_has_permissions,
+ has_permissions as has_permissions,
+ has_guild_permissions as has_guild_permissions,
+ is_owner as is_owner,
+ guildowner as guildowner,
+ guildowner_or_permissions as guildowner_or_permissions,
+ admin as admin,
+ admin_or_permissions as admin_or_permissions,
+ mod as mod,
+ mod_or_permissions as mod_or_permissions,
+)
+
+from ._dpy_reimplements import (
+ check as check,
+ guild_only as guild_only,
+ cooldown as cooldown,
+ dm_only as dm_only,
+ is_nsfw as is_nsfw,
+ has_role as has_role,
+ has_any_role as has_any_role,
+ bot_has_role as bot_has_role,
+ when_mentioned_or as when_mentioned_or,
+ when_mentioned as when_mentioned,
+ bot_has_any_role as bot_has_any_role,
+)
+
+### DEP-WARN: Check this *every* discord.py update
+from discord.ext.commands import (
+ BadArgument as BadArgument,
+ EmojiConverter as EmojiConverter,
+ InvalidEndOfQuotedStringError as InvalidEndOfQuotedStringError,
+ MemberConverter as MemberConverter,
+ BotMissingRole as BotMissingRole,
+ PrivateMessageOnly as PrivateMessageOnly,
+ HelpCommand as HelpCommand,
+ MinimalHelpCommand as MinimalHelpCommand,
+ DisabledCommand as DisabledCommand,
+ ExtensionFailed as ExtensionFailed,
+ Bot as Bot,
+ NotOwner as NotOwner,
+ CategoryChannelConverter as CategoryChannelConverter,
+ CogMeta as CogMeta,
+ ConversionError as ConversionError,
+ UserInputError as UserInputError,
+ Converter as Converter,
+ InviteConverter as InviteConverter,
+ ExtensionError as ExtensionError,
+ Cooldown as Cooldown,
+ CheckFailure as CheckFailure,
+ MessageConverter as MessageConverter,
+ MissingPermissions as MissingPermissions,
+ BadUnionArgument as BadUnionArgument,
+ DefaultHelpCommand as DefaultHelpCommand,
+ ExtensionNotFound as ExtensionNotFound,
+ UserConverter as UserConverter,
+ MissingRole as MissingRole,
+ CommandOnCooldown as CommandOnCooldown,
+ MissingAnyRole as MissingAnyRole,
+ ExtensionNotLoaded as ExtensionNotLoaded,
+ clean_content as clean_content,
+ CooldownMapping as CooldownMapping,
+ ArgumentParsingError as ArgumentParsingError,
+ RoleConverter as RoleConverter,
+ CommandError as CommandError,
+ TextChannelConverter as TextChannelConverter,
+ UnexpectedQuoteError as UnexpectedQuoteError,
+ Paginator as Paginator,
+ BucketType as BucketType,
+ NoEntryPointError as NoEntryPointError,
+ CommandInvokeError as CommandInvokeError,
+ TooManyArguments as TooManyArguments,
+ Greedy as Greedy,
+ ExpectedClosingQuoteError as ExpectedClosingQuoteError,
+ ColourConverter as ColourConverter,
+ VoiceChannelConverter as VoiceChannelConverter,
+ NSFWChannelRequired as NSFWChannelRequired,
+ IDConverter as IDConverter,
+ MissingRequiredArgument as MissingRequiredArgument,
+ GameConverter as GameConverter,
+ CommandNotFound as CommandNotFound,
+ BotMissingAnyRole as BotMissingAnyRole,
+ NoPrivateMessage as NoPrivateMessage,
+ AutoShardedBot as AutoShardedBot,
+ ExtensionAlreadyLoaded as ExtensionAlreadyLoaded,
+ PartialEmojiConverter as PartialEmojiConverter,
+ check_any as check_any,
+ max_concurrency as max_concurrency,
+ CheckAnyFailure as CheckAnyFailure,
+ MaxConcurrency as MaxConcurrency,
+ MaxConcurrencyReached as MaxConcurrencyReached,
+ bot_has_guild_permissions as bot_has_guild_permissions,
+)
diff --git a/redbot/core/commands/_dpy_reimplements.py b/redbot/core/commands/_dpy_reimplements.py
new file mode 100644
index 000000000..6421a9643
--- /dev/null
+++ b/redbot/core/commands/_dpy_reimplements.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+import inspect
+import functools
+from typing import (
+ TypeVar,
+ Callable,
+ Awaitable,
+ Coroutine,
+ Union,
+ Type,
+ TYPE_CHECKING,
+ List,
+ Any,
+ Generator,
+ Protocol,
+ overload,
+)
+
+import discord
+from discord.ext import commands as dpy_commands
+
+# So much of this can be stripped right back out with proper stubs.
+if not TYPE_CHECKING:
+ from discord.ext.commands import (
+ check as check,
+ guild_only as guild_only,
+ dm_only as dm_only,
+ is_nsfw as is_nsfw,
+ has_role as has_role,
+ has_any_role as has_any_role,
+ bot_has_role as bot_has_role,
+ bot_has_any_role as bot_has_any_role,
+ cooldown as cooldown,
+ )
+
+from ..i18n import Translator
+from .context import Context
+from .commands import Command
+
+
+_ = Translator("nah", __file__)
+
+
+"""
+Anything here is either a reimplementation or re-export
+of a discord.py funtion or class with more lies for mypy
+"""
+
+__all__ = [
+ "check",
+ # "check_any", # discord.py 1.3
+ "guild_only",
+ "dm_only",
+ "is_nsfw",
+ "has_role",
+ "has_any_role",
+ "bot_has_role",
+ "bot_has_any_role",
+ "when_mentioned_or",
+ "cooldown",
+ "when_mentioned",
+]
+
+_CT = TypeVar("_CT", bound=Context)
+_T = TypeVar("_T")
+_F = TypeVar("_F")
+CheckType = Union[Callable[[_CT], bool], Callable[[_CT], Coroutine[Any, Any, bool]]]
+CoroLike = Callable[..., Union[Awaitable[_T], Generator[Any, None, _T]]]
+
+
+class CheckDecorator(Protocol):
+ predicate: Coroutine[Any, Any, bool]
+
+ @overload
+ def __call__(self, func: _CT) -> _CT:
+ ...
+
+ @overload
+ def __call__(self, func: CoroLike) -> CoroLike:
+ ...
+
+
+if TYPE_CHECKING:
+
+ def check(predicate: CheckType) -> CheckDecorator:
+ ...
+
+ def guild_only() -> CheckDecorator:
+ ...
+
+ def dm_only() -> CheckDecorator:
+ ...
+
+ def is_nsfw() -> CheckDecorator:
+ ...
+
+ def has_role() -> CheckDecorator:
+ ...
+
+ def has_any_role() -> CheckDecorator:
+ ...
+
+ def bot_has_role() -> CheckDecorator:
+ ...
+
+ def bot_has_any_role() -> CheckDecorator:
+ ...
+
+ def cooldown(rate: int, per: float, type: dpy_commands.BucketType = ...) -> Callable[[_F], _F]:
+ ...
+
+
+PrefixCallable = Callable[[dpy_commands.bot.BotBase, discord.Message], List[str]]
+
+
+def when_mentioned(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
+ return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "]
+
+
+def when_mentioned_or(*prefixes) -> PrefixCallable:
+ def inner(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
+ r = list(prefixes)
+ r = when_mentioned(bot, msg) + r
+ return r
+
+ return inner
diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py
index 313f72f2f..26fe92990 100644
--- a/redbot/core/commands/commands.py
+++ b/redbot/core/commands/commands.py
@@ -1,24 +1,53 @@
"""Module for command helpers and classes.
This module contains extended classes and functions which are intended to
-replace those from the `discord.ext.commands` module.
+be used instead of those from the `discord.ext.commands` module.
"""
+from __future__ import annotations
+
import inspect
import re
import weakref
-from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
+from typing import (
+ Awaitable,
+ Callable,
+ Coroutine,
+ TypeVar,
+ Type,
+ Dict,
+ List,
+ Optional,
+ Tuple,
+ Union,
+ MutableMapping,
+ TYPE_CHECKING,
+ cast,
+)
import discord
-from discord.ext import commands
+from discord.ext.commands import (
+ BadArgument,
+ CommandError,
+ CheckFailure,
+ DisabledCommand,
+ command as dpy_command_deco,
+ Command as DPYCommand,
+ Cog as DPYCog,
+ CogMeta as DPYCogMeta,
+ Group as DPYGroup,
+ Greedy,
+)
from . import converter as converters
from .errors import ConversionFailure
-from .requires import PermState, PrivilegeLevel, Requires
+from .requires import PermState, PrivilegeLevel, Requires, PermStateAllowedStates
from ..i18n import Translator
if TYPE_CHECKING:
+ # circular import avoidance
from .context import Context
+
__all__ = [
"Cog",
"CogMixin",
@@ -38,11 +67,17 @@ RESERVED_COMMAND_NAMES = (
)
_ = Translator("commands.commands", __file__)
+DisablerDictType = MutableMapping[discord.Guild, Callable[["Context"], Awaitable[bool]]]
class CogCommandMixin:
"""A mixin for cogs and commands."""
+ @property
+ def help(self) -> str:
+ """To be defined by subclasses"""
+ ...
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self, Command):
@@ -182,7 +217,7 @@ class CogCommandMixin:
self.deny_to(Requires.DEFAULT, guild_id=guild_id)
-class Command(CogCommandMixin, commands.Command):
+class Command(CogCommandMixin, DPYCommand):
"""Command class for Red.
This should not be created directly, and instead via the decorator.
@@ -198,7 +233,10 @@ class Command(CogCommandMixin, commands.Command):
`Requires.checks`.
translator : Translator
A translator for this command's help docstring.
-
+ ignore_optional_for_conversion : bool
+ A value which can be set to not have discord.py's
+ argument parsing behavior for ``typing.Optional``
+ (type used will be of the inner type instead)
"""
def __call__(self, *args, **kwargs):
@@ -209,6 +247,7 @@ class Command(CogCommandMixin, commands.Command):
return self.callback(*args, **kwargs)
def __init__(self, *args, **kwargs):
+ self.ignore_optional_for_conversion = kwargs.pop("ignore_optional_for_conversion", False)
super().__init__(*args, **kwargs)
self._help_override = kwargs.pop("help_override", None)
self.translator = kwargs.pop("i18n", None)
@@ -229,8 +268,62 @@ class Command(CogCommandMixin, commands.Command):
# Red specific
other.requires = self.requires
+ other.ignore_optional_for_conversion = self.ignore_optional_for_conversion
return other
+ @property
+ def callback(self):
+ return self._callback
+
+ @callback.setter
+ def callback(self, function):
+ """
+ Below should be mostly the same as discord.py
+ The only (current) change is to filter out typing.Optional
+ if a user has specified the desire for this behavior
+ """
+ self._callback = function
+ self.module = function.__module__
+
+ signature = inspect.signature(function)
+ self.params = signature.parameters.copy()
+
+ # PEP-563 allows postponing evaluation of annotations with a __future__
+ # import. When postponed, Parameter.annotation will be a string and must
+ # be replaced with the real value for the converters to work later on
+ for key, value in self.params.items():
+ if isinstance(value.annotation, str):
+ self.params[key] = value = value.replace(
+ annotation=eval(value.annotation, function.__globals__)
+ )
+
+ # fail early for when someone passes an unparameterized Greedy type
+ if value.annotation is Greedy:
+ raise TypeError("Unparameterized Greedy[...] is disallowed in signature.")
+
+ if not self.ignore_optional_for_conversion:
+ continue # reduces indentation compared to alternative
+
+ try:
+ vtype = value.annotation.__origin__
+ if vtype is Union:
+ _NoneType = type if TYPE_CHECKING else type(None)
+ args = value.annotation.__args__
+ if _NoneType in args:
+ args = tuple(a for a in args if a is not _NoneType)
+ if len(args) == 1:
+ # can't have a union of 1 or 0 items
+ # 1 prevents this from becoming 0
+ # we need to prevent 2 become 1
+ # (Don't change that to becoming, it's intentional :musical_note:)
+ self.params[key] = value = value.replace(annotation=args[0])
+ else:
+ # and mypy wretches at the correct Union[args]
+ temp_type = type if TYPE_CHECKING else Union[args]
+ self.params[key] = value = value.replace(annotation=temp_type)
+ except AttributeError:
+ continue
+
@property
def help(self):
"""Help string for this command.
@@ -311,7 +404,7 @@ class Command(CogCommandMixin, commands.Command):
for parent in reversed(self.parents):
try:
result = await parent.can_run(ctx, change_permission_state=True)
- except commands.CommandError:
+ except CommandError:
result = False
if result is False:
@@ -334,12 +427,10 @@ class Command(CogCommandMixin, commands.Command):
ctx.command = self
if not self.enabled:
- raise commands.DisabledCommand(f"{self.name} command is disabled")
+ raise DisabledCommand(f"{self.name} command is disabled")
if not await self.can_run(ctx, change_permission_state=True):
- raise commands.CheckFailure(
- f"The check functions for command {self.qualified_name} failed."
- )
+ raise CheckFailure(f"The check functions for command {self.qualified_name} failed.")
if self.cooldown_after_parsing:
await self._parse_arguments(ctx)
@@ -373,7 +464,7 @@ class Command(CogCommandMixin, commands.Command):
try:
return await super().do_conversion(ctx, converter, argument, param)
- except commands.BadArgument as exc:
+ except BadArgument as exc:
raise ConversionFailure(converter, argument, param, *exc.args) from exc
except ValueError as exc:
# Some common converters need special treatment...
@@ -408,7 +499,7 @@ class Command(CogCommandMixin, commands.Command):
can_run = await self.can_run(
ctx, check_all_parents=True, change_permission_state=False
)
- except (commands.CheckFailure, commands.errors.DisabledCommand):
+ except (CheckFailure, DisabledCommand):
return False
else:
if can_run is False:
@@ -564,10 +655,9 @@ class GroupMixin(discord.ext.commands.GroupMixin):
class CogGroupMixin:
requires: Requires
- all_commands: Dict[str, Command]
def reevaluate_rules_for(
- self, model_id: Union[str, int], guild_id: Optional[int]
+ self, model_id: Union[str, int], guild_id: int = 0
) -> Tuple[PermState, bool]:
"""Re-evaluate a rule by checking subcommand rules.
@@ -590,15 +680,16 @@ class CogGroupMixin:
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
- if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
- # These three states are unaffected by subcommand rules
- return cur_rule, False
- else:
+ if cur_rule not in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
+ # The above three states are unaffected by subcommand rules
# Remaining states can be changed if there exists no actively-allowed
# subcommand (this includes subcommands multiple levels below)
+
+ all_commands: Dict[str, Command] = getattr(self, "all_commands", {})
+
if any(
- cmd.requires.get_rule(model_id, guild_id=guild_id) in PermState.ALLOWED_STATES
- for cmd in self.all_commands.values()
+ cmd.requires.get_rule(model_id, guild_id=guild_id) in PermStateAllowedStates
+ for cmd in all_commands.values()
):
return cur_rule, False
elif cur_rule is PermState.PASSIVE_ALLOW:
@@ -608,8 +699,11 @@ class CogGroupMixin:
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
return PermState.ACTIVE_DENY, True
+ # Default return value
+ return cur_rule, False
-class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
+
+class Group(GroupMixin, Command, CogGroupMixin, DPYGroup):
"""Group command class for Red.
This class inherits from `Command`, with :class:`GroupMixin` and
@@ -653,14 +747,6 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
class CogMixin(CogGroupMixin, CogCommandMixin):
"""Mixin class for a cog, intended for use with discord.py's cog class"""
- @property
- def all_commands(self) -> Dict[str, Command]:
- """
- This does not have identical behavior to
- Group.all_commands but should return what you expect
- """
- return {cmd.name: cmd for cmd in self.__cog_commands__}
-
@property
def help(self):
doc = self.__doc__
@@ -689,7 +775,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
try:
can_run = await self.requires.verify(ctx)
- except commands.CommandError:
+ except CommandError:
return False
return can_run
@@ -718,16 +804,22 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
return await self.can_run(ctx)
-class Cog(CogMixin, commands.Cog):
+class Cog(CogMixin, DPYCog, metaclass=DPYCogMeta):
"""
Red's Cog base class
This includes a metaclass from discord.py
"""
- # NB: Do not move the inheritcance of this. Keeping the mix of that metaclass
- # seperate gives us more freedoms in several places.
- pass
+ __cog_commands__: Tuple[Command]
+
+ @property
+ def all_commands(self) -> Dict[str, Command]:
+ """
+ This does not have identical behavior to
+ Group.all_commands but should return what you expect
+ """
+ return {cmd.name: cmd for cmd in self.__cog_commands__}
def command(name=None, cls=Command, **attrs):
@@ -736,7 +828,8 @@ def command(name=None, cls=Command, **attrs):
Same interface as `discord.ext.commands.command`.
"""
attrs["help_override"] = attrs.pop("help", None)
- return commands.command(name, cls, **attrs)
+
+ return dpy_command_deco(name, cls, **attrs)
def group(name=None, cls=Group, **attrs):
@@ -744,10 +837,10 @@ def group(name=None, cls=Group, **attrs):
Same interface as `discord.ext.commands.group`.
"""
- return command(name, cls, **attrs)
+ return dpy_command_deco(name, cls, **attrs)
-__command_disablers = weakref.WeakValueDictionary()
+__command_disablers: DisablerDictType = weakref.WeakValueDictionary()
def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitable[bool]]:
@@ -762,7 +855,7 @@ def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitabl
async def disabler(ctx: "Context") -> bool:
if ctx.guild == guild:
- raise commands.DisabledCommand()
+ raise DisabledCommand()
return True
__command_disablers[guild] = disabler
diff --git a/redbot/core/commands/context.py b/redbot/core/commands/context.py
index d07f73103..ebbb32c0f 100644
--- a/redbot/core/commands/context.py
+++ b/redbot/core/commands/context.py
@@ -1,21 +1,28 @@
+from __future__ import annotations
+
import asyncio
import contextlib
+import os
import re
-from typing import Iterable, List, Union
+from typing import Iterable, List, Union, Optional, TYPE_CHECKING
import discord
-from discord.ext import commands
+from discord.ext.commands import Context as DPYContext
from .requires import PermState
from ..utils.chat_formatting import box
from ..utils.predicates import MessagePredicate
from ..utils import common_filters
+if TYPE_CHECKING:
+ from .commands import Command
+ from ..bot import Red
+
TICK = "\N{WHITE HEAVY CHECK MARK}"
-__all__ = ["Context"]
+__all__ = ["Context", "GuildContext", "DMContext"]
-class Context(commands.Context):
+class Context(DPYContext):
"""Command invocation context for Red.
All context passed into commands will be of this type.
@@ -40,6 +47,10 @@ class Context(commands.Context):
The permission state the current context is in.
"""
+ command: "Command"
+ invoked_subcommand: "Optional[Command]"
+ bot: "Red"
+
def __init__(self, **attrs):
self.assume_yes = attrs.pop("assume_yes", False)
super().__init__(**attrs)
@@ -254,7 +265,7 @@ class Context(commands.Context):
return pattern.sub(f"@{me.display_name}", self.prefix)
@property
- def me(self) -> discord.abc.User:
+ def me(self) -> Union[discord.ClientUser, discord.Member]:
"""discord.abc.User: The bot member or user object.
If the context is DM, this will be a `discord.User` object.
@@ -263,3 +274,63 @@ class Context(commands.Context):
return self.guild.me
else:
return self.bot.user
+
+
+if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
+
+ class DMContext(Context):
+ """
+ At runtime, this will still be a normal context object.
+
+ This lies about some type narrowing for type analysis in commands
+ using a dm_only decorator.
+
+ It is only correct to use when those types are already narrowed
+ """
+
+ @property
+ def author(self) -> discord.User:
+ ...
+
+ @property
+ def channel(self) -> discord.DMChannel:
+ ...
+
+ @property
+ def guild(self) -> None:
+ ...
+
+ @property
+ def me(self) -> discord.ClientUser:
+ ...
+
+ class GuildContext(Context):
+ """
+ At runtime, this will still be a normal context object.
+
+ This lies about some type narrowing for type analysis in commands
+ using a guild_only decorator.
+
+ It is only correct to use when those types are already narrowed
+ """
+
+ @property
+ def author(self) -> discord.Member:
+ ...
+
+ @property
+ def channel(self) -> discord.TextChannel:
+ ...
+
+ @property
+ def guild(self) -> discord.Guild:
+ ...
+
+ @property
+ def me(self) -> discord.Member:
+ ...
+
+
+else:
+ GuildContext = Context
+ DMContext = Context
diff --git a/redbot/core/commands/converter.py b/redbot/core/commands/converter.py
index bb5ce669d..a2e604a03 100644
--- a/redbot/core/commands/converter.py
+++ b/redbot/core/commands/converter.py
@@ -1,14 +1,33 @@
+"""
+commands.converter
+==================
+This module contains useful functions and classes for command argument conversion.
+
+Some of the converters within are included provisionaly and are marked as such.
+"""
+import os
import re
import functools
from datetime import timedelta
-from typing import TYPE_CHECKING, Optional, List, Dict
+from typing import (
+ TYPE_CHECKING,
+ Generic,
+ Optional,
+ Optional as NoParseOptional,
+ Tuple,
+ List,
+ Dict,
+ Type,
+ TypeVar,
+ Literal as Literal,
+)
import discord
from discord.ext import commands as dpy_commands
+from discord.ext.commands import BadArgument
-from . import BadArgument
from ..i18n import Translator
-from ..utils.chat_formatting import humanize_timedelta
+from ..utils.chat_formatting import humanize_timedelta, humanize_list
if TYPE_CHECKING:
from .context import Context
@@ -17,10 +36,13 @@ __all__ = [
"APIToken",
"DictConverter",
"GuildConverter",
+ "UserInputOptional",
+ "NoParseOptional",
"TimedeltaConverter",
"get_dict_converter",
"get_timedelta_converter",
"parse_timedelta",
+ "Literal",
]
_ = Translator("commands.converter", __file__)
@@ -67,7 +89,7 @@ def parse_timedelta(
allowed_units : Optional[List[str]]
If provided, you can constrain a user to expressing the amount of time
in specific units. The units you can chose to provide are the same as the
- parser understands. `weeks` `days` `hours` `minutes` `seconds`
+ parser understands. (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
Returns
-------
@@ -138,17 +160,18 @@ class APIToken(discord.ext.commands.Converter):
This will parse the input argument separating the key value pairs into a
format to be used for the core bots API token storage.
- This will split the argument by either `;` ` `, or `,` and return a dict
+ This will split the argument by a space, comma, or semicolon and return a dict
to be stored. Since all API's are different and have different naming convention,
this leaves the onus on the cog creator to clearly define how to setup the correct
credential names for their cogs.
- Note: Core usage of this has been replaced with DictConverter use instead.
+ Note: Core usage of this has been replaced with `DictConverter` use instead.
- This may be removed at a later date (with warning)
+ .. warning::
+ This will be removed in version 3.4.
"""
- async def convert(self, ctx, argument) -> dict:
+ async def convert(self, ctx: "Context", argument) -> dict:
bot = ctx.bot
result = {}
match = re.split(r";|,| ", argument)
@@ -162,140 +185,262 @@ class APIToken(discord.ext.commands.Converter):
return result
-class DictConverter(dpy_commands.Converter):
- """
- Converts pairs of space seperated values to a dict
- """
+# Below this line are a lot of lies for mypy about things that *end up* correct when
+# These are used for command conversion purposes. Please refer to the portion
+# which is *not* for type checking for the actual implementation
+# and ensure the lies stay correct for how the object should look as a typehint
- def __init__(self, *expected_keys: str, delims: Optional[List[str]] = None):
- self.expected_keys = expected_keys
- self.delims = delims or [" "]
- self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
+if TYPE_CHECKING:
+ DictConverter = Dict[str, str]
+else:
- async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
+ class DictConverter(dpy_commands.Converter):
+ """
+ Converts pairs of space seperated values to a dict
+ """
- ret: Dict[str, str] = {}
- args = self.pattern.split(argument)
+ def __init__(self, *expected_keys: str, delims: Optional[List[str]] = None):
+ self.expected_keys = expected_keys
+ self.delims = delims or [" "]
+ self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
- if len(args) % 2 != 0:
- raise BadArgument()
+ async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
+ ret: Dict[str, str] = {}
+ args = self.pattern.split(argument)
- iterator = iter(args)
+ if len(args) % 2 != 0:
+ raise BadArgument()
- for key in iterator:
- if self.expected_keys and key not in self.expected_keys:
- raise BadArgument(_("Unexpected key {key}").format(key=key))
+ iterator = iter(args)
- ret[key] = next(iterator)
+ for key in iterator:
+ if self.expected_keys and key not in self.expected_keys:
+ raise BadArgument(_("Unexpected key {key}").format(key=key))
- return ret
+ ret[key] = next(iterator)
+
+ return ret
-def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> type:
- """
- Returns a typechecking safe `DictConverter` suitable for use with discord.py
- """
+if TYPE_CHECKING:
- class PartialMeta(type(DictConverter)):
- __call__ = functools.partialmethod(
- type(DictConverter).__call__, *expected_keys, delims=delims
- )
-
- class ValidatedConverter(DictConverter, metaclass=PartialMeta):
- pass
-
- return ValidatedConverter
+ def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
+ ...
-class TimedeltaConverter(dpy_commands.Converter):
- """
- This is a converter for timedeltas.
- The units should be in order from largest to smallest.
- This works with or without whitespace.
+else:
- See `parse_timedelta` for more information about how this functions.
+ def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
+ """
+ Returns a typechecking safe `DictConverter` suitable for use with discord.py
+ """
- Attributes
- ----------
- maximum : Optional[timedelta]
- If provided, any parsed value higher than this will raise an exception
- minimum : Optional[timedelta]
- If provided, any parsed value lower than this will raise an exception
- allowed_units : Optional[List[str]]
- If provided, you can constrain a user to expressing the amount of time
- in specific units. The units you can chose to provide are the same as the
- parser understands: `weeks` `days` `hours` `minutes` `seconds`
- default_unit : Optional[str]
- If provided, it will additionally try to match integer-only input into
- a timedelta, using the unit specified. Same units as in `allowed_units`
- apply.
- """
-
- def __init__(self, *, minimum=None, maximum=None, allowed_units=None, default_unit=None):
- self.allowed_units = allowed_units
- self.default_unit = default_unit
- self.minimum = minimum
- self.maximum = maximum
-
- async def convert(self, ctx: "Context", argument: str) -> timedelta:
- if self.default_unit and argument.isdecimal():
- delta = timedelta(**{self.default_unit: int(argument)})
- else:
- delta = parse_timedelta(
- argument,
- minimum=self.minimum,
- maximum=self.maximum,
- allowed_units=self.allowed_units,
+ class PartialMeta(type):
+ __call__ = functools.partialmethod(
+ type(DictConverter).__call__, *expected_keys, delims=delims
)
- if delta is not None:
- return delta
- raise BadArgument() # This allows this to be a required argument.
+
+ class ValidatedConverter(DictConverter, metaclass=PartialMeta):
+ pass
+
+ return ValidatedConverter
-def get_timedelta_converter(
- *,
- default_unit: Optional[str] = None,
- maximum: Optional[timedelta] = None,
- minimum: Optional[timedelta] = None,
- allowed_units: Optional[List[str]] = None,
-) -> type:
- """
- This creates a type suitable for typechecking which works with discord.py's
- commands.
-
- See `parse_timedelta` for more information about how this functions.
+if TYPE_CHECKING:
+ TimedeltaConverter = timedelta
+else:
- Parameters
- ----------
- maximum : Optional[timedelta]
- If provided, any parsed value higher than this will raise an exception
- minimum : Optional[timedelta]
- If provided, any parsed value lower than this will raise an exception
- allowed_units : Optional[List[str]]
- If provided, you can constrain a user to expressing the amount of time
- in specific units. The units you can chose to provide are the same as the
- parser understands: `weeks` `days` `hours` `minutes` `seconds`
- default_unit : Optional[str]
- If provided, it will additionally try to match integer-only input into
- a timedelta, using the unit specified. Same units as in `allowed_units`
- apply.
+ class TimedeltaConverter(dpy_commands.Converter):
+ """
+ This is a converter for timedeltas.
+ The units should be in order from largest to smallest.
+ This works with or without whitespace.
- Returns
- -------
- type
- The converter class, which will be a subclass of `TimedeltaConverter`
- """
+ See `parse_timedelta` for more information about how this functions.
- class PartialMeta(type(TimedeltaConverter)):
- __call__ = functools.partialmethod(
- type(DictConverter).__call__,
- allowed_units=allowed_units,
- default_unit=default_unit,
- minimum=minimum,
- maximum=maximum,
- )
+ Attributes
+ ----------
+ maximum : Optional[timedelta]
+ If provided, any parsed value higher than this will raise an exception
+ minimum : Optional[timedelta]
+ If provided, any parsed value lower than this will raise an exception
+ allowed_units : Optional[List[str]]
+ If provided, you can constrain a user to expressing the amount of time
+ in specific units. The units you can choose to provide are the same as the
+ parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
+ default_unit : Optional[str]
+ If provided, it will additionally try to match integer-only input into
+ a timedelta, using the unit specified. Same units as in ``allowed_units``
+ apply.
+ """
- class ValidatedConverter(TimedeltaConverter, metaclass=PartialMeta):
- pass
+ def __init__(self, *, minimum=None, maximum=None, allowed_units=None, default_unit=None):
+ self.allowed_units = allowed_units
+ self.default_unit = default_unit
+ self.minimum = minimum
+ self.maximum = maximum
- return ValidatedConverter
+ async def convert(self, ctx: "Context", argument: str) -> timedelta:
+ if self.default_unit and argument.isdecimal():
+ delta = timedelta(**{self.default_unit: int(argument)})
+ else:
+ delta = parse_timedelta(
+ argument,
+ minimum=self.minimum,
+ maximum=self.maximum,
+ allowed_units=self.allowed_units,
+ )
+ if delta is not None:
+ return delta
+ raise BadArgument() # This allows this to be a required argument.
+
+
+if TYPE_CHECKING:
+
+ def get_timedelta_converter(
+ *,
+ default_unit: Optional[str] = None,
+ maximum: Optional[timedelta] = None,
+ minimum: Optional[timedelta] = None,
+ allowed_units: Optional[List[str]] = None,
+ ) -> Type[timedelta]:
+ ...
+
+
+else:
+
+ def get_timedelta_converter(
+ *,
+ default_unit: Optional[str] = None,
+ maximum: Optional[timedelta] = None,
+ minimum: Optional[timedelta] = None,
+ allowed_units: Optional[List[str]] = None,
+ ) -> Type[timedelta]:
+ """
+ This creates a type suitable for typechecking which works with discord.py's
+ commands.
+
+ See `parse_timedelta` for more information about how this functions.
+
+ Parameters
+ ----------
+ maximum : Optional[timedelta]
+ If provided, any parsed value higher than this will raise an exception
+ minimum : Optional[timedelta]
+ If provided, any parsed value lower than this will raise an exception
+ allowed_units : Optional[List[str]]
+ If provided, you can constrain a user to expressing the amount of time
+ in specific units. The units you can choose to provide are the same as the
+ parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
+ default_unit : Optional[str]
+ If provided, it will additionally try to match integer-only input into
+ a timedelta, using the unit specified. Same units as in ``allowed_units``
+ apply.
+
+ Returns
+ -------
+ type
+ The converter class, which will be a subclass of `TimedeltaConverter`
+ """
+
+ class PartialMeta(type):
+ __call__ = functools.partialmethod(
+ type(DictConverter).__call__,
+ allowed_units=allowed_units,
+ default_unit=default_unit,
+ minimum=minimum,
+ maximum=maximum,
+ )
+
+ class ValidatedConverter(TimedeltaConverter, metaclass=PartialMeta):
+ pass
+
+ return ValidatedConverter
+
+
+if not TYPE_CHECKING:
+
+ class NoParseOptional:
+ """
+ This can be used instead of `typing.Optional`
+ to avoid discord.py special casing the conversion behavior.
+
+ .. warning::
+ This converter class is still provisional.
+
+ .. seealso::
+ The `ignore_optional_for_conversion` option of commands.
+ """
+
+ def __class_getitem__(cls, key):
+ if isinstance(key, tuple):
+ raise TypeError("Must only provide a single type to Optional")
+ return key
+
+
+_T_OPT = TypeVar("_T_OPT", bound=Type)
+
+if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
+
+ class UserInputOptional(Generic[_T_OPT]):
+ """
+ This can be used when user input should be converted as discord.py
+ treats `typing.Optional`, but the type should not be equivalent to
+ ``typing.Union[DesiredType, None]`` for type checking.
+
+
+ .. warning::
+ This converter class is still provisional.
+
+ This class may not play well with mypy yet
+ and may still require you guard this in a
+ type checking conditional import vs the desired types
+
+ We're aware and looking into improving this.
+ """
+
+ def __class_getitem__(cls, key: _T_OPT) -> _T_OPT:
+ if isinstance(key, tuple):
+ raise TypeError("Must only provide a single type to Optional")
+ return key
+
+
+else:
+ UserInputOptional = Optional
+
+
+if not TYPE_CHECKING:
+
+ class Literal(dpy_commands.Converter):
+ """
+ This can be used as a converter for `typing.Literal`.
+
+ In a type checking context it is `typing.Literal`.
+ In a runtime context, it's a converter which only matches the literals it was given.
+
+
+ .. warning::
+ This converter class is still provisional.
+ """
+
+ def __init__(self, valid_names: Tuple[str]):
+ self.valid_names = valid_names
+
+ def __call__(self, ctx, arg):
+ # Callable's are treated as valid types:
+ # https://github.com/python/cpython/blob/3.8/Lib/typing.py#L148
+ # Without this, ``typing.Union[Literal["clear"], bool]`` would fail
+ return self.convert(ctx, arg)
+
+ async def convert(self, ctx, arg):
+ if arg in self.valid_names:
+ return arg
+ raise BadArgument(_("Expected one of: {}").format(humanize_list(self.valid_names)))
+
+ def __class_getitem__(cls, k):
+ if not k:
+ raise ValueError("Need at least one value for Literal")
+ if isinstance(k, tuple):
+ return cls(k)
+ else:
+ return cls((k,))
diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py
index 88b348209..69d7d44c5 100644
--- a/redbot/core/commands/requires.py
+++ b/redbot/core/commands/requires.py
@@ -8,6 +8,7 @@ checks like bot permissions checks.
"""
import asyncio
import enum
+import inspect
from typing import (
Union,
Optional,
@@ -45,6 +46,7 @@ __all__ = [
"permissions_check",
"bot_has_permissions",
"has_permissions",
+ "has_guild_permissions",
"is_owner",
"guildowner",
"guildowner_or_permissions",
@@ -52,6 +54,9 @@ __all__ = [
"admin_or_permissions",
"mod",
"mod_or_permissions",
+ "transition_permstate_to",
+ "PermStateTransitions",
+ "PermStateAllowedStates",
]
_T = TypeVar("_T")
@@ -182,11 +187,6 @@ class PermState(enum.Enum):
"""This command has been actively denied by a permission hook
check validation doesn't need this, but is useful to developers"""
- def transition_to(
- self, next_state: "PermState"
- ) -> Tuple[Optional[bool], Union["PermState", Dict[bool, "PermState"]]]:
- return self.TRANSITIONS[self][next_state]
-
@classmethod
def from_bool(cls, value: Optional[bool]) -> "PermState":
"""Get a PermState from a bool or ``NoneType``."""
@@ -211,7 +211,11 @@ class PermState(enum.Enum):
# result of the default permission checks - the transition from NORMAL
# to PASSIVE_ALLOW. In this case "next state" is a dict mapping the
# permission check results to the actual next state.
-PermState.TRANSITIONS = {
+
+TransitionResult = Tuple[Optional[bool], Union[PermState, Dict[bool, PermState]]]
+TransitionDict = Dict[PermState, Dict[PermState, TransitionResult]]
+
+PermStateTransitions: TransitionDict = {
PermState.ACTIVE_ALLOW: {
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
PermState.NORMAL: (True, PermState.ACTIVE_ALLOW),
@@ -248,13 +252,18 @@ PermState.TRANSITIONS = {
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
},
}
-PermState.ALLOWED_STATES = (
+
+PermStateAllowedStates = (
PermState.ACTIVE_ALLOW,
PermState.PASSIVE_ALLOW,
PermState.CAUTIOUS_ALLOW,
)
+def transition_permstate_to(prev: PermState, next_state: PermState) -> TransitionResult:
+ return PermStateTransitions[prev][next_state]
+
+
class Requires:
"""This class describes the requirements for executing a specific command.
@@ -326,13 +335,13 @@ class Requires:
@staticmethod
def get_decorator(
- privilege_level: Optional[PrivilegeLevel], user_perms: Dict[str, bool]
+ privilege_level: Optional[PrivilegeLevel], user_perms: Optional[Dict[str, bool]]
) -> Callable[["_CommandOrCoro"], "_CommandOrCoro"]:
if not user_perms:
user_perms = None
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
- if asyncio.iscoroutinefunction(func):
+ if inspect.iscoroutinefunction(func):
func.__requires_privilege_level__ = privilege_level
func.__requires_user_perms__ = user_perms
else:
@@ -341,6 +350,7 @@ class Requires:
func.requires.user_perms = None
else:
_validate_perms_dict(user_perms)
+ assert func.requires.user_perms is not None
func.requires.user_perms.update(**user_perms)
return func
@@ -488,7 +498,7 @@ class Requires:
async def _transition_state(self, ctx: "Context") -> bool:
prev_state = ctx.permission_state
cur_state = self._get_rule_from_ctx(ctx)
- should_invoke, next_state = prev_state.transition_to(cur_state)
+ should_invoke, next_state = transition_permstate_to(prev_state, cur_state)
if should_invoke is None:
# NORMAL invokation, we simply follow standard procedure
should_invoke = await self._verify_user(ctx)
@@ -509,6 +519,7 @@ class Requires:
would_invoke = await self._verify_user(ctx)
next_state = next_state[would_invoke]
+ assert isinstance(next_state, PermState)
ctx.permission_state = next_state
return should_invoke
@@ -635,6 +646,20 @@ def permissions_check(predicate: CheckPredicate):
return decorator
+def has_guild_permissions(**perms):
+ """Restrict the command to users with these guild permissions.
+
+ This check can be overridden by rules.
+ """
+
+ _validate_perms_dict(perms)
+
+ def predicate(ctx):
+ return ctx.guild and ctx.author.guild_permissions >= discord.Permissions(**perms)
+
+ return permissions_check(predicate)
+
+
def bot_has_permissions(**perms: bool):
"""Complain if the bot is missing permissions.
diff --git a/redbot/core/config.py b/redbot/core/config.py
index d1e804897..dcb6cde54 100644
--- a/redbot/core/config.py
+++ b/redbot/core/config.py
@@ -979,7 +979,7 @@ class Config:
"""
return self._get_base_group(self.CHANNEL, str(channel_id))
- def channel(self, channel: discord.TextChannel) -> Group:
+ def channel(self, channel: discord.abc.GuildChannel) -> Group:
"""Returns a `Group` for the given channel.
This does not discriminate between text and voice channels.
diff --git a/redbot/core/utils/predicates.py b/redbot/core/utils/predicates.py
index 2b35c5051..76c0dd8a7 100644
--- a/redbot/core/utils/predicates.py
+++ b/redbot/core/utils/predicates.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import re
from typing import Callable, ClassVar, List, Optional, Pattern, Sequence, Tuple, Union, cast
From 3d4f9500e916080856338819a0562ed8f225d10e Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 19:00:08 -0500
Subject: [PATCH 72/91] [Permissions] Ordering fix (#3452)
---
redbot/core/commands/requires.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py
index 69d7d44c5..a476e9c79 100644
--- a/redbot/core/commands/requires.py
+++ b/redbot/core/commands/requires.py
@@ -9,6 +9,7 @@ checks like bot permissions checks.
import asyncio
import enum
import inspect
+from collections import ChainMap
from typing import (
Union,
Optional,
@@ -21,6 +22,7 @@ from typing import (
TypeVar,
Tuple,
ClassVar,
+ Mapping,
)
import discord
@@ -367,6 +369,8 @@ class Requires:
guild_id : int
The ID of the guild for the rule's scope. Set to
`Requires.GLOBAL` for a global rule.
+ If a global rule is set for a model,
+ it will be prefered over the guild rule.
Returns
-------
@@ -377,8 +381,9 @@ class Requires:
"""
if not isinstance(model, (str, int)):
model = model.id
+ rules: Mapping[Union[int, str], PermState]
if guild_id:
- rules = self._guild_rules.get(guild_id, _RulesDict())
+ rules = ChainMap(self._global_rules, self._guild_rules.get(guild_id, _RulesDict()))
else:
rules = self._global_rules
return rules.get(model, PermState.NORMAL)
From a664615a2da4efbc4f488c05c401b7015aa8c73b Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 19:25:58 -0500
Subject: [PATCH 73/91] shortdoc should be formatted too, + generic replacement
method (#3451)
---
redbot/core/commands/commands.py | 74 ++++++++++++++++++++++++++------
redbot/core/commands/help.py | 12 +++---
2 files changed, 68 insertions(+), 18 deletions(-)
diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py
index 26fe92990..649e85bfd 100644
--- a/redbot/core/commands/commands.py
+++ b/redbot/core/commands/commands.py
@@ -93,6 +93,45 @@ class CogCommandMixin:
checks=getattr(decorated, "__requires_checks__", []),
)
+ def format_text_for_context(self, ctx: "Context", text: str) -> str:
+ """
+ This formats text based on values in context
+
+ The steps are (currently, roughly) the following:
+
+ - substitute ``[p]`` with ``ctx.clean_prefix``
+ - substitute ``[botname]`` with ``ctx.me.display_name``
+
+ More steps may be added at a later time.
+
+ Cog creators should only override this if they want
+ help text to be modified, and may also want to
+ look at `format_help_for_context` and (for commands only)
+ ``format_shortdoc_for_context``
+
+ Parameters
+ ----------
+ ctx: Context
+ text: str
+
+ Returns
+ -------
+ str
+ text which has had some portions replaced based on context
+ """
+ formatting_pattern = re.compile(r"\[p\]|\[botname\]")
+
+ def replacement(m: re.Match) -> str:
+ s = m.group(0)
+ if s == "[p]":
+ return ctx.clean_prefix
+ if s == "[botname]":
+ return ctx.me.display_name
+ # We shouldnt get here:
+ return s
+
+ return formatting_pattern.sub(replacement, text)
+
def format_help_for_context(self, ctx: "Context") -> str:
"""
This formats the help string based on values in context
@@ -123,18 +162,7 @@ class CogCommandMixin:
# Short circuit out on an empty help string
return help_str
- formatting_pattern = re.compile(r"\[p\]|\[botname\]")
-
- def replacement(m: re.Match) -> str:
- s = m.group(0)
- if s == "[p]":
- return ctx.clean_prefix
- if s == "[botname]":
- return ctx.me.display_name
- # We shouldnt get here:
- return s
-
- return formatting_pattern.sub(replacement, help_str)
+ return self.format_text_for_context(ctx, help_str)
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
"""Actively allow this command for the given model.
@@ -619,6 +647,28 @@ class Command(CogCommandMixin, DPYCommand):
"""
return super().error(coro)
+ def format_shortdoc_for_context(self, ctx: "Context") -> str:
+ """
+ This formats the short version of the help
+ tring based on values in context
+
+ See ``format_text_for_context`` for the actual implementation details
+
+ Cog creators may override this in their own command classes
+ as long as the method signature stays the same.
+
+ Parameters
+ ----------
+ ctx: Context
+
+ Returns
+ -------
+ str
+ Localized help with some formatting
+ """
+ sh = self.short_doc
+ return self.format_text_for_context(ctx, sh) if sh else sh
+
class GroupMixin(discord.ext.commands.GroupMixin):
"""Mixin for `Group` and `Red` classes.
diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py
index acb5d6d61..34d3a3700 100644
--- a/redbot/core/commands/help.py
+++ b/redbot/core/commands/help.py
@@ -224,7 +224,7 @@ class RedHelpFormatter:
return a_line[:67] + "..."
subtext = "\n".join(
- shorten_line(f"**{name}** {command.short_doc}")
+ shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
for name, command in sorted(subcommands.items())
)
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
@@ -249,7 +249,7 @@ class RedHelpFormatter:
doc_max_width = 80 - max_width
for nm, com in sorted(cmds):
width_gap = discord.utils._string_width(nm) - len(nm)
- doc = com.short_doc
+ doc = com.format_shortdoc_for_context(ctx)
if len(doc) > doc_max_width:
doc = doc[: doc_max_width - 3] + "..."
yield nm, doc, max_width - width_gap
@@ -399,7 +399,7 @@ class RedHelpFormatter:
return a_line[:67] + "..."
command_text = "\n".join(
- shorten_line(f"**{name}** {command.short_doc}")
+ shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
for name, command in sorted(coms.items())
)
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
@@ -423,7 +423,7 @@ class RedHelpFormatter:
doc_max_width = 80 - max_width
for nm, com in sorted(cmds):
width_gap = discord.utils._string_width(nm) - len(nm)
- doc = com.short_doc
+ doc = com.format_shortdoc_for_context(ctx)
if len(doc) > doc_max_width:
doc = doc[: doc_max_width - 3] + "..."
yield nm, doc, max_width - width_gap
@@ -466,7 +466,7 @@ class RedHelpFormatter:
return a_line[:67] + "..."
cog_text = "\n".join(
- shorten_line(f"**{name}** {command.short_doc}")
+ shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}")
for name, command in sorted(data.items())
)
@@ -494,7 +494,7 @@ class RedHelpFormatter:
doc_max_width = 80 - max_width
for nm, com in cmds:
width_gap = discord.utils._string_width(nm) - len(nm)
- doc = com.short_doc
+ doc = com.format_shortdoc_for_context(ctx)
if len(doc) > doc_max_width:
doc = doc[: doc_max_width - 3] + "..."
yield nm, doc, max_width - width_gap
From 97a9fde5fd2f9320be03684ca0d2461605578b61 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 20:01:22 -0500
Subject: [PATCH 74/91] slowmode should properly error out on 7 hours now
(#3453)
---
redbot/core/commands/converter.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/redbot/core/commands/converter.py b/redbot/core/commands/converter.py
index a2e604a03..66de0c114 100644
--- a/redbot/core/commands/converter.py
+++ b/redbot/core/commands/converter.py
@@ -282,14 +282,15 @@ else:
async def convert(self, ctx: "Context", argument: str) -> timedelta:
if self.default_unit and argument.isdecimal():
- delta = timedelta(**{self.default_unit: int(argument)})
- else:
- delta = parse_timedelta(
- argument,
- minimum=self.minimum,
- maximum=self.maximum,
- allowed_units=self.allowed_units,
- )
+ argument = argument + self.default_unit
+
+ delta = parse_timedelta(
+ argument,
+ minimum=self.minimum,
+ maximum=self.maximum,
+ allowed_units=self.allowed_units,
+ )
+
if delta is not None:
return delta
raise BadArgument() # This allows this to be a required argument.
From 0d3c72f356d0f6818151f2ba56deeec004066011 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 20:18:25 -0500
Subject: [PATCH 75/91] changelog and bump (#3454)
---
changelog.d/3152.feature.rst | 1 -
changelog.d/3241.misc.rst | 1 -
changelog.d/3378.bugfix.rst | 1 -
changelog.d/3427.misc.rst | 1 -
changelog.d/3433.feature.rst | 1 -
changelog.d/admin/3010.enhance.rst | 1 -
changelog.d/audio/3431.enhance.rst | 1 -
changelog.d/customcom/3416.bugfix.rst | 1 -
changelog.d/downloader/3409.misc.rst | 1 -
changelog.d/downloader/3415.bugfix.rst | 1 -
changelog.d/downloader/3444.misc.rst | 1 -
changelog.d/general/3284.bugfix.rst | 1 -
changelog.d/mod/2930.enhance.rst | 1 -
changelog.d/mod/2990.enhance.rst | 1 -
changelog.d/trivia/3427.bugfix.rst | 1 -
docs/changelog_3_3_0.rst | 68 ++++++++++++++++++++++++++
docs/index.rst | 1 +
redbot/__init__.py | 2 +-
setup.cfg | 2 +-
19 files changed, 71 insertions(+), 17 deletions(-)
delete mode 100644 changelog.d/3152.feature.rst
delete mode 100644 changelog.d/3241.misc.rst
delete mode 100644 changelog.d/3378.bugfix.rst
delete mode 100644 changelog.d/3427.misc.rst
delete mode 100644 changelog.d/3433.feature.rst
delete mode 100644 changelog.d/admin/3010.enhance.rst
delete mode 100644 changelog.d/audio/3431.enhance.rst
delete mode 100644 changelog.d/customcom/3416.bugfix.rst
delete mode 100644 changelog.d/downloader/3409.misc.rst
delete mode 100644 changelog.d/downloader/3415.bugfix.rst
delete mode 100644 changelog.d/downloader/3444.misc.rst
delete mode 100644 changelog.d/general/3284.bugfix.rst
delete mode 100644 changelog.d/mod/2930.enhance.rst
delete mode 100644 changelog.d/mod/2990.enhance.rst
delete mode 100644 changelog.d/trivia/3427.bugfix.rst
create mode 100644 docs/changelog_3_3_0.rst
diff --git a/changelog.d/3152.feature.rst b/changelog.d/3152.feature.rst
deleted file mode 100644
index ae12fe46f..000000000
--- a/changelog.d/3152.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Adds toggle for channels for embedset.
\ No newline at end of file
diff --git a/changelog.d/3241.misc.rst b/changelog.d/3241.misc.rst
deleted file mode 100644
index ea372bcf7..000000000
--- a/changelog.d/3241.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-implements ``__call__`` for commands
diff --git a/changelog.d/3378.bugfix.rst b/changelog.d/3378.bugfix.rst
deleted file mode 100644
index caff4e2ef..000000000
--- a/changelog.d/3378.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed an error when ``redbot.core.utils.mod.mass_purge`` is passed ``COUNT % 100 == 1`` messages AND the last message in the list does not exist.
diff --git a/changelog.d/3427.misc.rst b/changelog.d/3427.misc.rst
deleted file mode 100644
index 4ff7f6324..000000000
--- a/changelog.d/3427.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Typo fixes in doc strings.
\ No newline at end of file
diff --git a/changelog.d/3433.feature.rst b/changelog.d/3433.feature.rst
deleted file mode 100644
index 7a7d6446f..000000000
--- a/changelog.d/3433.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added a ``[p]helpset deletedelay`` command, that lets you set a delay (in seconds) after which help messages / pages will be deleted.
diff --git a/changelog.d/admin/3010.enhance.rst b/changelog.d/admin/3010.enhance.rst
deleted file mode 100644
index 5db617388..000000000
--- a/changelog.d/admin/3010.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-Role granting/removing commands will now notify when the user already has/doesn't have a role when attempting to add/remove it.
diff --git a/changelog.d/audio/3431.enhance.rst b/changelog.d/audio/3431.enhance.rst
deleted file mode 100644
index e2ed19a08..000000000
--- a/changelog.d/audio/3431.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-Playlist no longer default to the Server scope, and will not return matches across multiple scopes.
\ No newline at end of file
diff --git a/changelog.d/customcom/3416.bugfix.rst b/changelog.d/customcom/3416.bugfix.rst
deleted file mode 100644
index 672d5f032..000000000
--- a/changelog.d/customcom/3416.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixes error on exiting customcom interactive menu.
\ No newline at end of file
diff --git a/changelog.d/downloader/3409.misc.rst b/changelog.d/downloader/3409.misc.rst
deleted file mode 100644
index d2bc0e365..000000000
--- a/changelog.d/downloader/3409.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Improve error message when user passes cog that isn't installed to a command that only accepts installed cogs.
diff --git a/changelog.d/downloader/3415.bugfix.rst b/changelog.d/downloader/3415.bugfix.rst
deleted file mode 100644
index 305377c9c..000000000
--- a/changelog.d/downloader/3415.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Downloader will now do the initialization in background to avoid timeout issues during bot startup.
\ No newline at end of file
diff --git a/changelog.d/downloader/3444.misc.rst b/changelog.d/downloader/3444.misc.rst
deleted file mode 100644
index 9a3798bfb..000000000
--- a/changelog.d/downloader/3444.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
- Log errors that may happen in initialization task.
diff --git a/changelog.d/general/3284.bugfix.rst b/changelog.d/general/3284.bugfix.rst
deleted file mode 100644
index 8b89090ce..000000000
--- a/changelog.d/general/3284.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-[General] Adds a maximum amount to roll command.
\ No newline at end of file
diff --git a/changelog.d/mod/2930.enhance.rst b/changelog.d/mod/2930.enhance.rst
deleted file mode 100644
index c85c0238d..000000000
--- a/changelog.d/mod/2930.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added a defaultdays command to set the amount of days for the ban commands to use for days of messages deleted when days isn't used in the command itself.
diff --git a/changelog.d/mod/2990.enhance.rst b/changelog.d/mod/2990.enhance.rst
deleted file mode 100644
index e27d9caa3..000000000
--- a/changelog.d/mod/2990.enhance.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added a ``[p]modset dm`` to toggle kick/bans sending an embed to the user detailing the reason for the kick/ban, the server they were kicked/banned from and if it was a kick or a ban.
diff --git a/changelog.d/trivia/3427.bugfix.rst b/changelog.d/trivia/3427.bugfix.rst
deleted file mode 100644
index 7fc6be355..000000000
--- a/changelog.d/trivia/3427.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Trivia list typo fixes.
\ No newline at end of file
diff --git a/docs/changelog_3_3_0.rst b/docs/changelog_3_3_0.rst
new file mode 100644
index 000000000..2b5eaa617
--- /dev/null
+++ b/docs/changelog_3_3_0.rst
@@ -0,0 +1,68 @@
+.. 3.3.x Changelogs
+
+Redbot 3.3.0 (2020-01-26)
+=========================
+
+Core Bot
+--------
+
+- The bot's description is now configurable.
+- We now use discord.py 1.3.1, this comes with added teams support.
+- The commands module has been slightly restructured to provide more useful data to developers.
+- Help is now self consistent in the extra formatting used.
+
+Core Commands
+-------------
+
+- Slowmode should no longer error on nonsensical time quantities.
+- Embed use can be configured per channel as well.
+
+Documentation
+-------------
+
+- We've made some small fixes to inaccurate instructions about installing with pyenv.
+- Notes about deprecating in 3.3 have been altered to 3.4 to match the intended timeframe.
+
+Admin
+-----
+
+- Gives feedback when adding or removing a role doesn't make sense.
+
+Audio
+-----
+
+- Playlist finding is more intuitive.
+- disconnect and repeat commands no longer interfere with eachother.
+
+CustomCom
+---------
+
+- No longer errors when exiting an interactive menu.
+
+Cleanup
+-------
+
+- A rare edge case involving messages which are deleted during cleanup and are the only message was fixed.
+
+Downloader
+----------
+
+- Some user facing messages were improved.
+- Downloader's initialization can no longer time out at startup.
+
+General
+-------
+
+- Roll command will no longer attempt to roll obscenely large amounts.
+
+Mod
+---
+
+- You can set a default amount of days to clean up when banning.
+- Ban and hackban now use that default.
+- Users can now optionally be DMed their ban reason.
+
+Permissions
+-----------
+
+- Now has stronger enforcement of prioritizing botwide settings.
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index f81419b02..2a7a714dd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -57,6 +57,7 @@ Welcome to Red - Discord Bot's documentation!
:maxdepth: 2
:caption: Changelogs:
+ changelog_3_3_0
release_notes_3_2_0
changelog_3_2_0
changelog_3_1_0
diff --git a/redbot/__init__.py b/redbot/__init__.py
index a00782338..952272d94 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.2.4.dev1"
+__version__ = "3.3.0"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
diff --git a/setup.cfg b/setup.cfg
index 6e43c7d6b..ef68bef17 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,7 +38,7 @@ install_requires =
Click==7.0
colorama==0.4.3
contextlib2==0.5.5
- discord.py==1.3.0
+ discord.py==1.3.1
distro==1.4.0; sys_platform == "linux"
fuzzywuzzy==0.17.0
idna==2.8
From 00bcd480e77fa4507f7008abeade3a2bcecbdcf9 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Sun, 26 Jan 2020 20:39:38 -0500
Subject: [PATCH 76/91] dev bump (#3455)
---
redbot/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 952272d94..ca2e0dad9 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.3.0"
+__version__ = "3.3.0.dev1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From 7420df959800173c3909f4c08b874c4aa95d4035 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Mon, 27 Jan 2020 03:35:16 -0500
Subject: [PATCH 77/91] let's fix this for dev testers (#3458)
---
redbot/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/redbot/__init__.py b/redbot/__init__.py
index ca2e0dad9..1e2517064 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.3.0.dev1"
+__version__ = "3.3.1.dev1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From 6fa02b1a8db5387039500fde24988d6ef82499f8 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Tue, 28 Jan 2020 04:41:57 +0100
Subject: [PATCH 78/91] [Docs] Trigger update on sudo add-apt-repository
(#3464)
---
docs/install_linux_mac.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index c21e7bfe6..f7aec25cd 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -231,14 +231,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 +262,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:
From b64802b92f46f8440b7c245200d67b53b8dc021e Mon Sep 17 00:00:00 2001
From: Kowlin
Date: Thu, 30 Jan 2020 18:55:11 +0100
Subject: [PATCH 79/91] Fix for the unknown days argument on hackban. (#3475)
---
redbot/cogs/mod/kickban.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index b9ce3532a..5b710f15e 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -308,6 +308,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 +332,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:
From 17234ac8fa96cf25e74e4d36140f32ee696a54a2 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Sat, 1 Feb 2020 01:26:39 +0100
Subject: [PATCH 80/91] 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
---
changelog.d/3483.docs.rst | 1 +
docs/autostart_systemd.rst | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/3483.docs.rst
diff --git a/changelog.d/3483.docs.rst b/changelog.d/3483.docs.rst
new file mode 100644
index 000000000..8ca9a15da
--- /dev/null
+++ b/changelog.d/3483.docs.rst
@@ -0,0 +1 @@
+Add `-e` flag to `journalctl` command in systemd guide so that it takes the user to the end of logs automatically.
diff --git a/docs/autostart_systemd.rst b/docs/autostart_systemd.rst
index fcf8111e9..168c06d66 100644
--- a/docs/autostart_systemd.rst
+++ b/docs/autostart_systemd.rst
@@ -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:
-:code:`sudo journalctl -u red@instancename`
+:code:`sudo journalctl -eu red@instancename`
From 64106c771a4d7b258a7bb916bfbe1ba1898e1df5 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 3 Feb 2020 22:08:49 +0100
Subject: [PATCH 81/91] 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
---
changelog.d/3481.feature.rst | 1 +
redbot/__main__.py | 33 ++++++++++++++++++++++++++++++---
redbot/core/cli.py | 2 +-
3 files changed, 32 insertions(+), 4 deletions(-)
create mode 100644 changelog.d/3481.feature.rst
diff --git a/changelog.d/3481.feature.rst b/changelog.d/3481.feature.rst
new file mode 100644
index 000000000..3bd9aeab5
--- /dev/null
+++ b/changelog.d/3481.feature.rst
@@ -0,0 +1 @@
+Allow to edit prefix from command line using ``redbot --edit``.
\ No newline at end of file
diff --git a/redbot/__main__.py b/redbot/__main__.py
index 2bf085f7d..eb3a6af0c 100644
--- a/redbot/__main__.py
+++ b/redbot/__main__.py
@@ -107,6 +107,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 +120,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 +159,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):
diff --git a/redbot/core/cli.py b/redbot/core/cli.py
index f638530d1..02da9c854 100644
--- a/redbot/core/cli.py
+++ b/redbot/core/cli.py
@@ -90,7 +90,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",
From 8454239a988f8e05e68dc0a0b2ccadc810d27045 Mon Sep 17 00:00:00 2001
From: jack1142 <6032823+jack1142@users.noreply.github.com>
Date: Mon, 3 Feb 2020 22:14:19 +0100
Subject: [PATCH 82/91] [Mod] Fix shorthelp for `[p]modset dm` (#3488)
* Update settings.py
* Update settings.py
* Create 3488.misc.rst
* Update settings.py
---
changelog.d/mod/3488.misc.rst | 1 +
redbot/cogs/mod/settings.py | 3 +--
2 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/mod/3488.misc.rst
diff --git a/changelog.d/mod/3488.misc.rst b/changelog.d/mod/3488.misc.rst
new file mode 100644
index 000000000..f85079e47
--- /dev/null
+++ b/changelog.d/mod/3488.misc.rst
@@ -0,0 +1 @@
+Fix incomplete short help of ``[p]modset dm`` command.
diff --git a/redbot/cogs/mod/settings.py b/redbot/cogs/mod/settings.py
index 3da4833db..24dfc7fc9 100644
--- a/redbot/cogs/mod/settings.py
+++ b/redbot/cogs/mod/settings.py
@@ -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.
From e44fc69d1487331d4f25bb3b1ec73dd1f5ac0bb7 Mon Sep 17 00:00:00 2001
From: PredaaA <46051820+PredaaA@users.noreply.github.com>
Date: Mon, 3 Feb 2020 22:14:45 +0100
Subject: [PATCH 83/91] [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
* :facepalm:
* style
---
changelog.d/3474.feature.rst | 1 +
redbot/core/bot.py | 10 ++++++++++
redbot/core/cli.py | 25 +++++++++++++++++++++++++
3 files changed, 36 insertions(+)
create mode 100644 changelog.d/3474.feature.rst
diff --git a/changelog.d/3474.feature.rst b/changelog.d/3474.feature.rst
new file mode 100644
index 000000000..2a93f4989
--- /dev/null
+++ b/changelog.d/3474.feature.rst
@@ -0,0 +1 @@
+Add a cli flag for setting a max size of message cache
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
index 852c6281e..689379800 100644
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -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,
diff --git a/redbot/core/cli.py b/redbot/core/cli.py
index 02da9c854..54e401d08 100644
--- a/redbot/core/cli.py
+++ b/redbot/core/cli.py
@@ -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 [arguments]"
@@ -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)
From 12e6f4413526302b6e9967ee62262970144e263b Mon Sep 17 00:00:00 2001
From: aikaterna <20862007+aikaterna@users.noreply.github.com>
Date: Mon, 3 Feb 2020 13:26:33 -0800
Subject: [PATCH 84/91] [Core] No DMing the bot (#3478)
* [Core] No DMing the bot
* Return early if target user is a bot
---
redbot/core/core_commands.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
index f17ab6cce..dd9b8f939 100644
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -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
From 90b099395b596cd161837c683aebe12b19b1d3c4 Mon Sep 17 00:00:00 2001
From: Lane Babuder
Date: Mon, 3 Feb 2020 16:57:09 -0500
Subject: [PATCH 85/91] 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.
---
docs/install_linux_mac.rst | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/docs/install_linux_mac.rst b/docs/install_linux_mac.rst
index f7aec25cd..a4ab20cb0 100644
--- a/docs/install_linux_mac.rst
+++ b/docs/install_linux_mac.rst
@@ -67,6 +67,25 @@ Complete the rest of the installation by `installing Python 3.8 with pyenv `.
+
+----
+
.. _install-debian-stretch:
~~~~~~~~~~~~~~
From 61ed864e022c6793ae1ce75cce7039ea17c74a88 Mon Sep 17 00:00:00 2001
From: Kowlin
Date: Wed, 5 Feb 2020 22:02:06 +0100
Subject: [PATCH 86/91] CI ports from Travis CI (#3435)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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>
---
.github/CODEOWNERS | 1 +
.github/workflows/publish_crowdin.yml | 28 ++++++++++
.github/workflows/publish_pypi.yml | 26 ++++++++++
.github/workflows/tests.yml | 73 +++++++++++++++++++++++++++
4 files changed, 128 insertions(+)
create mode 100644 .github/workflows/publish_crowdin.yml
create mode 100644 .github/workflows/publish_pypi.yml
create mode 100644 .github/workflows/tests.yml
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 85f836a68..7a5be858d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -62,3 +62,4 @@ redbot/setup.py @tekulvw
# Others
.travis.yml @Kowlin
crowdin.yml @Kowlin
+.github/workflows/* @Kowlin
diff --git a/.github/workflows/publish_crowdin.yml b/.github/workflows/publish_crowdin.yml
new file mode 100644
index 000000000..f4a8f8519
--- /dev/null
+++ b/.github/workflows/publish_crowdin.yml
@@ -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
diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml
new file mode 100644
index 000000000..780326e6d
--- /dev/null
+++ b/.github/workflows/publish_pypi.yml
@@ -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/*
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 000000000..261b58d61
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -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
From 00cf3954838aee22a4e59355902f9314efb0b8f4 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Wed, 5 Feb 2020 17:16:13 -0500
Subject: [PATCH 87/91] 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>
---
redbot/__main__.py | 12 ++++++-----
redbot/cogs/downloader/downloader.py | 2 +-
redbot/cogs/downloader/repo_manager.py | 5 +----
redbot/core/utils/__init__.py | 29 +++++++++++++++++++-------
redbot/core/utils/menus.py | 5 ++++-
redbot/pytest/downloader.py | 13 +-----------
redbot/setup.py | 10 +++------
tests/conftest.py | 4 +++-
8 files changed, 41 insertions(+), 39 deletions(-)
diff --git a/redbot/__main__.py b/redbot/__main__.py
index eb3a6af0c..a15b9d2a6 100644
--- a/redbot/__main__.py
+++ b/redbot/__main__.py
@@ -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__
@@ -298,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:
@@ -310,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)
@@ -460,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(
@@ -524,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
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
index ed48f8cfd..162a748af 100644
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -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)
diff --git a/redbot/cogs/downloader/repo_manager.py b/redbot/cogs/downloader/repo_manager.py
index fe786ef98..f05a4b94e 100644
--- a/redbot/cogs/downloader/repo_manager.py
+++ b/redbot/cogs/downloader/repo_manager.py
@@ -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),
)
diff --git a/redbot/core/utils/__init__.py b/redbot/core/utils/__init__.py
index dfdab23b0..0da1213df 100644
--- a/redbot/core/utils/__init__.py
+++ b/redbot/core/utils/__init__.py
@@ -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)
diff --git a/redbot/core/utils/menus.py b/redbot/core/utils/menus.py
index 6305b19ff..f4e9cf364 100644
--- a/redbot/core/utils/menus.py
+++ b/redbot/core/utils/menus.py
@@ -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())
diff --git a/redbot/pytest/downloader.py b/redbot/pytest/downloader.py
index 0ac043791..dbeb2e6f6 100644
--- a/redbot/pytest/downloader.py
+++ b/redbot/pytest/downloader.py
@@ -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
diff --git a/redbot/setup.py b/redbot/setup.py
index 2be440acc..5ed245a5e 100644
--- a/redbot/setup.py
+++ b/redbot/setup.py
@@ -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():
diff --git a/tests/conftest.py b/tests/conftest.py
index cb420c900..32038d835 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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()
From c426aefd1afa1c386bcc7b967be49898005f1e23 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Wed, 5 Feb 2020 17:21:38 -0500
Subject: [PATCH 88/91] Version 3.3.1 (#3510)
* 331
* okay sphinx
---
changelog.d/3474.feature.rst | 1 -
changelog.d/3481.feature.rst | 1 -
changelog.d/3483.docs.rst | 1 -
changelog.d/mod/3488.misc.rst | 1 -
docs/changelog_3_3_0.rst | 34 ++++++++++++++++++++++++++++++++++
redbot/__init__.py | 2 +-
6 files changed, 35 insertions(+), 5 deletions(-)
delete mode 100644 changelog.d/3474.feature.rst
delete mode 100644 changelog.d/3481.feature.rst
delete mode 100644 changelog.d/3483.docs.rst
delete mode 100644 changelog.d/mod/3488.misc.rst
diff --git a/changelog.d/3474.feature.rst b/changelog.d/3474.feature.rst
deleted file mode 100644
index 2a93f4989..000000000
--- a/changelog.d/3474.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Add a cli flag for setting a max size of message cache
diff --git a/changelog.d/3481.feature.rst b/changelog.d/3481.feature.rst
deleted file mode 100644
index 3bd9aeab5..000000000
--- a/changelog.d/3481.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Allow to edit prefix from command line using ``redbot --edit``.
\ No newline at end of file
diff --git a/changelog.d/3483.docs.rst b/changelog.d/3483.docs.rst
deleted file mode 100644
index 8ca9a15da..000000000
--- a/changelog.d/3483.docs.rst
+++ /dev/null
@@ -1 +0,0 @@
-Add `-e` flag to `journalctl` command in systemd guide so that it takes the user to the end of logs automatically.
diff --git a/changelog.d/mod/3488.misc.rst b/changelog.d/mod/3488.misc.rst
deleted file mode 100644
index f85079e47..000000000
--- a/changelog.d/mod/3488.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fix incomplete short help of ``[p]modset dm`` command.
diff --git a/docs/changelog_3_3_0.rst b/docs/changelog_3_3_0.rst
index 2b5eaa617..c59e82437 100644
--- a/docs/changelog_3_3_0.rst
+++ b/docs/changelog_3_3_0.rst
@@ -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)
=========================
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 1e2517064..843d52d97 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.3.1.dev1"
+__version__ = "3.3.1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From 0adc960c60687f267a40e77eb54f959bddb86de1 Mon Sep 17 00:00:00 2001
From: Michael H
Date: Wed, 5 Feb 2020 17:32:05 -0500
Subject: [PATCH 89/91] dev bump (#3512)
---
redbot/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/redbot/__init__.py b/redbot/__init__.py
index 843d52d97..16461d28d 100644
--- a/redbot/__init__.py
+++ b/redbot/__init__.py
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
-__version__ = "3.3.1"
+__version__ = "3.3.2.dev1"
version_info = VersionInfo.from_str(__version__)
# Filter fuzzywuzzy slow sequence matcher warning
From 1fc4ece14c5a661c748b5aae5773047260fc2007 Mon Sep 17 00:00:00 2001
From: Kowlin
Date: Wed, 5 Feb 2020 23:32:35 +0100
Subject: [PATCH 90/91] Updated readme badges. (#3511)
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index d65963747..9bc2e6c0a 100644
--- a/README.md
+++ b/README.md
@@ -26,8 +26,8 @@
-
-
+
+
From 8d73838d8033c79893a3a2440ed28b5f7b663c62 Mon Sep 17 00:00:00 2001
From: trundleroo <57276947+trundleroo@users.noreply.github.com>
Date: Thu, 6 Feb 2020 13:27:32 -0400
Subject: [PATCH 91/91] Update announcer.py (#3514)
* Update announcer.py
* Update announcer.py
---
redbot/cogs/admin/announcer.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/redbot/cogs/admin/announcer.py b/redbot/cogs/admin/announcer.py
index f0045b167..3aa420b4b 100644
--- a/redbot/cogs/admin/announcer.py
+++ b/redbot/cogs/admin/announcer.py
@@ -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