mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
Merge branch 'V3/develop' into V3/feature/mutes
This commit is contained in:
commit
4c77cde249
9
.github/CONTRIBUTING.md
vendored
9
.github/CONTRIBUTING.md
vendored
@ -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.
|
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
|
# 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.
|
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:
|
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||||
@ -54,7 +53,7 @@ Red's repository is configured to follow a particular development workflow, usin
|
|||||||
|
|
||||||
### 4.1 Setting up your development environment
|
### 4.1 Setting up your development environment
|
||||||
The following requirements must be installed prior to setting up:
|
The following requirements must be installed prior to setting up:
|
||||||
- Python 3.7.0 or greater
|
- Python 3.8.1 or greater
|
||||||
- git
|
- git
|
||||||
- pip
|
- 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.
|
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||||
|
|
||||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on 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 documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||||
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||||
|
|
||||||
@ -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.
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
7
.github/workflows/auto_labeler.yml
vendored
7
.github/workflows/auto_labeler.yml
vendored
@ -13,9 +13,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
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({
|
github.issues.addLabels({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
labels: ['Status: Needs Triage']
|
labels: ['Status: Needs Triage']
|
||||||
})
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.python.org/downloads/">
|
<a href="https://www.python.org/downloads/">
|
||||||
<img src="https://img.shields.io/badge/Made%20With-Python%203.7-blue.svg?style=for-the-badge" alt="Made with Python 3.7">
|
<img src="https://img.shields.io/badge/Made%20With-Python%203.8-blue.svg?style=for-the-badge" alt="Made with Python 3.8">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://crowdin.com/project/red-discordbot">
|
<a href="https://crowdin.com/project/red-discordbot">
|
||||||
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
||||||
|
|||||||
9
docs/_templates/layout.html
vendored
Normal file
9
docs/_templates/layout.html
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends '!layout.html' %}
|
||||||
|
{% block document %}
|
||||||
|
{{ super() }}
|
||||||
|
<a href="https://github.com/Cog-Creators/Red-DiscordBot">
|
||||||
|
<img style="position: absolute; top: 0; right: 0; border: 0;"
|
||||||
|
src="https://github.blog/wp-content/uploads/2008/12/forkme_right_darkblue_121621.png?resize=149%2C149"
|
||||||
|
class="attachment-full size-full" alt="Fork me on GitHub">
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
@ -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.
|
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`.
|
You can add additional Red based arguments after the instance, such as :code:`--dev`.
|
||||||
|
|
||||||
:code:`pm2 start redbot --name "<Insert a name here>" --interpreter "<Location to your Python Interpreter>" -- <Red Instance> --no-prompt`
|
.. code-block:: none
|
||||||
|
|
||||||
|
pm2 start redbot --name "<Insert a name here>" --interpreter "<Location to your Python Interpreter>" --interpreter-args "-O" -- <Red Instance> --no-prompt
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
Arguments to replace.
|
Arguments to replace.
|
||||||
|
|
||||||
--name ""
|
<Insert a name here>
|
||||||
A name to identify the bot within pm2, this is not your Red instance.
|
A name to identify the bot within pm2, this is not your Red instance.
|
||||||
|
|
||||||
--interpreter ""
|
<Location to your Python Interpreter>
|
||||||
The location of your Python interpreter, to find out where that is use the following command:
|
The location of your Python interpreter, to find out where that is use the following command inside activated venv:
|
||||||
which python3.6
|
which python
|
||||||
|
|
||||||
<Red Instance>
|
<Red Instance>
|
||||||
The name of your Red instance.
|
The name of your Red instance.
|
||||||
|
|||||||
@ -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
|
# If you are using pyenv
|
||||||
pyenv shell <name>
|
pyenv shell <name>
|
||||||
|
|
||||||
which redbot
|
which python
|
||||||
|
|
||||||
Then create the new service file:
|
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
|
After=multi-user.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=path %I --no-prompt
|
ExecStart=path -O -m redbot %I --no-prompt
|
||||||
User=username
|
User=username
|
||||||
Group=username
|
Group=username
|
||||||
Type=idle
|
Type=idle
|
||||||
|
|||||||
@ -1,5 +1,68 @@
|
|||||||
.. 3.2.x Changelogs
|
.. 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 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.
|
||||||
|
|
||||||
|
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 on new ``format_help_for_context`` method.
|
||||||
|
- 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 details 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)
|
Redbot 3.2.2 (2020-01-10)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
@ -49,7 +112,8 @@ Breaking Changes
|
|||||||
- ``bot.get_mod_role_ids`` (`#2967 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2967>`_)
|
- ``bot.get_mod_role_ids`` (`#2967 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2967>`_)
|
||||||
- Reserved some command names for internal Red use. These are available programatically as ``redbot.core.commands.RESERVED_COMMAND_NAMES``. (`#2973 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2973>`_)
|
- Reserved some command names for internal Red use. These are available programatically as ``redbot.core.commands.RESERVED_COMMAND_NAMES``. (`#2973 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2973>`_)
|
||||||
- Removed ``bot._counter``, Made a few more attrs private (``cog_mgr``, ``main_dir``). (`#2976 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2976>`_)
|
- Removed ``bot._counter``, Made a few more attrs private (``cog_mgr``, ``main_dir``). (`#2976 <https://github.com/Cog-Creators/Red-DiscordBot/issues/2976>`_)
|
||||||
- ``bot.wait_until_ready`` should no longer be used during extension setup. (`#3073 <https://github.com/Cog-Creators/Red-DiscordBot/issues/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 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3073>`_)
|
||||||
- Removed the mongo driver. (`#3099 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3099>`_)
|
- Removed the mongo driver. (`#3099 <https://github.com/Cog-Creators/Red-DiscordBot/issues/3099>`_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -60,3 +60,16 @@ Event Reference
|
|||||||
:type service_name: :class:`str`
|
:type service_name: :class:`str`
|
||||||
:param api_tokens: New Mapping of token names to tokens. This contains api tokens that weren't changed too.
|
: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`]
|
: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
|
||||||
|
|||||||
@ -15,6 +15,7 @@ extend functionlities used throughout the bot, as outlined below.
|
|||||||
|
|
||||||
.. autoclass:: redbot.core.commands.Command
|
.. autoclass:: redbot.core.commands.Command
|
||||||
:members:
|
:members:
|
||||||
|
:inherited-members: format_help_for_context
|
||||||
|
|
||||||
.. autoclass:: redbot.core.commands.Group
|
.. autoclass:: redbot.core.commands.Group
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
@ -25,7 +25,7 @@ Basic Usage
|
|||||||
async def ban(self, ctx, user: discord.Member, reason: str = None):
|
async def ban(self, ctx, user: discord.Member, reason: str = None):
|
||||||
await ctx.guild.ban(user)
|
await ctx.guild.ban(user)
|
||||||
case = await modlog.create_case(
|
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
|
user=user, moderator=ctx.author, reason=reason
|
||||||
)
|
)
|
||||||
await ctx.send("Done. It was about time.")
|
await ctx.send("Done. It was about time.")
|
||||||
|
|||||||
@ -19,12 +19,22 @@ Please install the pre-requirements using the commands listed for your operating
|
|||||||
The pre-requirements are:
|
The pre-requirements are:
|
||||||
- Python 3.8.1 or greater
|
- Python 3.8.1 or greater
|
||||||
- Pip 18.1 or greater
|
- Pip 18.1 or greater
|
||||||
- Git
|
- Git 2.11+
|
||||||
- Java Runtime Environment 11 or later (for audio support)
|
- Java Runtime Environment 11 or later (for audio support)
|
||||||
|
|
||||||
We also recommend installing some basic compiler tools, in case our dependencies don't provide
|
We also recommend installing some basic compiler tools, in case our dependencies don't provide
|
||||||
pre-built "wheels" for your architecture.
|
pre-built "wheels" for your architecture.
|
||||||
|
|
||||||
|
|
||||||
|
*****************
|
||||||
|
Operating systems
|
||||||
|
*****************
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-arch:
|
.. _install-arch:
|
||||||
|
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@ -35,6 +45,10 @@ Arch Linux
|
|||||||
|
|
||||||
sudo pacman -Syu python python-pip git jre-openjdk-headless base-devel
|
sudo pacman -Syu python python-pip git jre-openjdk-headless base-devel
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-centos:
|
.. _install-centos:
|
||||||
.. _install-rhel:
|
.. _install-rhel:
|
||||||
|
|
||||||
@ -51,15 +65,44 @@ CentOS and RHEL 7
|
|||||||
|
|
||||||
Complete the rest of the installation by `installing Python 3.8 with pyenv <install-python-pyenv>`.
|
Complete the rest of the installation by `installing Python 3.8 with pyenv <install-python-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-python-pyenv>`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-debian:
|
.. _install-debian:
|
||||||
.. _install-raspbian:
|
.. _install-raspbian:
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Debian and Raspbian
|
Debian and Raspbian Buster
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
We recommend installing pyenv as a method of installing non-native versions of python on
|
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
|
.. code-block:: none
|
||||||
|
|
||||||
@ -71,6 +114,8 @@ Debian/Raspbian. 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-python-pyenv>`.
|
Complete the rest of the installation by `installing Python 3.8 with pyenv <install-python-pyenv>`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-fedora:
|
.. _install-fedora:
|
||||||
|
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
@ -84,6 +129,10 @@ them with dnf:
|
|||||||
|
|
||||||
sudo dnf -y install python38 git java-latest-openjdk-headless @development-tools
|
sudo dnf -y install python38 git java-latest-openjdk-headless @development-tools
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-mac:
|
.. _install-mac:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
@ -110,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
|
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``.
|
the Python 3.8 folder then double click ``Install certificates.command``.
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-opensuse:
|
.. _install-opensuse:
|
||||||
|
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
@ -150,6 +203,8 @@ Now, install pip with easy_install:
|
|||||||
|
|
||||||
sudo /opt/python/bin/easy_install-3.8 pip
|
sudo /opt/python/bin/easy_install-3.8 pip
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
openSUSE Tumbleweed
|
openSUSE Tumbleweed
|
||||||
*******************
|
*******************
|
||||||
|
|
||||||
@ -161,35 +216,74 @@ with zypper:
|
|||||||
sudo zypper install python3-base python3-pip git-core java-12-openjdk-headless
|
sudo zypper install python3-base python3-pip git-core java-12-openjdk-headless
|
||||||
sudo zypper install -t pattern devel_basis
|
sudo zypper install -t pattern devel_basis
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-ubuntu:
|
.. _install-ubuntu:
|
||||||
|
|
||||||
~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Ubuntu
|
Ubuntu LTS versions (18.04 and 16.04)
|
||||||
~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. note:: **Ubuntu Python Availability**
|
We recommend adding the ``git-core`` ppa to install Git 2.11 or greater:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt update
|
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:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt -y install python3.8 python3.8-dev python3.8-venv python3-pip git default-jre-headless \
|
sudo apt -y install python3.8 python3.8-dev python3.8-venv python3-pip git default-jre-headless \
|
||||||
build-essential
|
build-essential
|
||||||
|
|
||||||
|
Continue by `creating-venv-linux`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
.. _install-ubuntu-non-lts:
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Ubuntu non-LTS versions
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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++
|
||||||
|
|
||||||
|
And then complete the rest of the installation by `installing Python 3.8 with pyenv <install-python-pyenv>`.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. _install-python-pyenv:
|
.. _install-python-pyenv:
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
****************************
|
||||||
Installing Python with pyenv
|
Installing Python with pyenv
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
****************************
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -227,11 +321,15 @@ After that is finished, run:
|
|||||||
|
|
||||||
Pyenv is now installed and your system should be configured to run Python 3.8.
|
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
|
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`.
|
straightforward. See the section `installing-in-virtual-environment`.
|
||||||
|
|
||||||
.. _installing-red-linux-mac:
|
.. _installing-red-linux-mac:
|
||||||
@ -242,31 +340,25 @@ Installing Red
|
|||||||
|
|
||||||
Choose one of the following commands to install 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:
|
To install without additional config backend support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python3.8 -m pip install -U setuptools wheel
|
python -m pip install -U pip setuptools wheel
|
||||||
python3.8 -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
Or, to install with PostgreSQL support:
|
Or, to install with PostgreSQL support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python3.8 -m pip install -U setuptools wheel
|
python -m pip install -U pip setuptools wheel
|
||||||
python3.8 -m pip install -U Red-DiscordBot[postgres]
|
python -m pip install -U Red-DiscordBot[postgres]
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
These commands are also used for updating Red
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
Setting Up and Running Red
|
Setting Up and Running Red
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|||||||
@ -64,6 +64,13 @@ Manually installing dependencies
|
|||||||
|
|
||||||
.. _installing-red-windows:
|
.. _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
|
Installing Red
|
||||||
--------------
|
--------------
|
||||||
@ -72,34 +79,27 @@ Installing Red
|
|||||||
for the PATH changes to take effect.
|
for the PATH changes to take effect.
|
||||||
|
|
||||||
1. Open a command prompt (open Start, search for "command prompt", then click it)
|
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`
|
2. Run **one** of the following set of commands, depending on what extras you want installed
|
||||||
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
|
|
||||||
|
|
||||||
* Normal installation:
|
* Normal installation:
|
||||||
|
|
||||||
.. code-block:: none
|
.. 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
|
python -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
* With PostgreSQL support:
|
* With PostgreSQL support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. 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]
|
python -m pip install -U Red-DiscordBot[postgres]
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
These commands are also used for updating Red
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
Setting Up and Running Red
|
Setting Up and Running Red
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|||||||
@ -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-venv` (quick and easy, involves two commands)
|
||||||
* :ref:`using-pyenv-virtualenv` (recommended if you installed Python with pyenv)
|
* :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?**
|
**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
|
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
|
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.
|
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
|
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
|
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
|
``venv`` on Linux or Mac
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Create your virtual environment with the following command::
|
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::
|
And activate it with the following command::
|
||||||
|
|
||||||
source redenv/bin/activate
|
source ~/redenv/bin/activate
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
@ -56,11 +51,11 @@ Continue reading `below <after-activating-virtual-environment>`.
|
|||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
Create your virtual environment with the following command::
|
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::
|
And activate it with the following command::
|
||||||
|
|
||||||
redenv\Scripts\activate.bat
|
%userprofile%\redenv\Scripts\activate.bat
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
|
|||||||
@ -181,9 +181,7 @@ class VersionInfo:
|
|||||||
|
|
||||||
|
|
||||||
def _update_event_loop_policy():
|
def _update_event_loop_policy():
|
||||||
if _sys.platform == "win32":
|
if _sys.implementation.name == "cpython":
|
||||||
_asyncio.set_event_loop_policy(_asyncio.WindowsProactorEventLoopPolicy())
|
|
||||||
elif _sys.implementation.name == "cpython":
|
|
||||||
# Let's not force this dependency, uvloop is much faster on cpython
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
try:
|
try:
|
||||||
import uvloop as _uvloop
|
import uvloop as _uvloop
|
||||||
@ -193,7 +191,7 @@ def _update_event_loop_policy():
|
|||||||
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.2.3.dev1"
|
__version__ = "3.2.4.dev1"
|
||||||
version_info = VersionInfo.from_str(__version__)
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import sys
|
|||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
@ -287,7 +288,18 @@ def handle_edit(cli_flags: Namespace):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(red: Red, cli_flags: Namespace):
|
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()
|
driver_cls = drivers.get_driver_class()
|
||||||
|
|
||||||
@ -341,6 +353,10 @@ async def run_bot(red: Red, cli_flags: Namespace):
|
|||||||
if confirm("\nDo you want to reset the token?"):
|
if confirm("\nDo you want to reset the token?"):
|
||||||
await red._config.token.set("")
|
await red._config.token.set("")
|
||||||
print("Token has been reset.")
|
print("Token has been reset.")
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def handle_early_exit_flags(cli_flags: Namespace):
|
def handle_early_exit_flags(cli_flags: Namespace):
|
||||||
@ -474,14 +490,13 @@ def main():
|
|||||||
# Allows transports to close properly, and prevent new ones from being opened.
|
# Allows transports to close properly, and prevent new ones from being opened.
|
||||||
# Transports may still not be closed correcly on windows, see below
|
# Transports may still not be closed correcly on windows, see below
|
||||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||||
if os.name == "nt":
|
# *we* aren't cleaning up more here, but it prevents
|
||||||
# *we* aren't cleaning up more here, but it prevents
|
# a runtime error at the event loop on windows
|
||||||
# a runtime error at the event loop on windows
|
# with resources which require longer to clean up.
|
||||||
# with resources which require longer to clean up.
|
# With other event loops, a failure to cleanup prior to here
|
||||||
# With other event loops, a failure to cleanup prior to here
|
# results in a resource warning instead
|
||||||
# results in a resource warning instead and does not break us.
|
log.info("Please wait, cleaning up a bit more")
|
||||||
log.info("Please wait, cleaning up a bit more")
|
loop.run_until_complete(asyncio.sleep(2))
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
|
||||||
loop.stop()
|
loop.stop()
|
||||||
loop.close()
|
loop.close()
|
||||||
exit_code = red._shutdown_mode if red is not None else 1
|
exit_code = red._shutdown_mode if red is not None else 1
|
||||||
|
|||||||
@ -116,12 +116,14 @@ class Admin(commands.Cog):
|
|||||||
:param role:
|
:param role:
|
||||||
:return:
|
: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:
|
if member is None:
|
||||||
member = ctx.author
|
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))
|
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
|
||||||
return
|
return
|
||||||
if not self.pass_hierarchy_check(ctx, role):
|
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:
|
if member is None:
|
||||||
member = ctx.author
|
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))
|
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
|
||||||
return
|
return
|
||||||
if not self.pass_hierarchy_check(ctx, role):
|
if not self.pass_hierarchy_check(ctx, role):
|
||||||
@ -365,7 +369,7 @@ class Admin(commands.Cog):
|
|||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, ctx.author, selfrole)
|
await self._addrole(ctx, ctx.author, selfrole, check_user=False)
|
||||||
|
|
||||||
@selfrole.command(name="remove")
|
@selfrole.command(name="remove")
|
||||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
@ -376,7 +380,7 @@ class Admin(commands.Cog):
|
|||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, ctx.author, selfrole)
|
await self._removerole(ctx, ctx.author, selfrole, check_user=False)
|
||||||
|
|
||||||
@selfrole.command(name="list")
|
@selfrole.command(name="list")
|
||||||
async def selfrole_list(self, ctx: commands.Context):
|
async def selfrole_list(self, ctx: commands.Context):
|
||||||
@ -406,6 +410,13 @@ class Admin(commands.Cog):
|
|||||||
|
|
||||||
NOTE: The role is case sensitive!
|
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:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
if role.id not in curr_selfroles:
|
if role.id not in curr_selfroles:
|
||||||
curr_selfroles.append(role.id)
|
curr_selfroles.append(role.id)
|
||||||
@ -421,6 +432,13 @@ class Admin(commands.Cog):
|
|||||||
|
|
||||||
NOTE: The role is case sensitive!
|
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:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
curr_selfroles.remove(role.id)
|
curr_selfroles.remove(role.id)
|
||||||
|
|
||||||
|
|||||||
@ -746,13 +746,17 @@ class MusicCache:
|
|||||||
(val, update) = await self.database.fetch_one("lavalink", "data", {"query": query})
|
(val, update) = await self.database.fetch_one("lavalink", "data", {"query": query})
|
||||||
if update:
|
if update:
|
||||||
val = None
|
val = None
|
||||||
if val and not isinstance(val, str):
|
if val and isinstance(val, dict):
|
||||||
log.debug(f"Querying Local Database for {query}")
|
log.debug(f"Querying Local Database for {query}")
|
||||||
task = ("update", ("lavalink", {"query": query}))
|
task = ("update", ("lavalink", {"query": query}))
|
||||||
self.append_task(ctx, *task)
|
self.append_task(ctx, *task)
|
||||||
if val and not forced:
|
else:
|
||||||
|
val = None
|
||||||
|
if val and not forced and isinstance(val, dict):
|
||||||
data = val
|
data = val
|
||||||
data["query"] = query
|
data["query"] = query
|
||||||
|
if data.get("loadType") == "V2_COMPACT":
|
||||||
|
data["loadType"] = "V2_COMPAT"
|
||||||
results = LoadResult(data)
|
results = LoadResult(data)
|
||||||
called_api = False
|
called_api = False
|
||||||
if results.has_error:
|
if results.has_error:
|
||||||
@ -778,21 +782,25 @@ class MusicCache:
|
|||||||
):
|
):
|
||||||
with contextlib.suppress(SQLError):
|
with contextlib.suppress(SQLError):
|
||||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||||
task = (
|
data = json.dumps(results._raw)
|
||||||
"insert",
|
if all(
|
||||||
(
|
k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
|
||||||
"lavalink",
|
):
|
||||||
[
|
task = (
|
||||||
{
|
"insert",
|
||||||
"query": query,
|
(
|
||||||
"data": json.dumps(results._raw),
|
"lavalink",
|
||||||
"last_updated": time_now,
|
[
|
||||||
"last_fetched": time_now,
|
{
|
||||||
}
|
"query": query,
|
||||||
],
|
"data": data,
|
||||||
),
|
"last_updated": time_now,
|
||||||
)
|
"last_fetched": time_now,
|
||||||
self.append_task(ctx, *task)
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.append_task(ctx, *task)
|
||||||
return results, called_api
|
return results, called_api
|
||||||
|
|
||||||
async def run_tasks(self, ctx: Optional[commands.Context] = None, _id=None):
|
async def run_tasks(self, ctx: Optional[commands.Context] = None, _id=None):
|
||||||
@ -853,10 +861,12 @@ class MusicCache:
|
|||||||
query_data["maxage"] = maxage_int
|
query_data["maxage"] = maxage_int
|
||||||
|
|
||||||
vals = await self.database.fetch_all("lavalink", "data", query_data)
|
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:
|
if recently_played:
|
||||||
track = random.choice(recently_played)
|
track = random.choice(recently_played)
|
||||||
|
if track.get("loadType") == "V2_COMPACT":
|
||||||
|
track["loadType"] = "V2_COMPAT"
|
||||||
results = LoadResult(track)
|
results = LoadResult(track)
|
||||||
tracks = list(results.tracks)
|
tracks = list(results.tracks)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -245,15 +245,20 @@ class Audio(commands.Cog):
|
|||||||
for t in tracks_in_playlist:
|
for t in tracks_in_playlist:
|
||||||
uri = t.get("info", {}).get("uri")
|
uri = t.get("info", {}).get("uri")
|
||||||
if uri:
|
if uri:
|
||||||
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
|
t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
|
||||||
database_entries.append(
|
data = json.dumps(t)
|
||||||
{
|
if all(
|
||||||
"query": uri,
|
k in data
|
||||||
"data": json.dumps(t),
|
for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
|
||||||
"last_updated": time_now,
|
):
|
||||||
"last_fetched": time_now,
|
database_entries.append(
|
||||||
}
|
{
|
||||||
)
|
"query": uri,
|
||||||
|
"data": data,
|
||||||
|
"last_updated": time_now,
|
||||||
|
"last_fetched": time_now,
|
||||||
|
}
|
||||||
|
)
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
if guild_playlist:
|
if guild_playlist:
|
||||||
all_playlist[str(guild_id)] = guild_playlist
|
all_playlist[str(guild_id)] = guild_playlist
|
||||||
@ -530,17 +535,18 @@ class Audio(commands.Cog):
|
|||||||
player_check = await self._players_check()
|
player_check = await self._players_check()
|
||||||
await self._status_check(*player_check)
|
await self._status_check(*player_check)
|
||||||
|
|
||||||
if not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
|
if event_type == lavalink.LavalinkEvents.QUEUE_END:
|
||||||
notify_channel = player.fetch("channel")
|
if not autoplay:
|
||||||
if notify_channel:
|
notify_channel = player.fetch("channel")
|
||||||
notify_channel = self.bot.get_channel(notify_channel)
|
if notify_channel and notify:
|
||||||
await self._embed_msg(notify_channel, title=_("Queue Ended."))
|
notify_channel = self.bot.get_channel(notify_channel)
|
||||||
elif not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and disconnect:
|
await self._embed_msg(notify_channel, title=_("Queue Ended."))
|
||||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
if disconnect:
|
||||||
await player.disconnect()
|
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and status:
|
await player.disconnect()
|
||||||
player_check = await self._players_check()
|
if status:
|
||||||
await self._status_check(*player_check)
|
player_check = await self._players_check()
|
||||||
|
await self._status_check(*player_check)
|
||||||
|
|
||||||
if event_type in [
|
if event_type in [
|
||||||
lavalink.LavalinkEvents.TRACK_EXCEPTION,
|
lavalink.LavalinkEvents.TRACK_EXCEPTION,
|
||||||
@ -690,7 +696,7 @@ class Audio(commands.Cog):
|
|||||||
async def dc(self, ctx: commands.Context):
|
async def dc(self, ctx: commands.Context):
|
||||||
"""Toggle the bot auto-disconnecting when done playing.
|
"""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()
|
disconnect = await self.config.guild(ctx.guild).disconnect()
|
||||||
@ -1117,7 +1123,7 @@ class Audio(commands.Cog):
|
|||||||
"""Set a playlist to auto-play songs from.
|
"""Set a playlist to auto-play songs from.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]audioset autoplay playlist_name_OR_id args
|
`[p]audioset autoplay playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -1140,9 +1146,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]audioset autoplay MyGuildPlaylist
|
`[p]audioset autoplay MyGuildPlaylist`
|
||||||
[p]audioset autoplay MyGlobalPlaylist --scope Global
|
`[p]audioset autoplay MyGlobalPlaylist --scope Global`
|
||||||
[p]audioset autoplay PersonalPlaylist --scope User --author Draper
|
`[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -1253,7 +1259,10 @@ class Audio(commands.Cog):
|
|||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def emptydisconnect(self, ctx: commands.Context, seconds: int):
|
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:
|
if seconds < 0:
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx, title=_("Invalid Time"), description=_("Seconds can't be less than zero.")
|
ctx, title=_("Invalid Time"), description=_("Seconds can't be less than zero.")
|
||||||
@ -2443,7 +2452,11 @@ class Audio(commands.Cog):
|
|||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
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(
|
async def _folder_list(
|
||||||
self, ctx: commands.Context, query: audio_dataclasses.Query
|
self, ctx: commands.Context, query: audio_dataclasses.Query
|
||||||
@ -2454,9 +2467,9 @@ class Audio(commands.Cog):
|
|||||||
if not query.track.exists():
|
if not query.track.exists():
|
||||||
return
|
return
|
||||||
return (
|
return (
|
||||||
query.track.tracks_in_tree()
|
await query.track.tracks_in_tree()
|
||||||
if query.search_subfolders
|
if query.search_subfolders
|
||||||
else query.track.tracks_in_folder()
|
else await query.track.tracks_in_folder()
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _folder_tracks(
|
async def _folder_tracks(
|
||||||
@ -2495,9 +2508,9 @@ class Audio(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
return (
|
return (
|
||||||
query.track.tracks_in_tree()
|
await query.track.tracks_in_tree()
|
||||||
if query.search_subfolders
|
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:
|
async def _localtracks_check(self, ctx: commands.Context) -> bool:
|
||||||
@ -2948,8 +2961,7 @@ class Audio(commands.Cog):
|
|||||||
return await self._embed_msg(ctx, embed=embed)
|
return await self._embed_msg(ctx, embed=embed)
|
||||||
elif isinstance(tracks, discord.Message):
|
elif isinstance(tracks, discord.Message):
|
||||||
return
|
return
|
||||||
queue_dur = await queue_duration(ctx)
|
queue_dur = await track_remaining_duration(ctx)
|
||||||
lavalink.utils.format_time(queue_dur)
|
|
||||||
index = query.track_index
|
index = query.track_index
|
||||||
seek = 0
|
seek = 0
|
||||||
if query.start_time:
|
if query.start_time:
|
||||||
@ -3996,7 +4008,7 @@ class Audio(commands.Cog):
|
|||||||
The track(s) will be appended to the end of the playlist.
|
The track(s) will be appended to the end of the playlist.
|
||||||
|
|
||||||
**Usage**:
|
**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**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4019,10 +4031,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist append MyGuildPlaylist Hello by Adele
|
`[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`
|
||||||
[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global
|
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
|
||||||
--Author Draper#6666
|
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4144,8 +4155,8 @@ class Audio(commands.Cog):
|
|||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.cooldown(1, 300, commands.BucketType.member)
|
@commands.cooldown(1, 150, commands.BucketType.member)
|
||||||
@playlist.command(name="copy", usage="<id_or_name> [args]")
|
@playlist.command(name="copy", usage="<id_or_name> [args]", cooldown_after_parsing=True)
|
||||||
async def _playlist_copy(
|
async def _playlist_copy(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -4157,7 +4168,7 @@ class Audio(commands.Cog):
|
|||||||
"""Copy a playlist from one scope to another.
|
"""Copy a playlist from one scope to another.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist copy playlist_name_OR_id args
|
`[p]playlist copy playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4184,11 +4195,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global
|
`[p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global`
|
||||||
[p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666
|
`[p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666 --to-scope User`
|
||||||
--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 MyPersonalPlaylist --from-scope user --to-author Draper#6666
|
|
||||||
--to-scope Guild --to-guild Red - Discord Bot
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
@ -4284,8 +4293,8 @@ class Audio(commands.Cog):
|
|||||||
).format(
|
).format(
|
||||||
name=from_playlist.name,
|
name=from_playlist.name,
|
||||||
from_id=from_playlist.id,
|
from_id=from_playlist.id,
|
||||||
from_scope=humanize_scope(from_scope, ctx=from_scope_name, the=True),
|
from_scope=humanize_scope(from_scope, ctx=from_scope_name),
|
||||||
to_scope=humanize_scope(to_scope, ctx=to_scope_name, the=True),
|
to_scope=humanize_scope(to_scope, ctx=to_scope_name),
|
||||||
to_id=to_playlist.id,
|
to_id=to_playlist.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -4297,7 +4306,7 @@ class Audio(commands.Cog):
|
|||||||
"""Create an empty playlist.
|
"""Create an empty playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist create playlist_name args
|
`[p]playlist create playlist_name [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4320,9 +4329,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist create MyGuildPlaylist
|
`[p]playlist create MyGuildPlaylist`
|
||||||
[p]playlist create MyGlobalPlaylist --scope Global
|
`[p]playlist create MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist create MyPersonalPlaylist --scope User
|
`[p]playlist create MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4364,7 +4373,7 @@ class Audio(commands.Cog):
|
|||||||
"""Delete a saved playlist.
|
"""Delete a saved playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist delete playlist_name_OR_id args
|
`[p]playlist delete playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4387,9 +4396,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist delete MyGuildPlaylist
|
`[p]playlist delete MyGuildPlaylist`
|
||||||
[p]playlist delete MyGlobalPlaylist --scope Global
|
`[p]playlist delete MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist delete MyPersonalPlaylist --scope User
|
`[p]playlist delete MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4438,7 +4447,9 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@commands.cooldown(1, 30, commands.BucketType.member)
|
@commands.cooldown(1, 30, commands.BucketType.member)
|
||||||
@playlist.command(name="dedupe", usage="<playlist_name_OR_id> [args]")
|
@playlist.command(
|
||||||
|
name="dedupe", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||||
|
)
|
||||||
async def _playlist_remdupe(
|
async def _playlist_remdupe(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -4449,7 +4460,7 @@ class Audio(commands.Cog):
|
|||||||
"""Remove duplicate tracks from a saved playlist.
|
"""Remove duplicate tracks from a saved playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist dedupe playlist_name_OR_id args
|
`[p]playlist dedupe playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4472,9 +4483,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist dedupe MyGuildPlaylist
|
`[p]playlist dedupe MyGuildPlaylist`
|
||||||
[p]playlist dedupe MyGlobalPlaylist --scope Global
|
`[p]playlist dedupe MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist dedupe MyPersonalPlaylist --scope User
|
`[p]playlist dedupe MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
@ -4571,9 +4582,13 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@playlist.command(name="download", usage="<playlist_name_OR_id> [v2=False] [args]")
|
@playlist.command(
|
||||||
|
name="download",
|
||||||
|
usage="<playlist_name_OR_id> [v2=False] [args]",
|
||||||
|
cooldown_after_parsing=True,
|
||||||
|
)
|
||||||
@commands.bot_has_permissions(attach_files=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(
|
async def _playlist_download(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -4584,12 +4599,12 @@ class Audio(commands.Cog):
|
|||||||
):
|
):
|
||||||
"""Download a copy of a playlist.
|
"""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
|
Red v2-compatible playlists can be generated by passing True
|
||||||
for the v2 variable.
|
for the v2 variable.
|
||||||
|
|
||||||
**Usage**:
|
**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**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4612,9 +4627,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist download MyGuildPlaylist True
|
`[p]playlist download MyGuildPlaylist True`
|
||||||
[p]playlist download MyGlobalPlaylist False --scope Global
|
`[p]playlist download MyGlobalPlaylist False --scope Global`
|
||||||
[p]playlist download MyPersonalPlaylist --scope User
|
`[p]playlist download MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4715,8 +4730,10 @@ class Audio(commands.Cog):
|
|||||||
await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt"))
|
await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt"))
|
||||||
to_write.close()
|
to_write.close()
|
||||||
|
|
||||||
@commands.cooldown(1, 20, commands.BucketType.member)
|
@commands.cooldown(1, 10, commands.BucketType.member)
|
||||||
@playlist.command(name="info", usage="<playlist_name_OR_id> [args]")
|
@playlist.command(
|
||||||
|
name="info", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||||
|
)
|
||||||
async def _playlist_info(
|
async def _playlist_info(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -4727,7 +4744,7 @@ class Audio(commands.Cog):
|
|||||||
"""Retrieve information from a saved playlist.
|
"""Retrieve information from a saved playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist info playlist_name_OR_id args
|
`[p]playlist info playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4750,9 +4767,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist info MyGuildPlaylist
|
`[p]playlist info MyGuildPlaylist`
|
||||||
[p]playlist info MyGlobalPlaylist --scope Global
|
`[p]playlist info MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist info MyPersonalPlaylist --scope User
|
`[p]playlist info MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4852,14 +4869,14 @@ class Audio(commands.Cog):
|
|||||||
page_list.append(embed)
|
page_list.append(embed)
|
||||||
await menu(ctx, page_list, DEFAULT_CONTROLS)
|
await menu(ctx, page_list, DEFAULT_CONTROLS)
|
||||||
|
|
||||||
@commands.cooldown(1, 30, commands.BucketType.guild)
|
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||||
@playlist.command(name="list", usage="[args]")
|
@playlist.command(name="list", usage="[args]", cooldown_after_parsing=True)
|
||||||
@commands.bot_has_permissions(add_reactions=True)
|
@commands.bot_has_permissions(add_reactions=True)
|
||||||
async def _playlist_list(self, ctx: commands.Context, *, scope_data: ScopeParser = None):
|
async def _playlist_list(self, ctx: commands.Context, *, scope_data: ScopeParser = None):
|
||||||
"""List saved playlists.
|
"""List saved playlists.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist list args
|
`[p]playlist list [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -4882,9 +4899,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist list
|
`[p]playlist list`
|
||||||
[p]playlist list --scope Global
|
`[p]playlist list --scope Global`
|
||||||
[p]playlist list --scope User
|
`[p]playlist list --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -4976,15 +4993,15 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@playlist.command(name="queue", usage="<name> [args]")
|
@playlist.command(name="queue", usage="<name> [args]", cooldown_after_parsing=True)
|
||||||
@commands.cooldown(1, 600, commands.BucketType.member)
|
@commands.cooldown(1, 300, commands.BucketType.member)
|
||||||
async def _playlist_queue(
|
async def _playlist_queue(
|
||||||
self, ctx: commands.Context, playlist_name: str, *, scope_data: ScopeParser = None
|
self, ctx: commands.Context, playlist_name: str, *, scope_data: ScopeParser = None
|
||||||
):
|
):
|
||||||
"""Save the queue to a playlist.
|
"""Save the queue to a playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist queue playlist_name
|
`[p]playlist queue playlist_name [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5007,9 +5024,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist queue MyGuildPlaylist
|
`[p]playlist queue MyGuildPlaylist`
|
||||||
[p]playlist queue MyGlobalPlaylist --scope Global
|
`[p]playlist queue MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist queue MyPersonalPlaylist --scope User
|
`[p]playlist queue MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
@ -5087,7 +5104,7 @@ class Audio(commands.Cog):
|
|||||||
"""Remove a track from a playlist by url.
|
"""Remove a track from a playlist by url.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist remove playlist_name_OR_id url args
|
`[p]playlist remove playlist_name_OR_id url [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5110,11 +5127,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
`[p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU`
|
||||||
[p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
`[p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope Global`
|
||||||
--scope Global
|
`[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
|
||||||
[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
|
||||||
--scope User
|
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -5188,8 +5203,8 @@ class Audio(commands.Cog):
|
|||||||
).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name),
|
).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
@playlist.command(name="save", usage="<name> <url> [args]")
|
@playlist.command(name="save", usage="<name> <url> [args]", cooldown_after_parsing=True)
|
||||||
@commands.cooldown(1, 120, commands.BucketType.member)
|
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||||
async def _playlist_save(
|
async def _playlist_save(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -5201,7 +5216,7 @@ class Audio(commands.Cog):
|
|||||||
"""Save a playlist from a url.
|
"""Save a playlist from a url.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist save name url args
|
`[p]playlist save name url [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5224,12 +5239,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist save MyGuildPlaylist
|
`[p]playlist save MyGuildPlaylist https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM`
|
||||||
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 MyGlobalPlaylist
|
`[p]playlist save MyPersonalPlaylist https://open.spotify.com/playlist/1RyeIbyFeIJVnNzlGr5KkR --scope User`
|
||||||
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:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -5282,8 +5294,13 @@ class Audio(commands.Cog):
|
|||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
@commands.cooldown(1, 30, commands.BucketType.member)
|
||||||
@playlist.command(name="start", aliases=["play"], usage="<playlist_name_OR_id> [args]")
|
@playlist.command(
|
||||||
|
name="start",
|
||||||
|
aliases=["play"],
|
||||||
|
usage="<playlist_name_OR_id> [args]",
|
||||||
|
cooldown_after_parsing=True,
|
||||||
|
)
|
||||||
async def _playlist_start(
|
async def _playlist_start(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -5294,7 +5311,7 @@ class Audio(commands.Cog):
|
|||||||
"""Load a playlist into the queue.
|
"""Load a playlist into the queue.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist start playlist_name_OR_id args
|
` [p]playlist start playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5317,9 +5334,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist start MyGuildPlaylist
|
`[p]playlist start MyGuildPlaylist`
|
||||||
[p]playlist start MyGlobalPlaylist --scope Global
|
`[p]playlist start MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist start MyPersonalPlaylist --scope User
|
`[p]playlist start MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -5451,7 +5468,9 @@ class Audio(commands.Cog):
|
|||||||
return await ctx.invoke(self.play, query=playlist.url)
|
return await ctx.invoke(self.play, query=playlist.url)
|
||||||
|
|
||||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||||
@playlist.command(name="update", usage="<playlist_name_OR_id> [args]")
|
@playlist.command(
|
||||||
|
name="update", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||||
|
)
|
||||||
async def _playlist_update(
|
async def _playlist_update(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -5462,7 +5481,7 @@ class Audio(commands.Cog):
|
|||||||
"""Updates all tracks in a playlist.
|
"""Updates all tracks in a playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist update playlist_name_OR_id args
|
`[p]playlist update playlist_name_OR_id [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5485,9 +5504,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist update MyGuildPlaylist
|
`[p]playlist update MyGuildPlaylist`
|
||||||
[p]playlist update MyGlobalPlaylist --scope Global
|
`[p]playlist update MyGlobalPlaylist --scope Global`
|
||||||
[p]playlist update MyPersonalPlaylist --scope User
|
`[p]playlist update MyPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
@ -5610,10 +5629,10 @@ class Audio(commands.Cog):
|
|||||||
"""Uploads a playlist file as a playlist for the bot.
|
"""Uploads a playlist file as a playlist for the bot.
|
||||||
|
|
||||||
V2 and old V3 playlist will be slow.
|
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**:
|
**Usage**:
|
||||||
[p]playlist upload args
|
`[p]playlist upload [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5636,9 +5655,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist upload
|
`[p]playlist upload`
|
||||||
[p]playlist upload --scope Global
|
`[p]playlist upload --scope Global`
|
||||||
[p]playlist upload --scope User
|
`[p]playlist upload --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -5728,7 +5747,9 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||||
@playlist.command(name="rename", usage="<playlist_name_OR_id> <new_name> [args]")
|
@playlist.command(
|
||||||
|
name="rename", usage="<playlist_name_OR_id> <new_name> [args]", cooldown_after_parsing=True
|
||||||
|
)
|
||||||
async def _playlist_rename(
|
async def _playlist_rename(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@ -5740,7 +5761,7 @@ class Audio(commands.Cog):
|
|||||||
"""Rename an existing playlist.
|
"""Rename an existing playlist.
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
[p]playlist rename playlist_name_OR_id new_name args
|
`[p]playlist rename playlist_name_OR_id new_name [args]`
|
||||||
|
|
||||||
**Args**:
|
**Args**:
|
||||||
The following are all optional:
|
The following are all optional:
|
||||||
@ -5763,9 +5784,9 @@ class Audio(commands.Cog):
|
|||||||
Exact guild name
|
Exact guild name
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
[p]playlist rename MyGuildPlaylist RenamedGuildPlaylist
|
`[p]playlist rename MyGuildPlaylist RenamedGuildPlaylist`
|
||||||
[p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global
|
`[p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global`
|
||||||
[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User
|
`[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
if scope_data is None:
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
@ -5882,15 +5903,17 @@ class Audio(commands.Cog):
|
|||||||
for t in track_list:
|
for t in track_list:
|
||||||
uri = t.get("info", {}).get("uri")
|
uri = t.get("info", {}).get("uri")
|
||||||
if uri:
|
if uri:
|
||||||
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
|
t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
|
||||||
database_entries.append(
|
data = json.dumps(t)
|
||||||
{
|
if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]):
|
||||||
"query": uri,
|
database_entries.append(
|
||||||
"data": json.dumps(t),
|
{
|
||||||
"last_updated": time_now,
|
"query": uri,
|
||||||
"last_fetched": time_now,
|
"data": data,
|
||||||
}
|
"last_updated": time_now,
|
||||||
)
|
"last_fetched": time_now,
|
||||||
|
}
|
||||||
|
)
|
||||||
if database_entries:
|
if database_entries:
|
||||||
await self.music_cache.database.insert("lavalink", database_entries)
|
await self.music_cache.database.insert("lavalink", database_entries)
|
||||||
|
|
||||||
@ -6793,8 +6816,8 @@ class Audio(commands.Cog):
|
|||||||
async def search(self, ctx: commands.Context, *, query: str):
|
async def search(self, ctx: commands.Context, *, query: str):
|
||||||
"""Pick a track with a search.
|
"""Pick a track with a search.
|
||||||
|
|
||||||
Use `[p]search list <search term>` to queue all tracks found on YouTube. `[p]search sc
|
Use `[p]search list <search term>` to queue all tracks found on YouTube.
|
||||||
<search term>` will search SoundCloud instead of YouTube.
|
`[p]search sc<search term>` will search SoundCloud instead of YouTube.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def _search_menu(
|
async def _search_menu(
|
||||||
@ -7357,8 +7380,8 @@ class Audio(commands.Cog):
|
|||||||
async def _shuffle_bumpped(self, ctx: commands.Context):
|
async def _shuffle_bumpped(self, ctx: commands.Context):
|
||||||
"""Toggle bumped track shuffle.
|
"""Toggle bumped track shuffle.
|
||||||
|
|
||||||
Set this to disabled if you wish to avoid bumped songs being shuffled. This takes priority
|
Set this to disabled if you wish to avoid bumped songs being shuffled.
|
||||||
over `[p]shuffle`.
|
This takes priority over `[p]shuffle`.
|
||||||
"""
|
"""
|
||||||
dj_enabled = self._dj_status_cache.setdefault(
|
dj_enabled = self._dj_status_cache.setdefault(
|
||||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import glob
|
||||||
import ntpath
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
from pathlib import Path, PosixPath, WindowsPath
|
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
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import lavalink
|
import lavalink
|
||||||
@ -167,29 +170,48 @@ class LocalPath:
|
|||||||
modified.path = modified.path.joinpath(*args)
|
modified.path = modified.path.joinpath(*args)
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
def multiglob(self, *patterns):
|
def rglob(self, pattern, folder=False) -> Iterator[str]:
|
||||||
paths = []
|
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:
|
for p in patterns:
|
||||||
paths.extend(list(self.path.glob(p)))
|
for rp in self.glob(p):
|
||||||
for p in self._filtered(paths):
|
rp = LocalPath(rp)
|
||||||
yield p
|
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):
|
async def multirglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
|
||||||
paths = []
|
|
||||||
for p in patterns:
|
for p in patterns:
|
||||||
paths.extend(list(self.path.rglob(p)))
|
for rp in self.rglob(p):
|
||||||
|
rp = LocalPath(rp)
|
||||||
for p in self._filtered(paths):
|
if folder and rp.is_dir() and rp.exists():
|
||||||
yield p
|
yield rp
|
||||||
|
await asyncio.sleep(0)
|
||||||
def _filtered(self, paths: List[Path]):
|
else:
|
||||||
for p in paths:
|
if rp.suffix in self._all_music_ext and rp.is_file() and rp.exists():
|
||||||
if p.suffix in self._all_music_ext:
|
yield rp
|
||||||
yield p
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_string()
|
return self.to_string()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
def to_string(self):
|
def to_string(self):
|
||||||
try:
|
try:
|
||||||
return str(self.path.absolute())
|
return str(self.path.absolute())
|
||||||
@ -209,48 +231,56 @@ class LocalPath:
|
|||||||
string = f"...{os.sep}{string}"
|
string = f"...{os.sep}{string}"
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def tracks_in_tree(self):
|
async def tracks_in_tree(self):
|
||||||
tracks = []
|
tracks = []
|
||||||
for track in self.multirglob(*[f"*{ext}" for ext in self._all_music_ext]):
|
async 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:
|
with contextlib.suppress(ValueError):
|
||||||
tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
|
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())
|
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||||
|
|
||||||
def subfolders_in_tree(self):
|
async 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)
|
|
||||||
return_folders = []
|
return_folders = []
|
||||||
for folder in folders:
|
async for f in self.multirglob("", folder=True):
|
||||||
if folder.exists() and folder.is_dir():
|
with contextlib.suppress(ValueError):
|
||||||
return_folders.append(LocalPath(str(folder.absolute())))
|
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())
|
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||||
|
|
||||||
def tracks_in_folder(self):
|
async def tracks_in_folder(self):
|
||||||
tracks = []
|
tracks = []
|
||||||
for track in self.multiglob(*[f"*{ext}" for ext in self._all_music_ext]):
|
async 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:
|
with contextlib.suppress(ValueError):
|
||||||
tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
|
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())
|
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||||
|
|
||||||
def subfolders(self):
|
async 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)
|
|
||||||
return_folders = []
|
return_folders = []
|
||||||
for folder in folders:
|
async for f in self.multiglob("", folder=True):
|
||||||
if folder.exists() and folder.is_dir():
|
with contextlib.suppress(ValueError):
|
||||||
return_folders.append(LocalPath(str(folder.absolute())))
|
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())
|
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, LocalPath):
|
if isinstance(other, LocalPath):
|
||||||
return NotImplemented
|
return self.path._cparts == other.path._cparts
|
||||||
return self.path._cparts == other.path._cparts
|
elif isinstance(other, Path):
|
||||||
|
return self.path._cparts == other._cpart
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
try:
|
try:
|
||||||
@ -260,24 +290,32 @@ class LocalPath:
|
|||||||
return self._hash
|
return self._hash
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
if not isinstance(other, LocalPath):
|
if isinstance(other, LocalPath):
|
||||||
return NotImplemented
|
return self.path._cparts < other.path._cparts
|
||||||
return self.path._cparts < other.path._cparts
|
elif isinstance(other, Path):
|
||||||
|
return self.path._cparts < other._cpart
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
if not isinstance(other, LocalPath):
|
if isinstance(other, LocalPath):
|
||||||
return NotImplemented
|
return self.path._cparts <= other.path._cparts
|
||||||
return self.path._cparts <= other.path._cparts
|
elif isinstance(other, Path):
|
||||||
|
return self.path._cparts <= other._cpart
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
if not isinstance(other, LocalPath):
|
if isinstance(other, LocalPath):
|
||||||
return NotImplemented
|
return self.path._cparts > other.path._cparts
|
||||||
return self.path._cparts > other.path._cparts
|
elif isinstance(other, Path):
|
||||||
|
return self.path._cparts > other._cpart
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
if not isinstance(other, LocalPath):
|
if isinstance(other, LocalPath):
|
||||||
return NotImplemented
|
return self.path._cparts >= other.path._cparts
|
||||||
return self.path._cparts >= other.path._cparts
|
elif isinstance(other, Path):
|
||||||
|
return self.path._cparts >= other._cpart
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
class Query:
|
||||||
@ -378,6 +416,10 @@ class Query:
|
|||||||
|
|
||||||
if isinstance(query, str):
|
if isinstance(query, str):
|
||||||
query = query.strip("<>")
|
query = query.strip("<>")
|
||||||
|
while "ytsearch:" in query:
|
||||||
|
query = query.replace("ytsearch:", "")
|
||||||
|
while "scsearch:" in query:
|
||||||
|
query = query.replace("scsearch:", "")
|
||||||
|
|
||||||
elif isinstance(query, Query):
|
elif isinstance(query, Query):
|
||||||
for key, val in kwargs.items():
|
for key, val in kwargs.items():
|
||||||
|
|||||||
@ -43,6 +43,7 @@ __all__ = [
|
|||||||
"CacheLevel",
|
"CacheLevel",
|
||||||
"format_playlist_picker_data",
|
"format_playlist_picker_data",
|
||||||
"get_track_description_unformatted",
|
"get_track_description_unformatted",
|
||||||
|
"track_remaining_duration",
|
||||||
"Notifier",
|
"Notifier",
|
||||||
"PlaylistScope",
|
"PlaylistScope",
|
||||||
]
|
]
|
||||||
@ -126,6 +127,20 @@ async def queue_duration(ctx) -> int:
|
|||||||
return queue_total_duration
|
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:
|
async def draw_time(ctx) -> str:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
paused = player.paused
|
paused = player.paused
|
||||||
@ -213,7 +228,7 @@ async def clear_react(bot: Red, message: discord.Message, emoji: MutableMapping
|
|||||||
def get_track_description(track) -> Optional[str]:
|
def get_track_description(track) -> Optional[str]:
|
||||||
if track and getattr(track, "uri", None):
|
if track and getattr(track, "uri", None):
|
||||||
query = Query.process_input(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":
|
if track.title != "Unknown title":
|
||||||
return f'**{escape(f"{track.author} - {track.title}")}**' + escape(
|
return f'**{escape(f"{track.author} - {track.title}")}**' + escape(
|
||||||
f"\n{query.to_string_user()} "
|
f"\n{query.to_string_user()} "
|
||||||
@ -229,7 +244,7 @@ def get_track_description(track) -> Optional[str]:
|
|||||||
def get_track_description_unformatted(track) -> Optional[str]:
|
def get_track_description_unformatted(track) -> Optional[str]:
|
||||||
if track and hasattr(track, "uri"):
|
if track and hasattr(track, "uri"):
|
||||||
query = Query.process_input(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":
|
if track.title != "Unknown title":
|
||||||
return escape(f"{track.author} - {track.title}")
|
return escape(f"{track.author} - {track.title}")
|
||||||
else:
|
else:
|
||||||
@ -521,8 +536,8 @@ class PlaylistScope(Enum):
|
|||||||
def humanize_scope(scope, ctx=None, the=None):
|
def humanize_scope(scope, ctx=None, the=None):
|
||||||
|
|
||||||
if scope == PlaylistScope.GLOBAL.value:
|
if scope == PlaylistScope.GLOBAL.value:
|
||||||
return _("the ") if the else "" + _("Global")
|
return (_("the ") if the else "") + _("Global")
|
||||||
elif scope == PlaylistScope.GUILD.value:
|
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:
|
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")
|
||||||
|
|||||||
@ -418,6 +418,11 @@ class Downloader(commands.Cog):
|
|||||||
elif target.is_file():
|
elif target.is_file():
|
||||||
os.remove(str(target))
|
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()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def pipinstall(self, ctx: commands.Context, *deps: str) -> None:
|
async def pipinstall(self, ctx: commands.Context, *deps: str) -> None:
|
||||||
@ -550,7 +555,7 @@ class Downloader(commands.Cog):
|
|||||||
if failed:
|
if failed:
|
||||||
message += "\n" + self.format_failed_repos(failed)
|
message += "\n" + self.format_failed_repos(failed)
|
||||||
|
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -596,12 +601,13 @@ class Downloader(commands.Cog):
|
|||||||
tuple(map(inline, libnames))
|
tuple(map(inline, libnames))
|
||||||
)
|
)
|
||||||
if message:
|
if message:
|
||||||
await ctx.send(
|
await self.send_pagified(
|
||||||
|
ctx,
|
||||||
_(
|
_(
|
||||||
"Cog requirements and shared libraries for all installed cogs"
|
"Cog requirements and shared libraries for all installed cogs"
|
||||||
" have been reinstalled but there were some errors:\n"
|
" have been reinstalled but there were some errors:\n"
|
||||||
)
|
)
|
||||||
+ message
|
+ message,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@ -643,8 +649,7 @@ class Downloader(commands.Cog):
|
|||||||
f"**{candidate.object_type} {candidate.rev}**"
|
f"**{candidate.object_type} {candidate.rev}**"
|
||||||
f" - {candidate.description}\n"
|
f" - {candidate.description}\n"
|
||||||
)
|
)
|
||||||
for page in pagify(msg):
|
await self.send_pagified(ctx, msg)
|
||||||
await ctx.send(msg)
|
|
||||||
return
|
return
|
||||||
except errors.UnknownRevision:
|
except errors.UnknownRevision:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@ -658,14 +663,14 @@ class Downloader(commands.Cog):
|
|||||||
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||||
cogs, message = await self._filter_incorrect_cogs_by_names(repo, cog_names)
|
cogs, message = await self._filter_incorrect_cogs_by_names(repo, cog_names)
|
||||||
if not cogs:
|
if not cogs:
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
return
|
return
|
||||||
failed_reqs = await self._install_requirements(cogs)
|
failed_reqs = await self._install_requirements(cogs)
|
||||||
if failed_reqs:
|
if failed_reqs:
|
||||||
message += _("\nFailed to install requirements: ") + humanize_list(
|
message += _("\nFailed to install requirements: ") + humanize_list(
|
||||||
tuple(map(inline, failed_reqs))
|
tuple(map(inline, failed_reqs))
|
||||||
)
|
)
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
installed_cogs, failed_cogs = await self._install_cogs(cogs)
|
installed_cogs, failed_cogs = await self._install_cogs(cogs)
|
||||||
@ -711,7 +716,7 @@ class Downloader(commands.Cog):
|
|||||||
+ message
|
+ message
|
||||||
)
|
)
|
||||||
# "---" added to separate cog install messages from Downloader's 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:
|
for cog in installed_cogs:
|
||||||
if cog.install_msg:
|
if cog.install_msg:
|
||||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||||
@ -748,14 +753,18 @@ class Downloader(commands.Cog):
|
|||||||
message += _("Successfully uninstalled cogs: ") + humanize_list(uninstalled_cogs)
|
message += _("Successfully uninstalled cogs: ") + humanize_list(uninstalled_cogs)
|
||||||
if failed_cogs:
|
if failed_cogs:
|
||||||
message += (
|
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)))
|
+ humanize_list(tuple(map(inline, failed_cogs)))
|
||||||
+ _(
|
+ _(
|
||||||
"\nYou may need to remove their files manually if they are still usable."
|
"\nThey were most likely removed without using `{prefix}cog uninstall`.\n"
|
||||||
" Also make sure you've unloaded those cogs with `{prefix}unload {cogs}`."
|
"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))
|
).format(prefix=ctx.prefix, cogs=" ".join(failed_cogs))
|
||||||
)
|
)
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
|
|
||||||
@cog.command(name="pin", usage="<cogs>")
|
@cog.command(name="pin", usage="<cogs>")
|
||||||
async def _cog_pin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
async def _cog_pin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||||
@ -778,7 +787,7 @@ class Downloader(commands.Cog):
|
|||||||
message += _("Pinned cogs: ") + humanize_list(cognames)
|
message += _("Pinned cogs: ") + humanize_list(cognames)
|
||||||
if already_pinned:
|
if already_pinned:
|
||||||
message += _("\nThese cogs were already pinned: ") + humanize_list(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="<cogs>")
|
@cog.command(name="unpin", usage="<cogs>")
|
||||||
async def _cog_unpin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
async def _cog_unpin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||||
@ -801,7 +810,7 @@ class Downloader(commands.Cog):
|
|||||||
message += _("Unpinned cogs: ") + humanize_list(cognames)
|
message += _("Unpinned cogs: ") + humanize_list(cognames)
|
||||||
if not_pinned:
|
if not_pinned:
|
||||||
message += _("\nThese cogs weren't pinned: ") + humanize_list(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")
|
@cog.command(name="checkforupdates")
|
||||||
async def _cog_checkforupdates(self, ctx: commands.Context) -> None:
|
async def _cog_checkforupdates(self, ctx: commands.Context) -> None:
|
||||||
@ -833,7 +842,7 @@ class Downloader(commands.Cog):
|
|||||||
if failed:
|
if failed:
|
||||||
message += "\n" + self.format_failed_repos(failed)
|
message += "\n" + self.format_failed_repos(failed)
|
||||||
|
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
|
|
||||||
@cog.command(name="update")
|
@cog.command(name="update")
|
||||||
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||||
@ -869,7 +878,6 @@ class Downloader(commands.Cog):
|
|||||||
rev: Optional[str] = None,
|
rev: Optional[str] = None,
|
||||||
cogs: Optional[List[InstalledModule]] = None,
|
cogs: Optional[List[InstalledModule]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
message = ""
|
|
||||||
failed_repos = set()
|
failed_repos = set()
|
||||||
updates_available = set()
|
updates_available = set()
|
||||||
|
|
||||||
@ -882,7 +890,7 @@ class Downloader(commands.Cog):
|
|||||||
await repo.update()
|
await repo.update()
|
||||||
except errors.UpdateError:
|
except errors.UpdateError:
|
||||||
message = self.format_failed_repos([repo.name])
|
message = self.format_failed_repos([repo.name])
|
||||||
await ctx.send(message)
|
await self.send_pagified(ctx, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -896,11 +904,10 @@ class Downloader(commands.Cog):
|
|||||||
f"**{candidate.object_type} {candidate.rev}**"
|
f"**{candidate.object_type} {candidate.rev}**"
|
||||||
f" - {candidate.description}\n"
|
f" - {candidate.description}\n"
|
||||||
)
|
)
|
||||||
for page in pagify(msg):
|
await self.send_pagified(ctx, msg)
|
||||||
await ctx.send(msg)
|
|
||||||
return
|
return
|
||||||
except errors.UnknownRevision:
|
except errors.UnknownRevision:
|
||||||
message += _(
|
message = _(
|
||||||
"Error: there is no revision `{rev}` in repo `{repo.name}`"
|
"Error: there is no revision `{rev}` in repo `{repo.name}`"
|
||||||
).format(rev=rev, repo=repo)
|
).format(rev=rev, repo=repo)
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
@ -917,6 +924,8 @@ class Downloader(commands.Cog):
|
|||||||
|
|
||||||
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
||||||
cogs_to_check -= pinned_cogs
|
cogs_to_check -= pinned_cogs
|
||||||
|
|
||||||
|
message = ""
|
||||||
if not cogs_to_check:
|
if not cogs_to_check:
|
||||||
cogs_to_update = libs_to_update = ()
|
cogs_to_update = libs_to_update = ()
|
||||||
message += _("There were no cogs to check.")
|
message += _("There were no cogs to check.")
|
||||||
@ -972,7 +981,7 @@ class Downloader(commands.Cog):
|
|||||||
if repos_with_libs:
|
if repos_with_libs:
|
||||||
message += DEPRECATION_NOTICE.format(repo_list=humanize_list(list(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:
|
if updates_available and updated_cognames:
|
||||||
await self._ask_for_cog_reload(ctx, updated_cognames)
|
await self._ask_for_cog_reload(ctx, updated_cognames)
|
||||||
|
|||||||
@ -38,6 +38,10 @@ class GitException(DownloaderException):
|
|||||||
Generic class for git exceptions.
|
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):
|
class InvalidRepoName(DownloaderException):
|
||||||
"""
|
"""
|
||||||
@ -138,8 +142,8 @@ class AmbiguousRevision(GitException):
|
|||||||
Thrown when specified revision is ambiguous.
|
Thrown when specified revision is ambiguous.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message: str, candidates: List[Candidate]) -> None:
|
def __init__(self, message: str, git_command: str, candidates: List[Candidate]) -> None:
|
||||||
super().__init__(message)
|
super().__init__(message, git_command)
|
||||||
self.candidates = candidates
|
self.candidates = candidates
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import distutils.dir_util
|
import functools
|
||||||
import shutil
|
import shutil
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -127,15 +127,13 @@ class Installable(RepoJSONMixin):
|
|||||||
if self._location.is_file():
|
if self._location.is_file():
|
||||||
copy_func = shutil.copy2
|
copy_func = shutil.copy2
|
||||||
else:
|
else:
|
||||||
# clear copy_tree's cache to make sure missing directories are created (GH-2690)
|
copy_func = functools.partial(shutil.copytree, dirs_exist_ok=True)
|
||||||
distutils.dir_util._path_created = {}
|
|
||||||
copy_func = distutils.dir_util.copy_tree
|
|
||||||
|
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||||
except: # noqa: E722
|
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 False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -203,21 +203,20 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
valid_exit_codes = (0, 1)
|
valid_exit_codes = (0, 1)
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_IS_ANCESTOR,
|
||||||
self.GIT_IS_ANCESTOR,
|
path=self.folder_path,
|
||||||
path=self.folder_path,
|
maybe_ancestor_rev=maybe_ancestor_rev,
|
||||||
maybe_ancestor_rev=maybe_ancestor_rev,
|
descendant_rev=descendant_rev,
|
||||||
descendant_rev=descendant_rev,
|
|
||||||
),
|
|
||||||
valid_exit_codes=valid_exit_codes,
|
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command, valid_exit_codes=valid_exit_codes)
|
||||||
|
|
||||||
if p.returncode in valid_exit_codes:
|
if p.returncode in valid_exit_codes:
|
||||||
return not bool(p.returncode)
|
return not bool(p.returncode)
|
||||||
raise errors.GitException(
|
raise errors.GitException(
|
||||||
f"Git failed to determine if commit {maybe_ancestor_rev}"
|
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:
|
async def is_on_branch(self) -> bool:
|
||||||
@ -253,15 +252,14 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
if new_rev is None:
|
if new_rev is None:
|
||||||
new_rev = self.branch
|
new_rev = self.branch
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_DIFF_FILE_STATUS, path=self.folder_path, old_rev=old_rev, new_rev=new_rev
|
||||||
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:
|
if p.returncode != 0:
|
||||||
raise errors.GitDiffError(
|
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")
|
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):
|
async with self.checkout(descendant_rev):
|
||||||
return discord.utils.get(self.available_modules, name=module_name)
|
return discord.utils.get(self.available_modules, name=module_name)
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
|
||||||
self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
|
path=self.folder_path,
|
||||||
path=self.folder_path,
|
descendant_rev=descendant_rev,
|
||||||
descendant_rev=descendant_rev,
|
module_name=module_name,
|
||||||
module_name=module_name,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.GitException(
|
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()
|
commit = p.stdout.decode().strip()
|
||||||
@ -418,19 +415,18 @@ class Repo(RepoJSONMixin):
|
|||||||
to get messages for.
|
to get messages for.
|
||||||
:return: Git commit note log
|
:return: Git commit note log
|
||||||
"""
|
"""
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_LOG,
|
||||||
self.GIT_LOG,
|
path=self.folder_path,
|
||||||
path=self.folder_path,
|
old_rev=old_rev,
|
||||||
old_rev=old_rev,
|
relative_file_path=relative_file_path,
|
||||||
relative_file_path=relative_file_path,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.GitException(
|
raise errors.GitException(
|
||||||
"An exception occurred while executing git log on"
|
f"An exception occurred while executing git log on this repo: {self.folder_path}",
|
||||||
" this repo: {}".format(self.folder_path)
|
git_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
@ -457,21 +453,24 @@ class Repo(RepoJSONMixin):
|
|||||||
Full sha1 object name for provided revision.
|
Full sha1 object name for provided revision.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev)
|
self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
stderr = p.stderr.decode().strip()
|
stderr = p.stderr.decode().strip()
|
||||||
ambiguous_error = f"error: short SHA1 {rev} is ambiguous\nhint: The candidates are:\n"
|
ambiguous_error = f"error: short SHA1 {rev} is ambiguous\nhint: The candidates are:\n"
|
||||||
if not stderr.startswith(ambiguous_error):
|
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 = []
|
candidates = []
|
||||||
for match in self.AMBIGUOUS_ERROR_REGEX.finditer(stderr, len(ambiguous_error)):
|
for match in self.AMBIGUOUS_ERROR_REGEX.finditer(stderr, len(ambiguous_error)):
|
||||||
candidates.append(Candidate(match["rev"], match["type"], match["desc"]))
|
candidates.append(Candidate(match["rev"], match["type"], match["desc"]))
|
||||||
if candidates:
|
if candidates:
|
||||||
raise errors.AmbiguousRevision(f"Short SHA1 {rev} is ambiguous.", candidates)
|
raise errors.AmbiguousRevision(
|
||||||
raise errors.UnknownRevision(f"Revision {rev} cannot be found.")
|
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()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@ -554,17 +553,14 @@ class Repo(RepoJSONMixin):
|
|||||||
return
|
return
|
||||||
exists, __ = self._existing_git_repo()
|
exists, __ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise errors.MissingGitRepo(
|
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
|
||||||
ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
|
p = await self._run(git_command)
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.UnknownRevision(
|
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()
|
await self._setup_repo()
|
||||||
@ -619,25 +615,22 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, path = self._existing_git_repo()
|
exists, path = self._existing_git_repo()
|
||||||
if exists:
|
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:
|
if self.branch is not None:
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
|
||||||
self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(
|
self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
|
||||||
self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
# Try cleaning up folder
|
# Try cleaning up folder
|
||||||
shutil.rmtree(str(self.folder_path), ignore_errors=True)
|
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:
|
if self.branch is None:
|
||||||
self.branch = await self.current_branch()
|
self.branch = await self.current_branch()
|
||||||
@ -657,17 +650,14 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, __ = self._existing_git_repo()
|
exists, __ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise errors.MissingGitRepo(
|
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
|
||||||
ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
|
p = await self._run(git_command)
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.GitException(
|
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()
|
return p.stdout.decode().strip()
|
||||||
@ -683,16 +673,13 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, __ = self._existing_git_repo()
|
exists, __ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise errors.MissingGitRepo(
|
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
|
||||||
ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
|
p = await self._run(git_command)
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
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()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@ -715,16 +702,15 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, __ = self._existing_git_repo()
|
exists, __ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise errors.MissingGitRepo(
|
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch)
|
self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode != 0:
|
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()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@ -751,10 +737,11 @@ class Repo(RepoJSONMixin):
|
|||||||
if folder is None:
|
if folder is None:
|
||||||
folder = self.folder_path
|
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:
|
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()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@ -773,19 +760,18 @@ class Repo(RepoJSONMixin):
|
|||||||
await self.checkout(branch)
|
await self.checkout(branch)
|
||||||
exists, __ = self._existing_git_repo()
|
exists, __ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise errors.MissingGitRepo(
|
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
git_command = ProcessFormatter().format(
|
||||||
ProcessFormatter().format(self.GIT_HARD_RESET, path=self.folder_path, branch=branch)
|
self.GIT_HARD_RESET, path=self.folder_path, branch=branch
|
||||||
)
|
)
|
||||||
|
p = await self._run(git_command)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.HardResetError(
|
raise errors.HardResetError(
|
||||||
"Some error occurred when trying to"
|
"Some error occurred when trying to execute a hard reset on the repo at"
|
||||||
" execute a hard reset on the repo at"
|
f" the following path: {self.folder_path}",
|
||||||
" the following path: {}".format(self.folder_path)
|
git_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update(self) -> Tuple[str, str]:
|
async def update(self) -> Tuple[str, str]:
|
||||||
@ -804,12 +790,14 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
await self.hard_reset()
|
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:
|
if p.returncode != 0:
|
||||||
raise errors.UpdateError(
|
raise errors.UpdateError(
|
||||||
"Git pull returned a non zero exit code"
|
"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()
|
await self._setup_repo()
|
||||||
@ -1114,7 +1102,7 @@ class RepoManager:
|
|||||||
"""
|
"""
|
||||||
repo = self.get_repo(name)
|
repo = self.get_repo(name)
|
||||||
if repo is None:
|
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)
|
safe_delete(repo.folder_path)
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,13 @@ class ModLog(commands.Cog):
|
|||||||
"""Manage modlog settings."""
|
"""Manage modlog settings."""
|
||||||
pass
|
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()
|
@modlogset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||||
|
|||||||
@ -12,7 +12,6 @@ from redbot.cogs.warnings.helpers import (
|
|||||||
from redbot.core import Config, checks, commands, modlog
|
from redbot.core import Config, checks, commands, modlog
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.mod import is_admin_or_superior
|
|
||||||
from redbot.core.utils.chat_formatting import warning, pagify
|
from redbot.core.utils.chat_formatting import warning, pagify
|
||||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
|
||||||
@ -342,30 +341,16 @@ class Warnings(commands.Cog):
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def warnings(
|
@checks.admin()
|
||||||
self, ctx: commands.Context, user: Optional[Union[discord.Member, int]] = None
|
async def warnings(self, ctx: commands.Context, user: Union[discord.Member, int]):
|
||||||
):
|
"""List the warnings for the specified user."""
|
||||||
"""List the warnings for the specified user.
|
|
||||||
|
|
||||||
Omit `<user>` to see your own warnings.
|
try:
|
||||||
|
userid: int = user.id
|
||||||
Note that showing warnings for users other than yourself requires
|
except AttributeError:
|
||||||
appropriate permissions.
|
userid: int = user
|
||||||
"""
|
user = ctx.guild.get_member(userid)
|
||||||
if user is None:
|
user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
|
||||||
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)
|
|
||||||
|
|
||||||
msg = ""
|
msg = ""
|
||||||
member_settings = self.config.member(user)
|
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)
|
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.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
|
|||||||
@ -838,9 +838,9 @@ async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
|
|||||||
amount = int(amount)
|
amount = int(amount)
|
||||||
max_bal = await get_max_balance(guild)
|
max_bal = await get_max_balance(guild)
|
||||||
|
|
||||||
if not (0 < amount <= max_bal):
|
if not (0 <= amount <= max_bal):
|
||||||
raise ValueError(
|
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")
|
max=humanize_number(max_bal, override_locale="en_US")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,7 +10,19 @@ from datetime import datetime
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from importlib.machinery import ModuleSpec
|
from importlib.machinery import ModuleSpec
|
||||||
from pathlib import Path
|
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
|
from types import MappingProxyType
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@ -24,6 +36,8 @@ from .dev_commands import Dev
|
|||||||
from .events import init_events
|
from .events import init_events
|
||||||
from .global_checks import init_global_checks
|
from .global_checks import init_global_checks
|
||||||
|
|
||||||
|
from .settings_caches import PrefixManager
|
||||||
|
|
||||||
from .rpc import RPCMixin
|
from .rpc import RPCMixin
|
||||||
from .utils import common_filters
|
from .utils import common_filters
|
||||||
|
|
||||||
@ -36,6 +50,9 @@ __all__ = ["RedBase", "Red", "ExitCodes"]
|
|||||||
|
|
||||||
NotMessage = namedtuple("NotMessage", "guild")
|
NotMessage = namedtuple("NotMessage", "guild")
|
||||||
|
|
||||||
|
PreInvokeCoroutine = Callable[[commands.Context], Awaitable[Any]]
|
||||||
|
T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
|
||||||
|
|
||||||
|
|
||||||
def _is_submodule(parent, child):
|
def _is_submodule(parent, child):
|
||||||
return parent == child or child.startswith(parent + ".")
|
return parent == child or child.startswith(parent + ".")
|
||||||
@ -76,6 +93,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
help__verify_checks=True,
|
help__verify_checks=True,
|
||||||
help__verify_exists=False,
|
help__verify_exists=False,
|
||||||
help__tagline="",
|
help__tagline="",
|
||||||
|
description="Red V3",
|
||||||
invite_public=False,
|
invite_public=False,
|
||||||
invite_perm=0,
|
invite_perm=0,
|
||||||
disabled_commands=[],
|
disabled_commands=[],
|
||||||
@ -108,23 +126,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
self._config.init_custom(SHARED_API_TOKENS, 2)
|
self._config.init_custom(SHARED_API_TOKENS, 2)
|
||||||
self._config.register_custom(SHARED_API_TOKENS)
|
self._config.register_custom(SHARED_API_TOKENS)
|
||||||
|
self._prefix_cache = PrefixManager(self._config, cli_flags)
|
||||||
|
|
||||||
async def prefix_manager(bot, message):
|
async def prefix_manager(bot, message) -> List[str]:
|
||||||
if not cli_flags.prefix:
|
prefixes = await self._prefix_cache.get_prefixes(message.guild)
|
||||||
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()
|
|
||||||
if cli_flags.mentionable:
|
if cli_flags.mentionable:
|
||||||
return (
|
return when_mentioned_or(*prefixes)(bot, message)
|
||||||
when_mentioned_or(*server_prefix)(bot, message)
|
return prefixes
|
||||||
if server_prefix
|
|
||||||
else when_mentioned_or(*global_prefix)(bot, message)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return server_prefix if server_prefix else global_prefix
|
|
||||||
|
|
||||||
if "command_prefix" not in kwargs:
|
if "command_prefix" not in kwargs:
|
||||||
kwargs["command_prefix"] = prefix_manager
|
kwargs["command_prefix"] = prefix_manager
|
||||||
@ -149,6 +157,64 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
self._permissions_hooks: List[commands.CheckPredicate] = []
|
self._permissions_hooks: List[commands.CheckPredicate] = []
|
||||||
self._red_ready = asyncio.Event()
|
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
|
@property
|
||||||
def cog_mgr(self) -> NoReturn:
|
def cog_mgr(self) -> NoReturn:
|
||||||
@ -400,6 +466,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
This should only be run once, prior to connecting to discord.
|
This should only be run once, prior to connecting to discord.
|
||||||
"""
|
"""
|
||||||
await self._maybe_update_config()
|
await self._maybe_update_config()
|
||||||
|
self.description = await self._config.description()
|
||||||
|
|
||||||
init_global_checks(self)
|
init_global_checks(self)
|
||||||
init_events(self, cli_flags)
|
init_events(self, cli_flags)
|
||||||
@ -547,9 +614,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
bool
|
bool
|
||||||
:code:`True` if an embed is requested
|
:code:`True` if an embed is requested
|
||||||
"""
|
"""
|
||||||
if isinstance(channel, discord.abc.PrivateChannel) or (
|
if isinstance(channel, discord.abc.PrivateChannel):
|
||||||
command and command == self.get_command("help")
|
|
||||||
):
|
|
||||||
user_setting = await self._config.user(user).embeds()
|
user_setting = await self._config.user(user).embeds()
|
||||||
if user_setting is not None:
|
if user_setting is not None:
|
||||||
return user_setting
|
return user_setting
|
||||||
@ -557,6 +622,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
guild_setting = await self._config.guild(channel.guild).embeds()
|
guild_setting = await self._config.guild(channel.guild).embeds()
|
||||||
if guild_setting is not None:
|
if guild_setting is not None:
|
||||||
return guild_setting
|
return guild_setting
|
||||||
|
|
||||||
global_setting = await self._config.embeds()
|
global_setting = await self._config.embeds()
|
||||||
return global_setting
|
return global_setting
|
||||||
|
|
||||||
|
|||||||
@ -135,7 +135,9 @@ def parse_cli_flags(args):
|
|||||||
"security implications if misused. Can be "
|
"security implications if misused. Can be "
|
||||||
"multiple.",
|
"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(
|
parser.add_argument(
|
||||||
"--no-prompt",
|
"--no-prompt",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ This module contains extended classes and functions which are intended to
|
|||||||
replace those from the `discord.ext.commands` module.
|
replace those from the `discord.ext.commands` module.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
||||||
|
|
||||||
@ -57,6 +58,49 @@ class CogCommandMixin:
|
|||||||
checks=getattr(decorated, "__requires_checks__", []),
|
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:
|
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
|
||||||
"""Actively allow this command for the given model.
|
"""Actively allow this command for the given model.
|
||||||
|
|
||||||
|
|||||||
@ -162,10 +162,10 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_default_tagline(ctx: Context):
|
def get_default_tagline(ctx: Context):
|
||||||
return (
|
return T_(
|
||||||
f"Type {ctx.clean_prefix}help <command> for more info on a command. "
|
"Type {ctx.clean_prefix}help <command> for more info on a command. "
|
||||||
f"You can also type {ctx.clean_prefix}help <category> for more info on a category."
|
"You can also type {ctx.clean_prefix}help <category> for more info on a category."
|
||||||
)
|
).format(ctx=ctx)
|
||||||
|
|
||||||
async def format_command_help(self, ctx: Context, obj: commands.Command):
|
async def format_command_help(self, ctx: Context, obj: commands.Command):
|
||||||
|
|
||||||
@ -187,7 +187,9 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
description = command.description or ""
|
description = command.description or ""
|
||||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
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
|
subcommands = None
|
||||||
|
|
||||||
if hasattr(command, "all_commands"):
|
if hasattr(command, "all_commands"):
|
||||||
@ -198,18 +200,19 @@ class RedHelpFormatter:
|
|||||||
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
|
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
emb["embed"]["title"] = f"*{description[:250]}*"
|
||||||
|
|
||||||
emb["footer"]["text"] = tagline
|
emb["footer"]["text"] = tagline
|
||||||
emb["embed"]["description"] = signature
|
emb["embed"]["description"] = signature
|
||||||
|
|
||||||
if command.help:
|
command_help = command.format_help_for_context(ctx)
|
||||||
splitted = command.help.split("\n\n")
|
if command_help:
|
||||||
|
splitted = command_help.split("\n\n")
|
||||||
name = splitted[0]
|
name = splitted[0]
|
||||||
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
value = "\n\n".join(splitted[1:])
|
||||||
if not value:
|
if not value:
|
||||||
value = EMPTY_STRING
|
value = EMPTY_STRING
|
||||||
field = EmbedField(name[:252], value[:1024], False)
|
field = EmbedField(name[:250], value[:1024], False)
|
||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
if subcommands:
|
if subcommands:
|
||||||
@ -225,9 +228,9 @@ class RedHelpFormatter:
|
|||||||
)
|
)
|
||||||
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
|
for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
title = "**__Subcommands:__**"
|
title = T_("**__Subcommands:__**")
|
||||||
else:
|
else:
|
||||||
title = "**__Subcommands:__** (continued)"
|
title = T_("**__Subcommands:__** (continued)")
|
||||||
field = EmbedField(title, page, False)
|
field = EmbedField(title, page, False)
|
||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
@ -238,7 +241,7 @@ class RedHelpFormatter:
|
|||||||
subtext = None
|
subtext = None
|
||||||
subtext_header = None
|
subtext_header = None
|
||||||
if subcommands:
|
if subcommands:
|
||||||
subtext_header = "Subcommands:"
|
subtext_header = T_("Subcommands:")
|
||||||
max_width = max(discord.utils._string_width(name) for name in subcommands.keys())
|
max_width = max(discord.utils._string_width(name) for name in subcommands.keys())
|
||||||
|
|
||||||
def width_maker(cmds):
|
def width_maker(cmds):
|
||||||
@ -261,7 +264,7 @@ class RedHelpFormatter:
|
|||||||
(
|
(
|
||||||
description,
|
description,
|
||||||
signature[1:-1],
|
signature[1:-1],
|
||||||
command.help.replace("[p]", ctx.clean_prefix),
|
command.format_help_for_context(ctx),
|
||||||
subtext_header,
|
subtext_header,
|
||||||
subtext,
|
subtext,
|
||||||
),
|
),
|
||||||
@ -301,7 +304,10 @@ class RedHelpFormatter:
|
|||||||
page_char_limit = await ctx.bot._config.help.page_char_limit()
|
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...
|
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
|
# Offset calculation here is for total embed size limit
|
||||||
# 20 accounts for# *Page {i} of {page_count}*
|
# 20 accounts for# *Page {i} of {page_count}*
|
||||||
@ -346,7 +352,9 @@ class RedHelpFormatter:
|
|||||||
embed = discord.Embed(color=color, **embed_dict["embed"])
|
embed = discord.Embed(color=color, **embed_dict["embed"])
|
||||||
|
|
||||||
if page_count > 1:
|
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.description = description
|
||||||
|
|
||||||
embed.set_author(**author_info)
|
embed.set_author(**author_info)
|
||||||
@ -366,7 +374,7 @@ class RedHelpFormatter:
|
|||||||
if not (coms or await ctx.bot._config.help.verify_exists()):
|
if not (coms or await ctx.bot._config.help.verify_exists()):
|
||||||
return
|
return
|
||||||
|
|
||||||
description = obj.help
|
description = obj.format_help_for_context(ctx)
|
||||||
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
|
|
||||||
if await ctx.embed_requested():
|
if await ctx.embed_requested():
|
||||||
@ -376,7 +384,7 @@ class RedHelpFormatter:
|
|||||||
if description:
|
if description:
|
||||||
splitted = description.split("\n\n")
|
splitted = description.split("\n\n")
|
||||||
name = splitted[0]
|
name = splitted[0]
|
||||||
value = "\n\n".join(splitted[1:]).replace("[p]", ctx.clean_prefix)
|
value = "\n\n".join(splitted[1:])
|
||||||
if not value:
|
if not value:
|
||||||
value = EMPTY_STRING
|
value = EMPTY_STRING
|
||||||
field = EmbedField(name[:252], value[:1024], False)
|
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)):
|
for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
title = "**__Commands:__**"
|
title = T_("**__Commands:__**")
|
||||||
else:
|
else:
|
||||||
title = "**__Commands:__** (continued)"
|
title = T_("**__Commands:__** (continued)")
|
||||||
field = EmbedField(title, page, False)
|
field = EmbedField(title, page, False)
|
||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
@ -407,7 +415,7 @@ class RedHelpFormatter:
|
|||||||
subtext = None
|
subtext = None
|
||||||
subtext_header = None
|
subtext_header = None
|
||||||
if coms:
|
if coms:
|
||||||
subtext_header = "Commands:"
|
subtext_header = T_("Commands:")
|
||||||
max_width = max(discord.utils._string_width(name) for name in coms.keys())
|
max_width = max(discord.utils._string_width(name) for name in coms.keys())
|
||||||
|
|
||||||
def width_maker(cmds):
|
def width_maker(cmds):
|
||||||
@ -442,14 +450,14 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
emb["footer"]["text"] = tagline
|
emb["footer"]["text"] = tagline
|
||||||
if description:
|
if description:
|
||||||
emb["embed"]["title"] = f"*{description[:2044]}*"
|
emb["embed"]["title"] = f"*{description[:250]}*"
|
||||||
|
|
||||||
for cog_name, data in coms:
|
for cog_name, data in coms:
|
||||||
|
|
||||||
if cog_name:
|
if cog_name:
|
||||||
title = f"**__{cog_name}:__**"
|
title = f"**__{cog_name}:__**"
|
||||||
else:
|
else:
|
||||||
title = f"**__No Category:__**"
|
title = f"**__{T_('No Category')}:__**"
|
||||||
|
|
||||||
def shorten_line(a_line: str) -> str:
|
def shorten_line(a_line: str) -> str:
|
||||||
if len(a_line) < 70: # embed max width needs to be lower
|
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)):
|
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)
|
field = EmbedField(title, page, False)
|
||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
@ -478,7 +486,7 @@ class RedHelpFormatter:
|
|||||||
names.extend(list(v.name for v in v.values()))
|
names.extend(list(v.name for v in v.values()))
|
||||||
|
|
||||||
max_width = max(
|
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):
|
def width_maker(cmds):
|
||||||
@ -492,7 +500,7 @@ class RedHelpFormatter:
|
|||||||
|
|
||||||
for cog_name, data in coms:
|
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)
|
to_join.append(title)
|
||||||
|
|
||||||
for name, doc, width in width_maker(sorted(data.items())):
|
for name, doc, width in width_maker(sorted(data.items())):
|
||||||
@ -543,7 +551,9 @@ class RedHelpFormatter:
|
|||||||
if fuzzy_commands:
|
if fuzzy_commands:
|
||||||
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
||||||
if 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)
|
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
ret.set_footer(text=tagline)
|
ret.set_footer(text=tagline)
|
||||||
await ctx.send(embed=ret)
|
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)
|
ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for)
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
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)
|
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
ret.set_footer(text=tagline)
|
ret.set_footer(text=tagline)
|
||||||
await ctx.send(embed=ret)
|
await ctx.send(embed=ret)
|
||||||
@ -569,7 +581,9 @@ class RedHelpFormatter:
|
|||||||
)
|
)
|
||||||
if await ctx.embed_requested():
|
if await ctx.embed_requested():
|
||||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
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)
|
tagline = (await ctx.bot._config.help.tagline()) or self.get_default_tagline(ctx)
|
||||||
ret.set_footer(text=tagline)
|
ret.set_footer(text=tagline)
|
||||||
await ctx.send(embed=ret)
|
await ctx.send(embed=ret)
|
||||||
|
|||||||
@ -95,8 +95,8 @@ class PrivilegeLevel(enum.IntEnum):
|
|||||||
"""Enumeration for special privileges."""
|
"""Enumeration for special privileges."""
|
||||||
|
|
||||||
# Maintainer Note: do NOT re-order these.
|
# Maintainer Note: do NOT re-order these.
|
||||||
# Each privelege level also implies access to the ones before it.
|
# Each privilege level also implies access to the ones before it.
|
||||||
# Inserting new privelege levels at a later point is fine if that is considered.
|
# Inserting new privilege levels at a later point is fine if that is considered.
|
||||||
|
|
||||||
NONE = enum.auto()
|
NONE = enum.auto()
|
||||||
"""No special privilege level."""
|
"""No special privilege level."""
|
||||||
|
|||||||
@ -257,10 +257,9 @@ class CoreLogic:
|
|||||||
The current (or new) list of prefixes.
|
The current (or new) list of prefixes.
|
||||||
"""
|
"""
|
||||||
if prefixes:
|
if prefixes:
|
||||||
prefixes = sorted(prefixes, reverse=True)
|
await self.bot._prefix_cache.set_prefixes(guild=None, prefixes=prefixes)
|
||||||
await self.bot._config.prefix.set(prefixes)
|
|
||||||
return prefixes
|
return prefixes
|
||||||
return await self.bot._config.prefix()
|
return await self.bot._prefix_cache.get_prefixes(guild=None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _version_info(cls) -> Dict[str, str]:
|
async def _version_info(cls) -> Dict[str, str]:
|
||||||
@ -563,7 +562,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
msg = ""
|
msg = ""
|
||||||
responses = []
|
responses = []
|
||||||
for i, server in enumerate(guilds, 1):
|
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))
|
responses.append(str(i))
|
||||||
|
|
||||||
for page in pagify(msg, ["\n"]):
|
for page in pagify(msg, ["\n"]):
|
||||||
@ -847,15 +846,13 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
mod_role_ids = await ctx.bot._config.guild(ctx.guild).mod_role()
|
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_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."
|
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(
|
guild_settings = _("Admin roles: {admin}\nMod roles: {mod}\n").format(
|
||||||
admin=admin_roles_str, mod=mod_roles_str
|
admin=admin_roles_str, mod=mod_roles_str
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guild_settings = ""
|
guild_settings = ""
|
||||||
prefixes = None # This is correct. The below can happen in a guild.
|
|
||||||
if not prefixes:
|
prefixes = await ctx.bot._prefix_cache.get_prefixes(ctx.guild)
|
||||||
prefixes = await ctx.bot._config.prefix()
|
|
||||||
locale = await ctx.bot._config.locale()
|
locale = await ctx.bot._config.locale()
|
||||||
|
|
||||||
prefix_string = " ".join(prefixes)
|
prefix_string = " ".join(prefixes)
|
||||||
@ -873,6 +870,32 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
for page in pagify(settings):
|
for page in pagify(settings):
|
||||||
await ctx.send(box(page))
|
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()
|
@_set.command()
|
||||||
@checks.guildowner()
|
@checks.guildowner()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -1156,11 +1179,11 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
||||||
"""Sets Red's server prefix(es)"""
|
"""Sets Red's server prefix(es)"""
|
||||||
if not prefixes:
|
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."))
|
await ctx.send(_("Guild prefixes have been reset."))
|
||||||
return
|
return
|
||||||
prefixes = sorted(prefixes, reverse=True)
|
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."))
|
await ctx.send(_("Prefix set."))
|
||||||
|
|
||||||
@_set.command()
|
@_set.command()
|
||||||
|
|||||||
@ -4,18 +4,20 @@ from . import commands
|
|||||||
|
|
||||||
def init_global_checks(bot):
|
def init_global_checks(bot):
|
||||||
@bot.check_once
|
@bot.check_once
|
||||||
def actually_up(ctx):
|
def minimum_bot_perms(ctx) -> bool:
|
||||||
"""
|
"""
|
||||||
Uptime is set during the initial startup process.
|
Too many 403, 401, and 429 Errors can cause bots to get global'd
|
||||||
If this hasn't been set, we should assume the bot isn't ready yet.
|
|
||||||
|
It's reasonable to assume the below as a minimum amount of perms for
|
||||||
|
commands.
|
||||||
"""
|
"""
|
||||||
return ctx.bot.uptime is not None
|
return ctx.channel.permissions_for(ctx.me).send_messages
|
||||||
|
|
||||||
@bot.check_once
|
@bot.check_once
|
||||||
async def whiteblacklist_checks(ctx):
|
async def whiteblacklist_checks(ctx) -> bool:
|
||||||
return await ctx.bot.allowed_by_whitelist_blacklist(ctx.author)
|
return await ctx.bot.allowed_by_whitelist_blacklist(ctx.author)
|
||||||
|
|
||||||
@bot.check_once
|
@bot.check_once
|
||||||
def bots(ctx):
|
def bots(ctx) -> bool:
|
||||||
"""Check the user is not another bot."""
|
"""Check the user is not another bot."""
|
||||||
return not ctx.author.bot
|
return not ctx.author.bot
|
||||||
|
|||||||
@ -142,6 +142,18 @@ async def _init(bot: Red):
|
|||||||
bot.add_listener(on_member_unban)
|
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):
|
async def _migrate_config(from_version: int, to_version: int):
|
||||||
if from_version == to_version:
|
if from_version == to_version:
|
||||||
return
|
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")
|
await _conf.guild(cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("cases")
|
||||||
|
|
||||||
if from_version < 3 <= to_version:
|
if from_version < 3 <= to_version:
|
||||||
all_casetypes = {
|
await handle_auditype_key()
|
||||||
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 _conf.schema_version.set(3)
|
await _conf.schema_version.set(3)
|
||||||
|
|
||||||
if from_version < 4 <= to_version:
|
if from_version < 4 <= to_version:
|
||||||
@ -507,8 +510,15 @@ class CaseType:
|
|||||||
self.image = image
|
self.image = image
|
||||||
self.case_str = case_str
|
self.case_str = case_str
|
||||||
self.guild = guild
|
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:
|
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):
|
async def to_json(self):
|
||||||
"""Transforms the case type into a dict and saves it"""
|
"""Transforms the case type into a dict and saves it"""
|
||||||
|
|||||||
53
redbot/core/settings_caches.py
Normal file
53
redbot/core/settings_caches.py
Normal file
@ -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)
|
||||||
0
redbot/py.typed
Normal file
0
redbot/py.typed
Normal file
@ -253,20 +253,23 @@ async def remove_instance(
|
|||||||
|
|
||||||
backend = get_current_backend(instance)
|
backend = get_current_backend(instance)
|
||||||
driver_cls = drivers.get_driver_class(backend)
|
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:
|
if interactive is True and remove_datapath is None:
|
||||||
await driver_cls.delete_all_data(interactive=interactive, drop_db=drop_db)
|
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:
|
if remove_datapath is True:
|
||||||
remove_datapath = click.confirm(
|
data_path = data_manager.core_data_path().parent
|
||||||
"Would you like to delete the instance's entire datapath?", default=False
|
safe_delete(data_path)
|
||||||
)
|
|
||||||
|
|
||||||
if remove_datapath is True:
|
save_config(instance, {}, remove=True)
|
||||||
data_path = data_manager.core_data_path().parent
|
finally:
|
||||||
safe_delete(data_path)
|
await driver_cls.teardown()
|
||||||
|
|
||||||
save_config(instance, {}, remove=True)
|
|
||||||
print("The instance {} has been removed\n".format(instance))
|
print("The instance {} has been removed\n".format(instance))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user