mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-07 11:48:55 -05:00
Merge remote-tracking branch 'release/V3/develop' into V3/develop
This commit is contained in:
commit
7dd5ed3446
44
.codeclimate.yml
Normal file
44
.codeclimate.yml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
version: "2" # required to adjust maintainability checks
|
||||||
|
checks:
|
||||||
|
argument-count:
|
||||||
|
config:
|
||||||
|
threshold: 6
|
||||||
|
complex-logic:
|
||||||
|
enabled: false # Disabled in favor of using Radon for this
|
||||||
|
config:
|
||||||
|
threshold: 4
|
||||||
|
file-lines:
|
||||||
|
config:
|
||||||
|
threshold: 1000 # I would set this lower if not for cogs as command containers.
|
||||||
|
method-complexity:
|
||||||
|
enabled: false # Disabled in favor of using Radon for this
|
||||||
|
config:
|
||||||
|
threshold: 5
|
||||||
|
method-count:
|
||||||
|
enabled: false # I would set this lower if not for cogs as command containers.
|
||||||
|
config:
|
||||||
|
threshold: 20
|
||||||
|
method-lines:
|
||||||
|
enabled: false
|
||||||
|
config:
|
||||||
|
threshold: 25 # I'm fine with long methods, cautious about the complexity of a single method.
|
||||||
|
nested-control-flow:
|
||||||
|
config:
|
||||||
|
threshold: 4
|
||||||
|
return-statements:
|
||||||
|
config:
|
||||||
|
threshold: 6
|
||||||
|
similar-code:
|
||||||
|
enabled: false
|
||||||
|
config:
|
||||||
|
threshold: # language-specific defaults. an override will affect all languages.
|
||||||
|
identical-code:
|
||||||
|
config:
|
||||||
|
threshold: # language-specific defaults. an override will affect all languages.
|
||||||
|
plugins:
|
||||||
|
bandit:
|
||||||
|
enabled: true
|
||||||
|
radon:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
threshold: "D"
|
||||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@ -33,7 +33,7 @@ redbot/cogs/audio/* @aikaterna
|
|||||||
redbot/cogs/bank/* @tekulvw
|
redbot/cogs/bank/* @tekulvw
|
||||||
redbot/cogs/cleanup/* @palmtree5
|
redbot/cogs/cleanup/* @palmtree5
|
||||||
redbot/cogs/customcom/* @palmtree5
|
redbot/cogs/customcom/* @palmtree5
|
||||||
redbot/cogs/downloader/* @tekulvw
|
redbot/cogs/downloader/* @tekulvw @jack1142
|
||||||
redbot/cogs/economy/* @palmtree5
|
redbot/cogs/economy/* @palmtree5
|
||||||
redbot/cogs/filter/* @palmtree5
|
redbot/cogs/filter/* @palmtree5
|
||||||
redbot/cogs/general/* @palmtree5
|
redbot/cogs/general/* @palmtree5
|
||||||
@ -49,6 +49,9 @@ redbot/cogs/warnings/* @palmtree5
|
|||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/cogs/downloader/* @jack1142
|
||||||
|
|
||||||
# Setup, instance setup, and running the bot
|
# Setup, instance setup, and running the bot
|
||||||
setup.py @tekulvw
|
setup.py @tekulvw
|
||||||
redbot/__init__.py @tekulvw
|
redbot/__init__.py @tekulvw
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/command_bug.md
vendored
3
.github/ISSUE_TEMPLATE/command_bug.md
vendored
@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Bug reports for commands
|
name: Bug reports for commands
|
||||||
about: For bugs that involve commands found within Red
|
about: For bugs that involve commands found within Red
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/feature_req.md
vendored
3
.github/ISSUE_TEMPLATE/feature_req.md
vendored
@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: For feature requests regarding Red itself.
|
about: For feature requests regarding Red itself.
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Feature'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/other_bug.md
vendored
3
.github/ISSUE_TEMPLATE/other_bug.md
vendored
@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: For bugs that don't involve a command.
|
about: For bugs that don't involve a command.
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -14,4 +14,3 @@ python:
|
|||||||
path: .
|
path: .
|
||||||
extra_requirements:
|
extra_requirements:
|
||||||
- docs
|
- docs
|
||||||
- mongo
|
|
||||||
|
|||||||
@ -27,10 +27,6 @@ jobs:
|
|||||||
postgresql: "10"
|
postgresql: "10"
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c 'create database red_db;' -U postgres
|
- psql -c 'create database red_db;' -U postgres
|
||||||
- env: TOXENV=mongo
|
|
||||||
services: mongodb
|
|
||||||
before_script:
|
|
||||||
- mongo red_db --eval 'db.createUser({user:"red",pwd:"red",roles:["readWrite"]});'
|
|
||||||
# These jobs only occur on tag creation if the prior ones succeed
|
# These jobs only occur on tag creation if the prior ones succeed
|
||||||
- stage: PyPi Deployment
|
- stage: PyPi Deployment
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
|
|||||||
1
changelog.d/2105.docs.rst
Normal file
1
changelog.d/2105.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added documentation for PM2 support.
|
||||||
1
changelog.d/2571.misc.rst
Normal file
1
changelog.d/2571.misc.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Tests now use same event loop policy as Red's code.
|
||||||
1
changelog.d/2962.enhance.rst
Normal file
1
changelog.d/2962.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
```redbot-setup delete`` now has the option to leave Red's data untouched on database backends.
|
||||||
1
changelog.d/3005.docs.rst
Normal file
1
changelog.d/3005.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Adds autostart documentation for Red users who installed it inside a virtual environment.
|
||||||
1
changelog.d/3045.enhance.rst
Normal file
1
changelog.d/3045.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Bot now handles more things prior to connecting to discord to reduce issues with initial load
|
||||||
1
changelog.d/3060.enhance.rst
Normal file
1
changelog.d/3060.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
All ``y/n`` confirmations in cli commands are now unified.
|
||||||
1
changelog.d/3060.feature.rst
Normal file
1
changelog.d/3060.feature.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``redbot --edit`` cli flag that can be used to edit instance name, token, owner and datapath.
|
||||||
1
changelog.d/3060.fix.rst
Normal file
1
changelog.d/3060.fix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Arguments ``--co-owner`` and ``--load-cogs`` now properly require at least one argument to be passed.
|
||||||
1
changelog.d/3073.breaking.rst
Normal file
1
changelog.d/3073.breaking.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
``bot.wait_until_ready`` should no longer be used during extension setup
|
||||||
1
changelog.d/3079.docs.rst
Normal file
1
changelog.d/3079.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Word using dev during install more strongly, to try to avoid end users using dev.
|
||||||
1
changelog.d/3083.docs.rst
Normal file
1
changelog.d/3083.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix some typos and wording, add MS Azure to host list
|
||||||
1
changelog.d/3090.feature.rst
Normal file
1
changelog.d/3090.feature.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
adds a licenseinfo command
|
||||||
1
changelog.d/3099.breaking.rst
Normal file
1
changelog.d/3099.breaking.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Removes the mongo driver.
|
||||||
1
changelog.d/3100.bugfix.rst
Normal file
1
changelog.d/3100.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
fix ``is_automod_immune`` handling of guild check and support for checking webhooks
|
||||||
1
changelog.d/3105.docs.rst
Normal file
1
changelog.d/3105.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Update docs footer copyright to 2019.
|
||||||
1
changelog.d/3110.docs.rst
Normal file
1
changelog.d/3110.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Update apikey framework documentation. Change bot.get_shared_api_keys() to bot.get_shared_api_tokens().
|
||||||
1
changelog.d/3118.feature.rst
Normal file
1
changelog.d/3118.feature.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Adds a command to list disabled commands globally or per guild.
|
||||||
1
changelog.d/3121.enhance.rst
Normal file
1
changelog.d/3121.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Change ``[p]info`` to say "This bot is an..." instead of "This is an..." for clarity.
|
||||||
1
changelog.d/3124.docs.rst
Normal file
1
changelog.d/3124.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add information about ``info.json``'s ``min_python_version`` key in Downloader Framework docs.
|
||||||
1
changelog.d/3134.docs.rst
Normal file
1
changelog.d/3134.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add event reference for ``on_red_api_tokens_update`` event in Shared API Keys docs.
|
||||||
1
changelog.d/3134.feature.rst
Normal file
1
changelog.d/3134.feature.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
New event ``on_red_api_tokens_update`` is now dispatched when shared api keys for the service are updated.
|
||||||
1
changelog.d/3172.enhance.rst
Normal file
1
changelog.d/3172.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Clarified that ``[p]backup`` saves the **bot's** data in the help text.
|
||||||
1
changelog.d/3174.bugfix.rst
Normal file
1
changelog.d/3174.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
``--owner`` and ``-p`` cli flags now work when added from launcher.
|
||||||
1
changelog.d/admin/3166.bugfix.rst
Normal file
1
changelog.d/admin/3166.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fixed ``[p]announce`` failing after encountering an error attempting to message the bot owner.
|
||||||
1
changelog.d/audio/3048.bugfix.rst
Normal file
1
changelog.d/audio/3048.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Unify capitalisation in ``[p]help playlist``.
|
||||||
1
changelog.d/audio/3050.bugfix.rst
Normal file
1
changelog.d/audio/3050.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Bot's status is now properly cleared on emptydisconnect.
|
||||||
1
changelog.d/audio/3051.enhance.rst
Normal file
1
changelog.d/audio/3051.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Improved explanation in help string for ``[p]audioset emptydisconnect``.
|
||||||
1
changelog.d/audio/3058.enhancement.rst
Normal file
1
changelog.d/audio/3058.enhancement.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add typing indicator to playlist dedupe
|
||||||
1
changelog.d/audio/3085.enhance.1.rst
Normal file
1
changelog.d/audio/3085.enhance.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Expose FriendlyExceptions to users on the play command.
|
||||||
1
changelog.d/audio/3104.misc.1.rst
Normal file
1
changelog.d/audio/3104.misc.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix an issue where some YouTube playlists were being recognised as single tracks.
|
||||||
1
changelog.d/downloader/1866.enhance.rst
Normal file
1
changelog.d/downloader/1866.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Downloader will now check if Python and bot version match requirements in ``info.json`` during update.
|
||||||
1
changelog.d/downloader/2527.docs.rst
Normal file
1
changelog.d/downloader/2527.docs.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added :func:`redbot.cogs.downloader.installable.InstalledModule` to Downloader's framework docs.
|
||||||
1
changelog.d/downloader/2527.enhance.1.rst
Normal file
1
changelog.d/downloader/2527.enhance.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
User can now pass multiple cog names to ``[p]cog install``.
|
||||||
1
changelog.d/downloader/2527.enhance.2.rst
Normal file
1
changelog.d/downloader/2527.enhance.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
When passing cogs to ``[p]cog update`` command, it will now only update those cogs, not all cogs from the repo these cogs are from.
|
||||||
1
changelog.d/downloader/2527.feature.1.rst
Normal file
1
changelog.d/downloader/2527.feature.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]repo update [repos]`` command that allows you to update repos without updating cogs from them.
|
||||||
1
changelog.d/downloader/2527.feature.2.rst
Normal file
1
changelog.d/downloader/2527.feature.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]cog installversion <repo_name> <revision> <cogs>`` command that allows you to install cogs from specified revision (commit, tag) of given repo. When using this command, the cog will automatically be pinned.
|
||||||
1
changelog.d/downloader/2527.feature.3.rst
Normal file
1
changelog.d/downloader/2527.feature.3.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]cog pin <cogs>`` and ``[p]cog unpin <cogs>`` for pinning cogs. Cogs that are pinned will not be updated when using update commands.
|
||||||
1
changelog.d/downloader/2527.feature.4.rst
Normal file
1
changelog.d/downloader/2527.feature.4.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]cog checkforupdates`` command that will tell which cogs can be updated (including pinned cog) without updating them.
|
||||||
1
changelog.d/downloader/2527.feature.5.rst
Normal file
1
changelog.d/downloader/2527.feature.5.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]cog updateallfromrepos <repos>`` command that will update all cogs from given repos.
|
||||||
1
changelog.d/downloader/2527.feature.6.rst
Normal file
1
changelog.d/downloader/2527.feature.6.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``[p]cog updatetoversion <repo_name> <revision> [cogs]`` command that updates all cogs or ones of user's choosing to chosen revision of given repo.
|
||||||
4
changelog.d/downloader/2527.misc.1.rst
Normal file
4
changelog.d/downloader/2527.misc.1.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Added :func:`redbot.cogs.downloader.installable.InstalledModule` which is used instead of :func:`redbot.cogs.downloader.installable.Installable` when we refer to installed cog or shared library.
|
||||||
|
Therefore:
|
||||||
|
- ``to_json`` and ``from_json`` methods were moved from :func:`redbot.cogs.downloader.installable.Installable` to :func:`redbot.cogs.downloader.installable.InstalledModule`
|
||||||
|
- return types changed for :func:`redbot.cogs.downloader.converters.InstalledCog.convert`, :func:`redbot.cogs.downloader.downloader.Downloader.installed_cogs`, :func:`redbot.cogs.downloader.repo_manager.Repo.install_cog` to use :func:`redbot.cogs.downloader.installable.InstalledModule`.
|
||||||
1
changelog.d/downloader/2571.bugfix.1.rst
Normal file
1
changelog.d/downloader/2571.bugfix.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Made regex for repo names use raw string to stop ``DeprecationWarning`` about invalid escape sequence.
|
||||||
1
changelog.d/downloader/2571.bugfix.2.rst
Normal file
1
changelog.d/downloader/2571.bugfix.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Downloader will no longer allow to install cog that is already installed.
|
||||||
1
changelog.d/downloader/2571.dep.rst
Normal file
1
changelog.d/downloader/2571.dep.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added ``pytest-mock`` requirement to ``tests`` extra.
|
||||||
1
changelog.d/downloader/2571.enhance.rst
Normal file
1
changelog.d/downloader/2571.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added error messages for failures during installing/reinstalling requirements and copying cogs and shared libraries.
|
||||||
1
changelog.d/downloader/2571.misc.rst
Normal file
1
changelog.d/downloader/2571.misc.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added more Downloader tests for Repo logic and git integration. New git tests use a test repo file that can be generated using new tool at ``tools/edit_testrepo.py``.
|
||||||
1
changelog.d/downloader/2927.bugfix.rst
Normal file
1
changelog.d/downloader/2927.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Downloader will no longer allow to install cog with same name as other that is installed.
|
||||||
1
changelog.d/downloader/2936.bugfix.rst
Normal file
1
changelog.d/downloader/2936.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Catch errors if remote repository or branch is deleted, notify user which repository failed and continue updating others.
|
||||||
1
changelog.d/downloader/3080.misc.1.rst
Normal file
1
changelog.d/downloader/3080.misc.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
`RepoManager.update_all_repos` replaced by new method `update_repos` which additionally handles failing repositories.
|
||||||
1
changelog.d/downloader/3080.misc.2.rst
Normal file
1
changelog.d/downloader/3080.misc.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added `Downloader.format_failed_repos` for formatting error message of repos failing to update.
|
||||||
1
changelog.d/downloader/3129.enhance.rst
Normal file
1
changelog.d/downloader/3129.enhance.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Use sanitized url (without HTTP Basic Auth fragments) in `[p]findcog` command.
|
||||||
1
changelog.d/downloader/3129.misc.rst
Normal file
1
changelog.d/downloader/3129.misc.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add `clean_url` property to :class:`redbot.cogs.downloader.repo_manager.Repo` which contains sanitized repo URL (without HTTP Basic Auth).
|
||||||
1
changelog.d/downloader/3141.bugfix.rst
Normal file
1
changelog.d/downloader/3141.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Make :attr:`redbot.cogs.downloader.repo_manager.Repo.clean_url` work with relative urls. This property uses `str` type now.
|
||||||
1
changelog.d/downloader/3153.bugfix.rst
Normal file
1
changelog.d/downloader/3153.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fixed an error on repo add from empty string values for the `install_msg` info.json field.
|
||||||
1
changelog.d/downloader/3159.bugfix.rst
Normal file
1
changelog.d/downloader/3159.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Disable all git auth prompts when adding/updating repo with Downloader.
|
||||||
1
changelog.d/downloader/3160.misc.rst
Normal file
1
changelog.d/downloader/3160.misc.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ensure consistent output from git commands for purpose of parsing.
|
||||||
1
changelog.d/downloader/3177.bugfix.rst
Normal file
1
changelog.d/downloader/3177.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
``[p]findcog`` now properly works for cogs with less typical folder structure.
|
||||||
1
changelog.d/permissions/3037.bugfix.rst
Normal file
1
changelog.d/permissions/3037.bugfix.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
defaults are cleared properly when clearing all rules
|
||||||
42
docs/autostart_pm2.rst
Normal file
42
docs/autostart_pm2.rst
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.. pm2 service guide
|
||||||
|
|
||||||
|
==============================================
|
||||||
|
Setting up auto-restart using pm2 on Linux
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
.. note:: This guide is for setting up PM2 on a Linux environment. This guide assumes that you already have a working Red instance.
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Installing PM2
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Start by installing Node.JS and NPM via your favorite package distributor. From there run the following command:
|
||||||
|
|
||||||
|
:code:`npm install pm2 -g`
|
||||||
|
|
||||||
|
After PM2 is installed, run the following command to enable your Red instance to be managed by PM2. Replace the brackets with the required information.
|
||||||
|
You can add additional Red based arguments after the instance, such as :code:`--dev`.
|
||||||
|
|
||||||
|
:code:`pm2 start redbot --name "<Insert a name here>" --interpreter "<Location to your Python Interpreter>" -- <Red Instance> --no-prompt`
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
Arguments to replace.
|
||||||
|
|
||||||
|
--name ""
|
||||||
|
A name to identify the bot within pm2, this is not your Red instance.
|
||||||
|
|
||||||
|
--interpreter ""
|
||||||
|
The location of your Python interpreter, to find out where that is use the following command:
|
||||||
|
which python3.6
|
||||||
|
|
||||||
|
<Red Instance>
|
||||||
|
The name of your Red instance.
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
Ensuring that PM2 stays online
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
To make sure that PM2 stays online and persistence between machine restarts, run the following commands:
|
||||||
|
|
||||||
|
:code:`pm2 save` & :code:`pm2 startup`
|
||||||
@ -8,11 +8,23 @@ Setting up auto-restart using systemd on Linux
|
|||||||
Creating the service file
|
Creating the service file
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
Create the new service file:
|
In order to create the service file, you will first need the location of your :code:`redbot` binary.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# If redbot is installed in a virtualenv
|
||||||
|
source redenv/bin/activate
|
||||||
|
|
||||||
|
# If you are using pyenv
|
||||||
|
pyenv shell <name>
|
||||||
|
|
||||||
|
which redbot
|
||||||
|
|
||||||
|
Then create the new service file:
|
||||||
|
|
||||||
:code:`sudo -e /etc/systemd/system/red@.service`
|
:code:`sudo -e /etc/systemd/system/red@.service`
|
||||||
|
|
||||||
Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root):
|
Paste the following and replace all instances of :code:`username` with the username, and :code:`path` with the location you obtained above:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
@ -21,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=/home/username/.local/bin/redbot %I --no-prompt
|
ExecStart=path %I --no-prompt
|
||||||
User=username
|
User=username
|
||||||
Group=username
|
Group=username
|
||||||
Type=idle
|
Type=idle
|
||||||
|
|||||||
@ -58,7 +58,7 @@ master_doc = "index"
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = "Red - Discord Bot"
|
project = "Red - Discord Bot"
|
||||||
copyright = "2018, Cog Creators"
|
copyright = "2018-2019, Cog Creators"
|
||||||
author = "Cog Creators"
|
author = "Cog Creators"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
|||||||
@ -18,7 +18,7 @@ and when accessed in the code it should be done by
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
await self.bot.get_shared_api_keys("twitch")
|
await self.bot.get_shared_api_tokens("twitch")
|
||||||
|
|
||||||
Each service has its own dict of key, value pairs for each required key type. If there's only one key required then a name for the key is still required for storing and accessing.
|
Each service has its own dict of key, value pairs for each required key type. If there's only one key required then a name for the key is still required for storing and accessing.
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ and when accessed in the code it should be done by
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
await self.bot.get_shared_api_keys("youtube")
|
await self.bot.get_shared_api_tokens("youtube")
|
||||||
|
|
||||||
|
|
||||||
***********
|
***********
|
||||||
@ -42,7 +42,21 @@ Basic Usage
|
|||||||
class MyCog:
|
class MyCog:
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def youtube(self, ctx, user: str):
|
async def youtube(self, ctx, user: str):
|
||||||
youtube_keys = await self.bot.get_shared_api_keys("youtube")
|
youtube_keys = await self.bot.get_shared_api_tokens("youtube")
|
||||||
if youtube_keys.get("api_key") is None:
|
if youtube_keys.get("api_key") is None:
|
||||||
return await ctx.send("The YouTube API key has not been set.")
|
return await ctx.send("The YouTube API key has not been set.")
|
||||||
# Use the API key to access content as you normally would
|
# Use the API key to access content as you normally would
|
||||||
|
|
||||||
|
|
||||||
|
***************
|
||||||
|
Event Reference
|
||||||
|
***************
|
||||||
|
|
||||||
|
.. function:: on_red_api_tokens_update(service_name, api_tokens)
|
||||||
|
|
||||||
|
Dispatched when service's api keys are updated.
|
||||||
|
|
||||||
|
:param service_name: Name of the service.
|
||||||
|
:type service_name: :class:`str`
|
||||||
|
:param api_tokens: New Mapping of token names to tokens. This contains api tokens that weren't changed too.
|
||||||
|
:type api_tokens: Mapping[:class:`str`, :class:`str`]
|
||||||
|
|||||||
@ -429,7 +429,3 @@ JSON Driver
|
|||||||
.. autoclass:: redbot.core.drivers.JsonDriver
|
.. autoclass:: redbot.core.drivers.JsonDriver
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Mongo Driver
|
|
||||||
^^^^^^^^^^^^
|
|
||||||
.. autoclass:: redbot.core.drivers.MongoDriver
|
|
||||||
:members:
|
|
||||||
|
|||||||
@ -35,6 +35,9 @@ Keys specific to the cog info.json (case sensitive)
|
|||||||
- ``max_bot_version`` (string) - Max version number of Red in the format ``MAJOR.MINOR.MICRO``,
|
- ``max_bot_version`` (string) - Max version number of Red in the format ``MAJOR.MINOR.MICRO``,
|
||||||
if ``min_bot_version`` is newer than ``max_bot_version``, ``max_bot_version`` will be ignored
|
if ``min_bot_version`` is newer than ``max_bot_version``, ``max_bot_version`` will be ignored
|
||||||
|
|
||||||
|
- ``min_python_version`` (list of integers) - Min version number of Python
|
||||||
|
in the format ``[MAJOR, MINOR, PATCH]``
|
||||||
|
|
||||||
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
||||||
|
|
||||||
- ``disabled`` (bool) - Determines if a cog is available for install.
|
- ``disabled`` (bool) - Determines if a cog is available for install.
|
||||||
@ -68,6 +71,12 @@ Installable
|
|||||||
.. autoclass:: Installable
|
.. autoclass:: Installable
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
InstalledModule
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: InstalledModule
|
||||||
|
:members:
|
||||||
|
|
||||||
.. automodule:: redbot.cogs.downloader.repo_manager
|
.. automodule:: redbot.cogs.downloader.repo_manager
|
||||||
|
|
||||||
Repo
|
Repo
|
||||||
|
|||||||
@ -115,8 +115,8 @@ to use one, do it like this: ``[p]cleanup messages 10``
|
|||||||
Cogs
|
Cogs
|
||||||
----
|
----
|
||||||
|
|
||||||
Red is built with cogs, fancy term for plugins. They are
|
Red is built with cogs, a fancy term for plugins. They are
|
||||||
modules that enhance the Red functionalities. They contain
|
modules that add functionality to Red. They contain
|
||||||
commands to use.
|
commands to use.
|
||||||
|
|
||||||
Red comes with 19 cogs containing the basic features, such
|
Red comes with 19 cogs containing the basic features, such
|
||||||
@ -162,10 +162,10 @@ there are hundreds of cogs available!
|
|||||||
|
|
||||||
.. 26-cogs not available, let's use my repo :3
|
.. 26-cogs not available, let's use my repo :3
|
||||||
|
|
||||||
Cogs comes with repositories. A repository is a container of cogs
|
Cogs come in repositories. A repository is a container of cogs
|
||||||
that you can install. Let's suppose you want to install the ``say``
|
that you can install. Let's suppose you want to install the ``say``
|
||||||
cog from the repository ``Laggrons-Dumb-Cogs``. You'll first need
|
cog from the repository ``Laggrons-Dumb-Cogs``. You'll first need
|
||||||
to install the repository.
|
to add the repository.
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ to install the repository.
|
|||||||
|
|
||||||
.. note:: You may need to specify a branch. If so, add its name after the link.
|
.. note:: You may need to specify a branch. If so, add its name after the link.
|
||||||
|
|
||||||
Then you can add the cog
|
Then you can install the cog
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ the level of permission needed for a command.
|
|||||||
Bot owner
|
Bot owner
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
The bot owner can access all commands on every guild. He can also use
|
The bot owner can access all commands on every guild. They can also use
|
||||||
exclusive commands that can interact with the global settings
|
exclusive commands that can interact with the global settings
|
||||||
or system files.
|
or system files.
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ Administrator
|
|||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
The administrator is defined by its roles. You can set multiple admin roles
|
The administrator is defined by its roles. You can set multiple admin roles
|
||||||
with the ``[p]addadminrole`` and ``[p]removeadminrole`` commands.
|
with the ``[p]set addadminrole`` and ``[p]set removeadminrole`` commands.
|
||||||
|
|
||||||
For example, in the mod cog, an admin can use the ``[p]modset`` command
|
For example, in the mod cog, an admin can use the ``[p]modset`` command
|
||||||
which defines the cog settings.
|
which defines the cog settings.
|
||||||
@ -224,7 +224,7 @@ Moderator
|
|||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
A moderator is a step above the average users. You can set multiple moderator
|
A moderator is a step above the average users. You can set multiple moderator
|
||||||
roles with the ``[p]addmodrole`` and ``[p]removemodrole`` commands.
|
roles with the ``[p]set addmodrole`` and ``[p]set removemodrole`` commands.
|
||||||
|
|
||||||
For example, in the mod cog (again), a mod will be able to mute, kick and ban;
|
For example, in the mod cog (again), a mod will be able to mute, kick and ban;
|
||||||
but he won't be able to modify the cog settings with the ``[p]modset`` command.
|
but he won't be able to modify the cog settings with the ``[p]modset`` command.
|
||||||
|
|||||||
@ -56,6 +56,9 @@ Others
|
|||||||
|`Google Cloud |Same as AWS, but it's Google. |
|
|`Google Cloud |Same as AWS, but it's Google. |
|
||||||
|<https://cloud.google.com/compute/>`_| |
|
|<https://cloud.google.com/compute/>`_| |
|
||||||
+-------------------------------------+-----------------------------------------------------+
|
+-------------------------------------+-----------------------------------------------------+
|
||||||
|
|`Microsoft Azure |Same as AWS, but it's Microsoft. |
|
||||||
|
|<https://azure.microsoft.com>`_ | |
|
||||||
|
+-------------------------------------+-----------------------------------------------------+
|
||||||
|`LowEndBox <http://lowendbox.com/>`_ |A curator for lower specced servers. |
|
|`LowEndBox <http://lowendbox.com/>`_ |A curator for lower specced servers. |
|
||||||
+-------------------------------------+-----------------------------------------------------+
|
+-------------------------------------+-----------------------------------------------------+
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
install_linux_mac
|
install_linux_mac
|
||||||
venv_guide
|
venv_guide
|
||||||
autostart_systemd
|
autostart_systemd
|
||||||
|
autostart_pm2
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|||||||
@ -265,18 +265,12 @@ Choose one of the following commands to install Red.
|
|||||||
|
|
||||||
python3.7 -m pip install --user -U Red-DiscordBot
|
python3.7 -m pip install --user -U Red-DiscordBot
|
||||||
|
|
||||||
To install without MongoDB support:
|
To install without additional config backend support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python3.7 -m pip install -U Red-DiscordBot
|
python3.7 -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
Or, to install with MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
python3.7 -m pip install -U Red-DiscordBot[mongo]
|
|
||||||
|
|
||||||
Or, to install with PostgreSQL support:
|
Or, to install with PostgreSQL support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
@ -286,7 +280,11 @@ Or, to install with PostgreSQL support:
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
||||||
following link:
|
link below. **The development version of the bot contains experimental changes. It is not
|
||||||
|
intended for normal users.** We will not support anyone using the development version in any
|
||||||
|
support channels. Using the development version may break third party cogs and not all core
|
||||||
|
commands may work. Downgrading to stable after installing the development version may cause
|
||||||
|
data loss, crashes or worse.
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
|
|||||||
@ -76,12 +76,6 @@ Installing Red
|
|||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
* With MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot[mongo]
|
|
||||||
|
|
||||||
* With PostgreSQL support:
|
* With PostgreSQL support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
@ -91,7 +85,11 @@ Installing Red
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
||||||
following link:
|
link below. **The development version of the bot contains experimental changes. It is not
|
||||||
|
intended for normal users.** We will not support anyone using the development version in any
|
||||||
|
support channels. Using the development version may break third party cogs and not all core
|
||||||
|
commands may work. Downgrading to stable after installing the development version may cause
|
||||||
|
data loss, crashes or worse.
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio as _asyncio
|
||||||
import re as _re
|
import re as _re
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
import warnings as _warnings
|
import warnings as _warnings
|
||||||
@ -15,8 +16,13 @@ from typing import (
|
|||||||
|
|
||||||
MIN_PYTHON_VERSION = (3, 7, 0)
|
MIN_PYTHON_VERSION = (3, 7, 0)
|
||||||
|
|
||||||
__all__ = ["MIN_PYTHON_VERSION", "__version__", "version_info", "VersionInfo"]
|
__all__ = [
|
||||||
|
"MIN_PYTHON_VERSION",
|
||||||
|
"__version__",
|
||||||
|
"version_info",
|
||||||
|
"VersionInfo",
|
||||||
|
"_update_event_loop_policy",
|
||||||
|
]
|
||||||
if _sys.version_info < MIN_PYTHON_VERSION:
|
if _sys.version_info < MIN_PYTHON_VERSION:
|
||||||
print(
|
print(
|
||||||
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
||||||
@ -173,7 +179,20 @@ class VersionInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.1.6"
|
def _update_event_loop_policy():
|
||||||
|
if _sys.platform == "win32":
|
||||||
|
_asyncio.set_event_loop_policy(_asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
elif _sys.implementation.name == "cpython":
|
||||||
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
|
try:
|
||||||
|
import uvloop as _uvloop
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "3.1.8"
|
||||||
version_info = VersionInfo.from_str(__version__)
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
|||||||
@ -6,24 +6,19 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
# Set the event loop policies here so any subsequent `get_event_loop()`
|
# Set the event loop policies here so any subsequent `get_event_loop()`
|
||||||
# calls, in particular those as a result of the following imports,
|
# calls, in particular those as a result of the following imports,
|
||||||
# return the correct loop object.
|
# return the correct loop object.
|
||||||
if sys.platform == "win32":
|
from redbot import _update_event_loop_policy
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
||||||
elif sys.implementation.name == "cpython":
|
_update_event_loop_policy()
|
||||||
# Let's not force this dependency, uvloop is much faster on cpython
|
|
||||||
try:
|
|
||||||
import uvloop
|
|
||||||
except ImportError:
|
|
||||||
uvloop = None
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
||||||
|
|
||||||
import redbot.logging
|
import redbot.logging
|
||||||
from redbot.core.bot import Red, ExitCodes
|
from redbot.core.bot import Red, ExitCodes
|
||||||
@ -31,7 +26,8 @@ from redbot.core.cog_manager import CogManagerUI
|
|||||||
from redbot.core.global_checks import init_global_checks
|
from redbot.core.global_checks import init_global_checks
|
||||||
from redbot.core.events import init_events
|
from redbot.core.events import init_events
|
||||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags
|
from redbot.core.cli import interactive_config, confirm, parse_cli_flags
|
||||||
from redbot.core.core_commands import Core
|
from redbot.core.core_commands import Core, license_info_command
|
||||||
|
from redbot.setup import get_data_dir, get_name, save_config
|
||||||
from redbot.core.dev_commands import Dev
|
from redbot.core.dev_commands import Dev
|
||||||
from redbot.core import __version__, modlog, bank, data_manager, drivers
|
from redbot.core import __version__, modlog, bank, data_manager, drivers
|
||||||
from signal import SIGTERM
|
from signal import SIGTERM
|
||||||
@ -56,6 +52,12 @@ async def _get_prefix_and_token(red, indict):
|
|||||||
indict["prefix"] = await red._config.prefix()
|
indict["prefix"] = await red._config.prefix()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_instance_names():
|
||||||
|
with data_manager.config_file.open(encoding="utf-8") as fs:
|
||||||
|
data = json.load(fs)
|
||||||
|
return sorted(data.keys())
|
||||||
|
|
||||||
|
|
||||||
def list_instances():
|
def list_instances():
|
||||||
if not data_manager.config_file.exists():
|
if not data_manager.config_file.exists():
|
||||||
print(
|
print(
|
||||||
@ -64,22 +66,164 @@ def list_instances():
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
with data_manager.config_file.open(encoding="utf-8") as fs:
|
|
||||||
data = json.load(fs)
|
|
||||||
text = "Configured Instances:\n\n"
|
text = "Configured Instances:\n\n"
|
||||||
for instance_name in sorted(data.keys()):
|
for instance_name in _get_instance_names():
|
||||||
text += "{}\n".format(instance_name)
|
text += "{}\n".format(instance_name)
|
||||||
print(text)
|
print(text)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_instance(red, cli_flags):
|
||||||
|
no_prompt = cli_flags.no_prompt
|
||||||
|
token = cli_flags.token
|
||||||
|
owner = cli_flags.owner
|
||||||
|
old_name = cli_flags.instance_name
|
||||||
|
new_name = cli_flags.edit_instance_name
|
||||||
|
data_path = cli_flags.edit_data_path
|
||||||
|
copy_data = cli_flags.copy_data
|
||||||
|
confirm_overwrite = cli_flags.overwrite_existing_instance
|
||||||
|
|
||||||
|
if data_path is None and copy_data:
|
||||||
|
print("--copy-data can't be used without --edit-data-path argument")
|
||||||
|
sys.exit(1)
|
||||||
|
if new_name is None and confirm_overwrite:
|
||||||
|
print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
|
||||||
|
sys.exit(1)
|
||||||
|
if no_prompt and all(to_change is None for to_change in (token, owner, new_name, data_path)):
|
||||||
|
print(
|
||||||
|
"No arguments to edit were provided. Available arguments (check help for more "
|
||||||
|
"information): --edit-instance-name, --edit-data-path, --copy-data, --owner, --token"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
_edit_token(red, token, no_prompt)
|
||||||
|
_edit_owner(red, owner, no_prompt)
|
||||||
|
|
||||||
|
data = deepcopy(data_manager.basic_config)
|
||||||
|
name = _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt)
|
||||||
|
_edit_data_path(data, data_path, copy_data, no_prompt)
|
||||||
|
|
||||||
|
save_config(name, data)
|
||||||
|
if old_name != name:
|
||||||
|
save_config(old_name, {}, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_token(red, token, no_prompt):
|
||||||
|
if token:
|
||||||
|
if not len(token) >= 50:
|
||||||
|
print(
|
||||||
|
"The provided token doesn't look a valid Discord bot token."
|
||||||
|
" Instance's token will remain unchanged.\n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
red.loop.run_until_complete(red._config.token.set(token))
|
||||||
|
elif not no_prompt and confirm("Would you like to change instance's token?", default=False):
|
||||||
|
interactive_config(red, False, True, print_header=False)
|
||||||
|
print("Token updated.\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_owner(red, owner, no_prompt):
|
||||||
|
if owner:
|
||||||
|
if not (15 <= len(str(owner)) <= 21):
|
||||||
|
print(
|
||||||
|
"The provided owner id doesn't look like a valid Discord user id."
|
||||||
|
" Instance's owner will remain unchanged."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
red.loop.run_until_complete(red._config.owner.set(owner))
|
||||||
|
elif not no_prompt and confirm("Would you like to change instance's owner?", default=False):
|
||||||
|
print(
|
||||||
|
"Remember:\n"
|
||||||
|
"ONLY the person who is hosting Red should be owner."
|
||||||
|
" This has SERIOUS security implications."
|
||||||
|
" The owner can access any data that is present on the host system.\n"
|
||||||
|
)
|
||||||
|
if confirm("Are you sure you want to change instance's owner?", default=False):
|
||||||
|
print("Please enter a Discord user id for new owner:")
|
||||||
|
while True:
|
||||||
|
owner_id = input("> ").strip()
|
||||||
|
if not (15 <= len(owner_id) <= 21 and owner_id.isdecimal()):
|
||||||
|
print("That doesn't look like a valid Discord user id.")
|
||||||
|
continue
|
||||||
|
owner_id = int(owner_id)
|
||||||
|
red.loop.run_until_complete(red._config.owner.set(owner_id))
|
||||||
|
print("Owner updated.")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Instance's owner will remain unchanged.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt):
|
||||||
|
if new_name:
|
||||||
|
name = new_name
|
||||||
|
if name in _get_instance_names() and not confirm_overwrite:
|
||||||
|
name = old_name
|
||||||
|
print(
|
||||||
|
"An instance with this name already exists.\n"
|
||||||
|
"If you want to remove the existing instance and replace it with this one,"
|
||||||
|
" run this command with --overwrite-existing-instance flag."
|
||||||
|
)
|
||||||
|
elif not no_prompt and confirm("Would you like to change the instance name?", default=False):
|
||||||
|
name = get_name()
|
||||||
|
if name in _get_instance_names():
|
||||||
|
print(
|
||||||
|
"WARNING: An instance already exists with this name. "
|
||||||
|
"Continuing will overwrite the existing instance config."
|
||||||
|
)
|
||||||
|
if not confirm(
|
||||||
|
"Are you absolutely certain you want to continue with this instance name?",
|
||||||
|
default=False,
|
||||||
|
):
|
||||||
|
print("Instance name will remain unchanged.")
|
||||||
|
name = old_name
|
||||||
|
else:
|
||||||
|
print("Instance name updated.")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
name = old_name
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_data_path(data, data_path, copy_data, no_prompt):
|
||||||
|
# This modifies the passed dict.
|
||||||
|
if data_path:
|
||||||
|
data["DATA_PATH"] = data_path
|
||||||
|
if copy_data and not _copy_data(data):
|
||||||
|
print("Can't copy data to non-empty location. Data location will remain unchanged.")
|
||||||
|
data["DATA_PATH"] = data_manager.basic_config["DATA_PATH"]
|
||||||
|
elif not no_prompt and confirm("Would you like to change the data location?", default=False):
|
||||||
|
data["DATA_PATH"] = get_data_dir()
|
||||||
|
if confirm(
|
||||||
|
"Do you want to copy the data from old location?", default=True
|
||||||
|
) and not _copy_data(data):
|
||||||
|
print("Can't copy the data to non-empty location.")
|
||||||
|
if not confirm("Do you still want to use the new data location?"):
|
||||||
|
data["DATA_PATH"] = data_manager.basic_config["DATA_PATH"]
|
||||||
|
print("Data location will remain unchanged.")
|
||||||
|
else:
|
||||||
|
print("Data location updated.")
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_data(data):
|
||||||
|
if Path(data["DATA_PATH"]).exists():
|
||||||
|
if any(os.scandir(data["DATA_PATH"])):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# this is needed because copytree doesn't work when destination folder exists
|
||||||
|
# Python 3.8 has `dirs_exist_ok` option for that
|
||||||
|
os.rmdir(data["DATA_PATH"])
|
||||||
|
shutil.copytree(data_manager.basic_config["DATA_PATH"], data["DATA_PATH"])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def sigterm_handler(red, log):
|
async def sigterm_handler(red, log):
|
||||||
log.info("SIGTERM received. Quitting...")
|
log.info("SIGTERM received. Quitting...")
|
||||||
await red.shutdown(restart=False)
|
await red.shutdown(restart=False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
description = "Red V3 (c) Cog Creators"
|
description = "Red V3"
|
||||||
cli_flags = parse_cli_flags(sys.argv[1:])
|
cli_flags = parse_cli_flags(sys.argv[1:])
|
||||||
if cli_flags.list_instances:
|
if cli_flags.list_instances:
|
||||||
list_instances()
|
list_instances()
|
||||||
@ -87,7 +231,7 @@ def main():
|
|||||||
print(description)
|
print(description)
|
||||||
print("Current Version: {}".format(__version__))
|
print("Current Version: {}".format(__version__))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
elif not cli_flags.instance_name and (not cli_flags.no_instance or cli_flags.edit):
|
||||||
print("Error: No instance name was provided!")
|
print("Error: No instance name was provided!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if cli_flags.no_instance:
|
if cli_flags.no_instance:
|
||||||
@ -116,6 +260,16 @@ def main():
|
|||||||
cli_flags=cli_flags, description=description, dm_help=None, fetch_offline_members=True
|
cli_flags=cli_flags, description=description, dm_help=None, fetch_offline_members=True
|
||||||
)
|
)
|
||||||
loop.run_until_complete(red._maybe_update_config())
|
loop.run_until_complete(red._maybe_update_config())
|
||||||
|
|
||||||
|
if cli_flags.edit:
|
||||||
|
try:
|
||||||
|
edit_instance(red, cli_flags)
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print("Aborted!")
|
||||||
|
finally:
|
||||||
|
loop.run_until_complete(driver_cls.teardown())
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
|
|
||||||
@ -128,6 +282,7 @@ def main():
|
|||||||
|
|
||||||
red.add_cog(Core(red))
|
red.add_cog(Core(red))
|
||||||
red.add_cog(CogManagerUI())
|
red.add_cog(CogManagerUI())
|
||||||
|
red.add_command(license_info_command)
|
||||||
if cli_flags.dev:
|
if cli_flags.dev:
|
||||||
red.add_cog(Dev())
|
red.add_cog(Dev())
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
@ -157,13 +312,12 @@ def main():
|
|||||||
loop.run_until_complete(red.http.close())
|
loop.run_until_complete(red.http.close())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(red.start(token, bot=True))
|
loop.run_until_complete(red.start(token, bot=True, cli_flags=cli_flags))
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
log.critical("This token doesn't seem to be valid.")
|
log.critical("This token doesn't seem to be valid.")
|
||||||
db_token = loop.run_until_complete(red._config.token())
|
db_token = loop.run_until_complete(red._config.token())
|
||||||
if db_token and not cli_flags.no_prompt:
|
if db_token and not cli_flags.no_prompt:
|
||||||
print("\nDo you want to reset the token? (y/n)")
|
if confirm("\nDo you want to reset the token?"):
|
||||||
if confirm("> "):
|
|
||||||
loop.run_until_complete(red._config.token.set(""))
|
loop.run_until_complete(red._config.token.set(""))
|
||||||
print("Token has been reset.")
|
print("Token has been reset.")
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import asyncio
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_list, inline
|
||||||
|
|
||||||
_ = Translator("Announcer", __file__)
|
_ = Translator("Announcer", __file__)
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class Announcer:
|
|||||||
|
|
||||||
async def announcer(self):
|
async def announcer(self):
|
||||||
guild_list = self.ctx.bot.guilds
|
guild_list = self.ctx.bot.guilds
|
||||||
bot_owner = (await self.ctx.bot.application_info()).owner
|
failed = []
|
||||||
for g in guild_list:
|
for g in guild_list:
|
||||||
if not self.active:
|
if not self.active:
|
||||||
return
|
return
|
||||||
@ -66,9 +67,14 @@ class Announcer:
|
|||||||
try:
|
try:
|
||||||
await channel.send(self.message)
|
await channel.send(self.message)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await bot_owner.send(
|
failed.append(str(g.id))
|
||||||
_("I could not announce to server: {server.id}").format(server=g)
|
|
||||||
)
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
_("I could not announce to the following server: ")
|
||||||
|
if len(failed) == 1
|
||||||
|
else _("I could not announce to the following servers: ")
|
||||||
|
)
|
||||||
|
msg += humanize_list(tuple(map(inline, failed)))
|
||||||
|
await self.ctx.bot.send_to_owners(msg)
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from redbot.core import commands
|
|||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
cog = Audio(bot)
|
cog = Audio(bot)
|
||||||
await cog.initialize()
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import random
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Callable, Dict, List, Mapping, NoReturn, Optional, Tuple, Union
|
from typing import Callable, Dict, List, Mapping, Optional, Tuple, Union
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from sqlite3 import Error as SQLError
|
from sqlite3 import Error as SQLError
|
||||||
@ -32,7 +32,7 @@ from lavalink.rest_api import LoadResult
|
|||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands
|
||||||
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 . import dataclasses
|
from . import audio_dataclasses
|
||||||
from .errors import InvalidTableError, SpotifyFetchError, YouTubeApiError
|
from .errors import InvalidTableError, SpotifyFetchError, YouTubeApiError
|
||||||
from .playlists import get_playlist
|
from .playlists import get_playlist
|
||||||
from .utils import CacheLevel, Notifier, is_allowed, queue_duration, track_limit
|
from .utils import CacheLevel, Notifier, is_allowed, queue_duration, track_limit
|
||||||
@ -193,7 +193,7 @@ class SpotifyAPI:
|
|||||||
)
|
)
|
||||||
return await r.json()
|
return await r.json()
|
||||||
|
|
||||||
async def _get_auth(self) -> NoReturn:
|
async def _get_auth(self):
|
||||||
if self.client_id is None or self.client_secret is None:
|
if self.client_id is None or self.client_secret is None:
|
||||||
tokens = await self.bot.get_shared_api_tokens("spotify")
|
tokens = await self.bot.get_shared_api_tokens("spotify")
|
||||||
self.client_id = tokens.get("client_id", "")
|
self.client_id = tokens.get("client_id", "")
|
||||||
@ -331,7 +331,7 @@ class MusicCache:
|
|||||||
self._lock: asyncio.Lock = asyncio.Lock()
|
self._lock: asyncio.Lock = asyncio.Lock()
|
||||||
self.config: Optional[Config] = None
|
self.config: Optional[Config] = None
|
||||||
|
|
||||||
async def initialize(self, config: Config) -> NoReturn:
|
async def initialize(self, config: Config):
|
||||||
if HAS_SQL:
|
if HAS_SQL:
|
||||||
await self.database.connect()
|
await self.database.connect()
|
||||||
|
|
||||||
@ -348,12 +348,12 @@ class MusicCache:
|
|||||||
await self.database.execute(query=_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE)
|
await self.database.execute(query=_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE)
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
async def close(self) -> NoReturn:
|
async def close(self):
|
||||||
if HAS_SQL:
|
if HAS_SQL:
|
||||||
await self.database.execute(query="PRAGMA optimize;")
|
await self.database.execute(query="PRAGMA optimize;")
|
||||||
await self.database.disconnect()
|
await self.database.disconnect()
|
||||||
|
|
||||||
async def insert(self, table: str, values: List[dict]) -> NoReturn:
|
async def insert(self, table: str, values: List[dict]):
|
||||||
# if table == "spotify":
|
# if table == "spotify":
|
||||||
# return
|
# return
|
||||||
if HAS_SQL:
|
if HAS_SQL:
|
||||||
@ -363,7 +363,7 @@ class MusicCache:
|
|||||||
|
|
||||||
await self.database.execute_many(query=query, values=values)
|
await self.database.execute_many(query=query, values=values)
|
||||||
|
|
||||||
async def update(self, table: str, values: Dict[str, str]) -> NoReturn:
|
async def update(self, table: str, values: Dict[str, str]):
|
||||||
# if table == "spotify":
|
# if table == "spotify":
|
||||||
# return
|
# return
|
||||||
if HAS_SQL:
|
if HAS_SQL:
|
||||||
@ -746,7 +746,7 @@ class MusicCache:
|
|||||||
if val:
|
if val:
|
||||||
try:
|
try:
|
||||||
result, called_api = await self.lavalink_query(
|
result, called_api = await self.lavalink_query(
|
||||||
ctx, player, dataclasses.Query.process_input(val)
|
ctx, player, audio_dataclasses.Query.process_input(val)
|
||||||
)
|
)
|
||||||
except (RuntimeError, aiohttp.ServerDisconnectedError):
|
except (RuntimeError, aiohttp.ServerDisconnectedError):
|
||||||
lock(ctx, False)
|
lock(ctx, False)
|
||||||
@ -805,7 +805,7 @@ class MusicCache:
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{single_track.title} {single_track.author} {single_track.uri} "
|
f"{single_track.title} {single_track.author} {single_track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(single_track))}"
|
f"{str(audio_dataclasses.Query.process_input(single_track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
has_not_allowed = True
|
has_not_allowed = True
|
||||||
@ -911,7 +911,7 @@ class MusicCache:
|
|||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
player: lavalink.Player,
|
player: lavalink.Player,
|
||||||
query: dataclasses.Query,
|
query: audio_dataclasses.Query,
|
||||||
forced: bool = False,
|
forced: bool = False,
|
||||||
) -> Tuple[LoadResult, bool]:
|
) -> Tuple[LoadResult, bool]:
|
||||||
"""
|
"""
|
||||||
@ -925,7 +925,7 @@ class MusicCache:
|
|||||||
The context this method is being called under.
|
The context this method is being called under.
|
||||||
player : lavalink.Player
|
player : lavalink.Player
|
||||||
The player who's requesting the query.
|
The player who's requesting the query.
|
||||||
query: dataclasses.Query
|
query: audio_dataclasses.Query
|
||||||
The Query object for the query in question.
|
The Query object for the query in question.
|
||||||
forced:bool
|
forced:bool
|
||||||
Whether or not to skip cache and call API first..
|
Whether or not to skip cache and call API first..
|
||||||
@ -939,7 +939,7 @@ class MusicCache:
|
|||||||
)
|
)
|
||||||
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
||||||
val = None
|
val = None
|
||||||
_raw_query = dataclasses.Query.process_input(query)
|
_raw_query = audio_dataclasses.Query.process_input(query)
|
||||||
query = str(_raw_query)
|
query = str(_raw_query)
|
||||||
if cache_enabled and not forced and not _raw_query.is_local:
|
if cache_enabled and not forced and not _raw_query.is_local:
|
||||||
update = True
|
update = True
|
||||||
@ -1003,14 +1003,10 @@ class MusicCache:
|
|||||||
tasks = self._tasks[ctx.message.id]
|
tasks = self._tasks[ctx.message.id]
|
||||||
del self._tasks[ctx.message.id]
|
del self._tasks[ctx.message.id]
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[asyncio.ensure_future(self.insert(*a)) for a in tasks["insert"]],
|
*[self.insert(*a) for a in tasks["insert"]], return_exceptions=True
|
||||||
loop=self.bot.loop,
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[asyncio.ensure_future(self.update(*a)) for a in tasks["update"]],
|
*[self.update(*a) for a in tasks["update"]], return_exceptions=True
|
||||||
loop=self.bot.loop,
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
log.debug(f"Completed database writes for {lock_id} " f"({lock_author})")
|
log.debug(f"Completed database writes for {lock_id} " f"({lock_author})")
|
||||||
|
|
||||||
@ -1025,14 +1021,10 @@ class MusicCache:
|
|||||||
self._tasks = {}
|
self._tasks = {}
|
||||||
|
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[asyncio.ensure_future(self.insert(*a)) for a in tasks["insert"]],
|
*[self.insert(*a) for a in tasks["insert"]], return_exceptions=True
|
||||||
loop=self.bot.loop,
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[asyncio.ensure_future(self.update(*a)) for a in tasks["update"]],
|
*[self.update(*a) for a in tasks["update"]], return_exceptions=True
|
||||||
loop=self.bot.loop,
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
log.debug("Completed pending writes to database have finished")
|
log.debug("Completed pending writes to database have finished")
|
||||||
|
|
||||||
@ -1096,7 +1088,9 @@ class MusicCache:
|
|||||||
if not tracks:
|
if not tracks:
|
||||||
ctx = namedtuple("Context", "message")
|
ctx = namedtuple("Context", "message")
|
||||||
results, called_api = await self.lavalink_query(
|
results, called_api = await self.lavalink_query(
|
||||||
ctx(player.channel.guild), player, dataclasses.Query.process_input(_TOP_100_US)
|
ctx(player.channel.guild),
|
||||||
|
player,
|
||||||
|
audio_dataclasses.Query.process_input(_TOP_100_US),
|
||||||
)
|
)
|
||||||
tracks = list(results.tracks)
|
tracks = list(results.tracks)
|
||||||
if tracks:
|
if tracks:
|
||||||
@ -1107,7 +1101,7 @@ class MusicCache:
|
|||||||
|
|
||||||
while valid is False and multiple:
|
while valid is False and multiple:
|
||||||
track = random.choice(tracks)
|
track = random.choice(tracks)
|
||||||
query = dataclasses.Query.process_input(track)
|
query = audio_dataclasses.Query.process_input(track)
|
||||||
if not query.valid:
|
if not query.valid:
|
||||||
continue
|
continue
|
||||||
if query.is_local and not query.track.exists():
|
if query.is_local and not query.track.exists():
|
||||||
@ -1116,7 +1110,7 @@ class MusicCache:
|
|||||||
player.channel.guild,
|
player.channel.guild,
|
||||||
(
|
(
|
||||||
f"{track.title} {track.author} {track.uri} "
|
f"{track.title} {track.author} {track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(track))}"
|
f"{str(audio_dataclasses.Query.process_input(track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|||||||
@ -34,7 +34,7 @@ from redbot.core.utils.menus import (
|
|||||||
start_adding_reactions,
|
start_adding_reactions,
|
||||||
)
|
)
|
||||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
from . import dataclasses
|
from . import audio_dataclasses
|
||||||
from .apis import MusicCache, HAS_SQL, _ERROR
|
from .apis import MusicCache, HAS_SQL, _ERROR
|
||||||
from .checks import can_have_caching
|
from .checks import can_have_caching
|
||||||
from .converters import ComplexScopeParser, ScopeParser, get_lazy_converter, get_playlist_converter
|
from .converters import ComplexScopeParser, ScopeParser, get_lazy_converter, get_playlist_converter
|
||||||
@ -142,7 +142,11 @@ class Audio(commands.Cog):
|
|||||||
self.play_lock = {}
|
self.play_lock = {}
|
||||||
|
|
||||||
self._manager: Optional[ServerManager] = None
|
self._manager: Optional[ServerManager] = None
|
||||||
self.bot.dispatch("red_audio_initialized", self)
|
# These has to be a task since this requires the bot to be ready
|
||||||
|
# If it waits for ready in startup, we cause a deadlock during initial load
|
||||||
|
# as initial load happens before the bot can ever be ready.
|
||||||
|
self._init_task = self.bot.loop.create_task(self.initialize())
|
||||||
|
self._ready_event = asyncio.Event()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owns_autoplay(self):
|
def owns_autoplay(self):
|
||||||
@ -166,9 +170,14 @@ class Audio(commands.Cog):
|
|||||||
self._cog_id = None
|
self._cog_id = None
|
||||||
|
|
||||||
async def cog_before_invoke(self, ctx: commands.Context):
|
async def cog_before_invoke(self, ctx: commands.Context):
|
||||||
|
await self._ready_event.wait()
|
||||||
|
# check for unsupported arch
|
||||||
|
# Check on this needs refactoring at a later date
|
||||||
|
# so that we have a better way to handle the tasks
|
||||||
if self.llsetup in [ctx.command, ctx.command.root_parent]:
|
if self.llsetup in [ctx.command, ctx.command.root_parent]:
|
||||||
pass
|
pass
|
||||||
elif self._connect_task.cancelled():
|
|
||||||
|
elif self._connect_task and self._connect_task.cancelled():
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"You have attempted to run Audio's Lavalink server on an unsupported"
|
"You have attempted to run Audio's Lavalink server on an unsupported"
|
||||||
" architecture. Only settings related commands will be available."
|
" architecture. Only settings related commands will be available."
|
||||||
@ -176,6 +185,7 @@ class Audio(commands.Cog):
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Not running audio command due to invalid machine architecture for Lavalink."
|
"Not running audio command due to invalid machine architecture for Lavalink."
|
||||||
)
|
)
|
||||||
|
|
||||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||||
if dj_enabled:
|
if dj_enabled:
|
||||||
dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role())
|
dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role())
|
||||||
@ -185,13 +195,13 @@ class Audio(commands.Cog):
|
|||||||
await self._embed_msg(ctx, _("No DJ role found. Disabling DJ mode."))
|
await self._embed_msg(ctx, _("No DJ role found. Disabling DJ mode."))
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
pass_config_to_dependencies(self.config, self.bot, await self.config.localpath())
|
await self.bot.wait_until_ready()
|
||||||
|
# Unlike most cases, we want the cache to exit before migration.
|
||||||
await self.music_cache.initialize(self.config)
|
await self.music_cache.initialize(self.config)
|
||||||
asyncio.ensure_future(
|
await self._migrate_config(
|
||||||
self._migrate_config(
|
from_version=await self.config.schema_version(), to_version=_SCHEMA_VERSION
|
||||||
from_version=await self.config.schema_version(), to_version=_SCHEMA_VERSION
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
pass_config_to_dependencies(self.config, self.bot, await self.config.localpath())
|
||||||
self._restart_connect()
|
self._restart_connect()
|
||||||
self._disconnect_task = self.bot.loop.create_task(self.disconnect_timer())
|
self._disconnect_task = self.bot.loop.create_task(self.disconnect_timer())
|
||||||
lavalink.register_event_listener(self.event_handler)
|
lavalink.register_event_listener(self.event_handler)
|
||||||
@ -209,6 +219,9 @@ class Audio(commands.Cog):
|
|||||||
await self.bot.send_to_owners(page)
|
await self.bot.send_to_owners(page)
|
||||||
log.critical(error_message)
|
log.critical(error_message)
|
||||||
|
|
||||||
|
self._ready_event.set()
|
||||||
|
self.bot.dispatch("red_audio_initialized", self)
|
||||||
|
|
||||||
async def _migrate_config(self, from_version: int, to_version: int):
|
async def _migrate_config(self, from_version: int, to_version: int):
|
||||||
database_entries = []
|
database_entries = []
|
||||||
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
||||||
@ -253,7 +266,7 @@ class Audio(commands.Cog):
|
|||||||
cast(discord.Guild, discord.Object(id=guild_id))
|
cast(discord.Guild, discord.Object(id=guild_id))
|
||||||
).clear_raw("playlists")
|
).clear_raw("playlists")
|
||||||
if database_entries and HAS_SQL:
|
if database_entries and HAS_SQL:
|
||||||
asyncio.ensure_future(self.music_cache.insert("lavalink", database_entries))
|
await self.music_cache.insert("lavalink", database_entries)
|
||||||
|
|
||||||
def _restart_connect(self):
|
def _restart_connect(self):
|
||||||
if self._connect_task:
|
if self._connect_task:
|
||||||
@ -366,7 +379,9 @@ class Audio(commands.Cog):
|
|||||||
async def _players_check():
|
async def _players_check():
|
||||||
try:
|
try:
|
||||||
get_single_title = lavalink.active_players()[0].current.title
|
get_single_title = lavalink.active_players()[0].current.title
|
||||||
query = dataclasses.Query.process_input(lavalink.active_players()[0].current.uri)
|
query = audio_dataclasses.Query.process_input(
|
||||||
|
lavalink.active_players()[0].current.uri
|
||||||
|
)
|
||||||
if get_single_title == "Unknown title":
|
if get_single_title == "Unknown title":
|
||||||
get_single_title = lavalink.active_players()[0].current.uri
|
get_single_title = lavalink.active_players()[0].current.uri
|
||||||
if not get_single_title.startswith("http"):
|
if not get_single_title.startswith("http"):
|
||||||
@ -463,18 +478,18 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
await notify_channel.send(embed=embed)
|
await notify_channel.send(embed=embed)
|
||||||
|
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
|
|
||||||
if query.is_local if player.current else False:
|
if query.is_local if player.current else False:
|
||||||
if player.current.title != "Unknown title":
|
if player.current.title != "Unknown title":
|
||||||
description = "**{} - {}**\n{}".format(
|
description = "**{} - {}**\n{}".format(
|
||||||
player.current.author,
|
player.current.author,
|
||||||
player.current.title,
|
player.current.title,
|
||||||
dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
audio_dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
description = "{}".format(
|
description = "{}".format(
|
||||||
dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
audio_dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
description = "**[{}]({})**".format(player.current.title, player.current.uri)
|
description = "**[{}]({})**".format(player.current.title, player.current.uri)
|
||||||
@ -532,9 +547,9 @@ class Audio(commands.Cog):
|
|||||||
message_channel = player.fetch("channel")
|
message_channel = player.fetch("channel")
|
||||||
if message_channel:
|
if message_channel:
|
||||||
message_channel = self.bot.get_channel(message_channel)
|
message_channel = self.bot.get_channel(message_channel)
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if player.current and query.is_local:
|
if player.current and query.is_local:
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if player.current.title == "Unknown title":
|
if player.current.title == "Unknown title":
|
||||||
description = "{}".format(query.track.to_string_hidden())
|
description = "{}".format(query.track.to_string_hidden())
|
||||||
else:
|
else:
|
||||||
@ -590,7 +605,7 @@ class Audio(commands.Cog):
|
|||||||
player.store("channel", channel.id)
|
player.store("channel", channel.id)
|
||||||
player.store("guild", guild.id)
|
player.store("guild", guild.id)
|
||||||
await self._data_check(guild.me)
|
await self._data_check(guild.me)
|
||||||
query = dataclasses.Query.process_input(query)
|
query = audio_dataclasses.Query.process_input(query)
|
||||||
ctx = namedtuple("Context", "message")
|
ctx = namedtuple("Context", "message")
|
||||||
results, called_api = await self.music_cache.lavalink_query(ctx(guild), player, query)
|
results, called_api = await self.music_cache.lavalink_query(ctx(guild), player, query)
|
||||||
|
|
||||||
@ -985,7 +1000,7 @@ 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-disconnection after x seconds while stopped. 0 to disable."""
|
"""Auto-disconnect from channel when bot is alone in it for x seconds. 0 to disable."""
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
return await self._embed_msg(ctx, _("Can't be less than zero."))
|
return await self._embed_msg(ctx, _("Can't be less than zero."))
|
||||||
if 10 > seconds > 0:
|
if 10 > seconds > 0:
|
||||||
@ -1094,7 +1109,7 @@ class Audio(commands.Cog):
|
|||||||
with contextlib.suppress(discord.HTTPException):
|
with contextlib.suppress(discord.HTTPException):
|
||||||
await info.delete()
|
await info.delete()
|
||||||
return
|
return
|
||||||
temp = dataclasses.LocalPath(local_path, forced=True)
|
temp = audio_dataclasses.LocalPath(local_path, forced=True)
|
||||||
if not temp.exists() or not temp.is_dir():
|
if not temp.exists() or not temp.is_dir():
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
@ -1536,7 +1551,7 @@ class Audio(commands.Cog):
|
|||||||
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
query = dataclasses.Query.process_input(p.current.uri)
|
query = audio_dataclasses.Query.process_input(p.current.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if p.current.title == "Unknown title":
|
if p.current.title == "Unknown title":
|
||||||
current_title = localtracks.LocalPath(p.current.uri).to_string_hidden()
|
current_title = localtracks.LocalPath(p.current.uri).to_string_hidden()
|
||||||
@ -1606,9 +1621,9 @@ class Audio(commands.Cog):
|
|||||||
bump_song = player.queue[bump_index]
|
bump_song = player.queue[bump_index]
|
||||||
player.queue.insert(0, bump_song)
|
player.queue.insert(0, bump_song)
|
||||||
removed = player.queue.pop(index)
|
removed = player.queue.pop(index)
|
||||||
query = dataclasses.Query.process_input(removed.uri)
|
query = audio_dataclasses.Query.process_input(removed.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
localtrack = dataclasses.LocalPath(removed.uri)
|
localtrack = audio_dataclasses.LocalPath(removed.uri)
|
||||||
if removed.title != "Unknown title":
|
if removed.title != "Unknown title":
|
||||||
description = "**{} - {}**\n{}".format(
|
description = "**{} - {}**\n{}".format(
|
||||||
removed.author, removed.title, localtrack.to_string_hidden()
|
removed.author, removed.title, localtrack.to_string_hidden()
|
||||||
@ -1997,12 +2012,12 @@ class Audio(commands.Cog):
|
|||||||
await ctx.invoke(self.local_play, play_subfolders=play_subfolders)
|
await ctx.invoke(self.local_play, play_subfolders=play_subfolders)
|
||||||
else:
|
else:
|
||||||
folder = folder.strip()
|
folder = folder.strip()
|
||||||
_dir = dataclasses.LocalPath.joinpath(folder)
|
_dir = audio_dataclasses.LocalPath.joinpath(folder)
|
||||||
if not _dir.exists():
|
if not _dir.exists():
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx, _("No localtracks folder named {name}.").format(name=folder)
|
ctx, _("No localtracks folder named {name}.").format(name=folder)
|
||||||
)
|
)
|
||||||
query = dataclasses.Query.process_input(_dir, search_subfolders=play_subfolders)
|
query = audio_dataclasses.Query.process_input(_dir, search_subfolders=play_subfolders)
|
||||||
await self._local_play_all(ctx, query, from_search=False if not folder else True)
|
await self._local_play_all(ctx, query, from_search=False if not folder else True)
|
||||||
|
|
||||||
@local.command(name="play")
|
@local.command(name="play")
|
||||||
@ -2064,8 +2079,8 @@ class Audio(commands.Cog):
|
|||||||
all_tracks = await self._folder_list(
|
all_tracks = await self._folder_list(
|
||||||
ctx,
|
ctx,
|
||||||
(
|
(
|
||||||
dataclasses.Query.process_input(
|
audio_dataclasses.Query.process_input(
|
||||||
dataclasses.LocalPath(
|
audio_dataclasses.LocalPath(
|
||||||
await self.config.localpath()
|
await self.config.localpath()
|
||||||
).localtrack_folder.absolute(),
|
).localtrack_folder.absolute(),
|
||||||
search_subfolders=play_subfolders,
|
search_subfolders=play_subfolders,
|
||||||
@ -2081,18 +2096,18 @@ class Audio(commands.Cog):
|
|||||||
return await ctx.invoke(self.search, query=search_list)
|
return await ctx.invoke(self.search, query=search_list)
|
||||||
|
|
||||||
async def _localtracks_folders(self, ctx: commands.Context, search_subfolders=False):
|
async def _localtracks_folders(self, ctx: commands.Context, search_subfolders=False):
|
||||||
audio_data = dataclasses.LocalPath(
|
audio_data = audio_dataclasses.LocalPath(
|
||||||
dataclasses.LocalPath(None).localtrack_folder.absolute()
|
audio_dataclasses.LocalPath(None).localtrack_folder.absolute()
|
||||||
)
|
)
|
||||||
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 audio_data.subfolders_in_tree() if search_subfolders else audio_data.subfolders()
|
||||||
|
|
||||||
async def _folder_list(self, ctx: commands.Context, query: dataclasses.Query):
|
async def _folder_list(self, ctx: commands.Context, query: audio_dataclasses.Query):
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
return
|
||||||
query = dataclasses.Query.process_input(query)
|
query = audio_dataclasses.Query.process_input(query)
|
||||||
if not query.track.exists():
|
if not query.track.exists():
|
||||||
return
|
return
|
||||||
return (
|
return (
|
||||||
@ -2102,12 +2117,12 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _folder_tracks(
|
async def _folder_tracks(
|
||||||
self, ctx, player: lavalink.player_manager.Player, query: dataclasses.Query
|
self, ctx, player: lavalink.player_manager.Player, query: audio_dataclasses.Query
|
||||||
):
|
):
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
audio_data = dataclasses.LocalPath(None)
|
audio_data = audio_dataclasses.LocalPath(None)
|
||||||
try:
|
try:
|
||||||
query.track.path.relative_to(audio_data.to_string())
|
query.track.path.relative_to(audio_data.to_string())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -2120,17 +2135,17 @@ class Audio(commands.Cog):
|
|||||||
return local_tracks
|
return local_tracks
|
||||||
|
|
||||||
async def _local_play_all(
|
async def _local_play_all(
|
||||||
self, ctx: commands.Context, query: dataclasses.Query, from_search=False
|
self, ctx: commands.Context, query: audio_dataclasses.Query, from_search=False
|
||||||
):
|
):
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
return
|
||||||
if from_search:
|
if from_search:
|
||||||
query = dataclasses.Query.process_input(
|
query = audio_dataclasses.Query.process_input(
|
||||||
query.track.to_string(), invoked_from="local folder"
|
query.track.to_string(), invoked_from="local folder"
|
||||||
)
|
)
|
||||||
await ctx.invoke(self.search, query=query)
|
await ctx.invoke(self.search, query=query)
|
||||||
|
|
||||||
async def _all_folder_tracks(self, ctx: commands.Context, query: dataclasses.Query):
|
async def _all_folder_tracks(self, ctx: commands.Context, query: audio_dataclasses.Query):
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2141,7 +2156,7 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _localtracks_check(self, ctx: commands.Context):
|
async def _localtracks_check(self, ctx: commands.Context):
|
||||||
folder = dataclasses.LocalPath(None)
|
folder = audio_dataclasses.LocalPath(None)
|
||||||
if folder.localtrack_folder.exists():
|
if folder.localtrack_folder.exists():
|
||||||
return True
|
return True
|
||||||
if ctx.invoked_with != "start":
|
if ctx.invoked_with != "start":
|
||||||
@ -2177,7 +2192,7 @@ class Audio(commands.Cog):
|
|||||||
dur = "LIVE"
|
dur = "LIVE"
|
||||||
else:
|
else:
|
||||||
dur = lavalink.utils.format_time(player.current.length)
|
dur = lavalink.utils.format_time(player.current.length)
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if not player.current.title == "Unknown title":
|
if not player.current.title == "Unknown title":
|
||||||
song = "**{track.author} - {track.title}**\n{uri}\n"
|
song = "**{track.author} - {track.title}**\n{uri}\n"
|
||||||
@ -2189,8 +2204,8 @@ class Audio(commands.Cog):
|
|||||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||||
song = song.format(
|
song = song.format(
|
||||||
track=player.current,
|
track=player.current,
|
||||||
uri=dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
uri=audio_dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
||||||
if dataclasses.Query.process_input(player.current.uri).is_local
|
if audio_dataclasses.Query.process_input(player.current.uri).is_local
|
||||||
else player.current.uri,
|
else player.current.uri,
|
||||||
arrow=arrow,
|
arrow=arrow,
|
||||||
pos=pos,
|
pos=pos,
|
||||||
@ -2301,9 +2316,9 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
if not player.current:
|
if not player.current:
|
||||||
return await self._embed_msg(ctx, _("Nothing playing."))
|
return await self._embed_msg(ctx, _("Nothing playing."))
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if player.current.title == "Unknown title":
|
if player.current.title == "Unknown title":
|
||||||
description = "{}".format(query.track.to_string_hidden())
|
description = "{}".format(query.track.to_string_hidden())
|
||||||
else:
|
else:
|
||||||
@ -2436,7 +2451,7 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
if not await self._currency_check(ctx, guild_data["jukebox_price"]):
|
if not await self._currency_check(ctx, guild_data["jukebox_price"]):
|
||||||
return
|
return
|
||||||
query = dataclasses.Query.process_input(query)
|
query = audio_dataclasses.Query.process_input(query)
|
||||||
if not query.valid:
|
if not query.valid:
|
||||||
return await self._embed_msg(ctx, _("No tracks to play."))
|
return await self._embed_msg(ctx, _("No tracks to play."))
|
||||||
if query.is_spotify:
|
if query.is_spotify:
|
||||||
@ -2593,7 +2608,7 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
playlists_search_page_list.append(embed)
|
playlists_search_page_list.append(embed)
|
||||||
playlists_pick = await menu(ctx, playlists_search_page_list, playlist_search_controls)
|
playlists_pick = await menu(ctx, playlists_search_page_list, playlist_search_controls)
|
||||||
query = dataclasses.Query.process_input(playlists_pick)
|
query = audio_dataclasses.Query.process_input(playlists_pick)
|
||||||
if not query.valid:
|
if not query.valid:
|
||||||
return await self._embed_msg(ctx, _("No tracks to play."))
|
return await self._embed_msg(ctx, _("No tracks to play."))
|
||||||
if not await self._currency_check(ctx, guild_data["jukebox_price"]):
|
if not await self._currency_check(ctx, guild_data["jukebox_price"]):
|
||||||
@ -2728,7 +2743,7 @@ class Audio(commands.Cog):
|
|||||||
elif player.current:
|
elif player.current:
|
||||||
await self._embed_msg(ctx, _("Adding a track to queue."))
|
await self._embed_msg(ctx, _("Adding a track to queue."))
|
||||||
|
|
||||||
async def _get_spotify_tracks(self, ctx: commands.Context, query: dataclasses.Query):
|
async def _get_spotify_tracks(self, ctx: commands.Context, query: audio_dataclasses.Query):
|
||||||
if ctx.invoked_with in ["play", "genre"]:
|
if ctx.invoked_with in ["play", "genre"]:
|
||||||
enqueue_tracks = True
|
enqueue_tracks = True
|
||||||
else:
|
else:
|
||||||
@ -2771,12 +2786,12 @@ class Audio(commands.Cog):
|
|||||||
self._play_lock(ctx, False)
|
self._play_lock(ctx, False)
|
||||||
try:
|
try:
|
||||||
if enqueue_tracks:
|
if enqueue_tracks:
|
||||||
new_query = dataclasses.Query.process_input(res[0])
|
new_query = audio_dataclasses.Query.process_input(res[0])
|
||||||
new_query.start_time = query.start_time
|
new_query.start_time = query.start_time
|
||||||
return await self._enqueue_tracks(ctx, new_query)
|
return await self._enqueue_tracks(ctx, new_query)
|
||||||
else:
|
else:
|
||||||
result, called_api = await self.music_cache.lavalink_query(
|
result, called_api = await self.music_cache.lavalink_query(
|
||||||
ctx, player, dataclasses.Query.process_input(res[0])
|
ctx, player, audio_dataclasses.Query.process_input(res[0])
|
||||||
)
|
)
|
||||||
tracks = result.tracks
|
tracks = result.tracks
|
||||||
if not tracks:
|
if not tracks:
|
||||||
@ -2808,7 +2823,9 @@ class Audio(commands.Cog):
|
|||||||
ctx, _("This doesn't seem to be a supported Spotify URL or code.")
|
ctx, _("This doesn't seem to be a supported Spotify URL or code.")
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _enqueue_tracks(self, ctx: commands.Context, query: Union[dataclasses.Query, list]):
|
async def _enqueue_tracks(
|
||||||
|
self, ctx: commands.Context, query: Union[audio_dataclasses.Query, list]
|
||||||
|
):
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
try:
|
try:
|
||||||
if self.play_lock[ctx.message.guild.id]:
|
if self.play_lock[ctx.message.guild.id]:
|
||||||
@ -2835,6 +2852,8 @@ class Audio(commands.Cog):
|
|||||||
if not tracks:
|
if not tracks:
|
||||||
self._play_lock(ctx, False)
|
self._play_lock(ctx, False)
|
||||||
embed = discord.Embed(title=_("Nothing found."), colour=await ctx.embed_colour())
|
embed = discord.Embed(title=_("Nothing found."), colour=await ctx.embed_colour())
|
||||||
|
if result.exception_message:
|
||||||
|
embed.set_footer(text=result.exception_message)
|
||||||
if await self.config.use_external_lavalink() and query.is_local:
|
if await self.config.use_external_lavalink() and query.is_local:
|
||||||
embed.description = _(
|
embed.description = _(
|
||||||
"Local tracks will not work "
|
"Local tracks will not work "
|
||||||
@ -2861,7 +2880,7 @@ class Audio(commands.Cog):
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{track.title} {track.author} {track.uri} "
|
f"{track.title} {track.author} {track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(track))}"
|
f"{str(audio_dataclasses.Query.process_input(track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||||
@ -2921,7 +2940,7 @@ class Audio(commands.Cog):
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{single_track.title} {single_track.author} {single_track.uri} "
|
f"{single_track.title} {single_track.author} {single_track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(single_track))}"
|
f"{str(audio_dataclasses.Query.process_input(single_track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||||
@ -2954,17 +2973,17 @@ class Audio(commands.Cog):
|
|||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx, _("Nothing found. Check your Lavalink logs for details.")
|
ctx, _("Nothing found. Check your Lavalink logs for details.")
|
||||||
)
|
)
|
||||||
query = dataclasses.Query.process_input(single_track.uri)
|
query = audio_dataclasses.Query.process_input(single_track.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if single_track.title != "Unknown title":
|
if single_track.title != "Unknown title":
|
||||||
description = "**{} - {}**\n{}".format(
|
description = "**{} - {}**\n{}".format(
|
||||||
single_track.author,
|
single_track.author,
|
||||||
single_track.title,
|
single_track.title,
|
||||||
dataclasses.LocalPath(single_track.uri).to_string_hidden(),
|
audio_dataclasses.LocalPath(single_track.uri).to_string_hidden(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
description = "{}".format(
|
description = "{}".format(
|
||||||
dataclasses.LocalPath(single_track.uri).to_string_hidden()
|
audio_dataclasses.LocalPath(single_track.uri).to_string_hidden()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
description = "**[{}]({})**".format(single_track.title, single_track.uri)
|
description = "**[{}]({})**".format(single_track.title, single_track.uri)
|
||||||
@ -2985,7 +3004,11 @@ class Audio(commands.Cog):
|
|||||||
self._play_lock(ctx, False)
|
self._play_lock(ctx, False)
|
||||||
|
|
||||||
async def _spotify_playlist(
|
async def _spotify_playlist(
|
||||||
self, ctx: commands.Context, stype: str, query: dataclasses.Query, enqueue: bool = False
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
stype: str,
|
||||||
|
query: audio_dataclasses.Query,
|
||||||
|
enqueue: bool = False,
|
||||||
):
|
):
|
||||||
|
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
@ -3253,8 +3276,8 @@ class Audio(commands.Cog):
|
|||||||
Only editable by bot owner.
|
Only editable by bot owner.
|
||||||
**Guild**:
|
**Guild**:
|
||||||
Visible to all users in this guild.
|
Visible to all users in this guild.
|
||||||
Editable By Bot Owner, Guild Owner, Guild Admins,
|
Editable by bot owner, guild owner, guild admins,
|
||||||
Guild Mods, DJ Role and playlist creator.
|
guild mods, DJ role and playlist creator.
|
||||||
**User**:
|
**User**:
|
||||||
Visible to all bot users, if --author is passed.
|
Visible to all bot users, if --author is passed.
|
||||||
Editable by bot owner and creator.
|
Editable by bot owner and creator.
|
||||||
@ -3338,7 +3361,7 @@ class Audio(commands.Cog):
|
|||||||
return
|
return
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
to_append = await self._playlist_tracks(
|
to_append = await self._playlist_tracks(
|
||||||
ctx, player, dataclasses.Query.process_input(query)
|
ctx, player, audio_dataclasses.Query.process_input(query)
|
||||||
)
|
)
|
||||||
if not to_append:
|
if not to_append:
|
||||||
return await self._embed_msg(ctx, _("Could not find a track matching your query."))
|
return await self._embed_msg(ctx, _("Could not find a track matching your query."))
|
||||||
@ -3714,89 +3737,92 @@ class Audio(commands.Cog):
|
|||||||
[p]playlist dedupe MyGlobalPlaylist --scope Global
|
[p]playlist dedupe MyGlobalPlaylist --scope Global
|
||||||
[p]playlist dedupe MyPersonalPlaylist --scope User
|
[p]playlist dedupe MyPersonalPlaylist --scope User
|
||||||
"""
|
"""
|
||||||
if scope_data is None:
|
async with ctx.typing():
|
||||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
if scope_data is None:
|
||||||
scope, author, guild, specified_user = scope_data
|
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||||
scope_name = humanize_scope(
|
scope, author, guild, specified_user = scope_data
|
||||||
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
scope_name = humanize_scope(
|
||||||
)
|
scope, ctx=guild if scope == PlaylistScope.GUILD.value else author
|
||||||
|
|
||||||
try:
|
|
||||||
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
|
||||||
ctx, playlist_matches, scope, author, guild, specified_user
|
|
||||||
)
|
|
||||||
except TooManyMatches as e:
|
|
||||||
return await self._embed_msg(ctx, str(e))
|
|
||||||
if playlist_id is None:
|
|
||||||
return await self._embed_msg(
|
|
||||||
ctx, _("Could not match '{arg}' to a playlist.").format(arg=playlist_arg)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist = await get_playlist(playlist_id, scope, self.bot, guild, author)
|
playlist_id, playlist_arg = await self._get_correct_playlist_id(
|
||||||
except RuntimeError:
|
ctx, playlist_matches, scope, author, guild, specified_user
|
||||||
return await self._embed_msg(
|
)
|
||||||
ctx,
|
except TooManyMatches as e:
|
||||||
_("Playlist {id} does not exist in {scope} scope.").format(
|
return await self._embed_msg(ctx, str(e))
|
||||||
id=playlist_id, scope=humanize_scope(scope, the=True)
|
if playlist_id is None:
|
||||||
),
|
return await self._embed_msg(
|
||||||
)
|
ctx, _("Could not match '{arg}' to a playlist.").format(arg=playlist_arg)
|
||||||
except MissingGuild:
|
)
|
||||||
return await self._embed_msg(
|
|
||||||
ctx, _("You need to specify the Guild ID for the guild to lookup.")
|
|
||||||
)
|
|
||||||
|
|
||||||
if not await self.can_manage_playlist(scope, playlist, ctx, author, guild):
|
try:
|
||||||
return
|
playlist = await get_playlist(playlist_id, scope, self.bot, guild, author)
|
||||||
|
except RuntimeError:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_("Playlist {id} does not exist in {scope} scope.").format(
|
||||||
|
id=playlist_id, scope=humanize_scope(scope, the=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except MissingGuild:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("You need to specify the Guild ID for the guild to lookup.")
|
||||||
|
)
|
||||||
|
|
||||||
track_objects = playlist.tracks_obj
|
if not await self.can_manage_playlist(scope, playlist, ctx, author, guild):
|
||||||
original_count = len(track_objects)
|
return
|
||||||
unique_tracks = set()
|
|
||||||
unique_tracks_add = unique_tracks.add
|
|
||||||
track_objects = [
|
|
||||||
x for x in track_objects if not (x in unique_tracks or unique_tracks_add(x))
|
|
||||||
]
|
|
||||||
|
|
||||||
tracklist = []
|
track_objects = playlist.tracks_obj
|
||||||
for track in track_objects:
|
original_count = len(track_objects)
|
||||||
track_keys = track._info.keys()
|
unique_tracks = set()
|
||||||
track_values = track._info.values()
|
unique_tracks_add = unique_tracks.add
|
||||||
track_id = track.track_identifier
|
track_objects = [
|
||||||
track_info = {}
|
x for x in track_objects if not (x in unique_tracks or unique_tracks_add(x))
|
||||||
for k, v in zip(track_keys, track_values):
|
]
|
||||||
track_info[k] = v
|
|
||||||
keys = ["track", "info"]
|
|
||||||
values = [track_id, track_info]
|
|
||||||
track_obj = {}
|
|
||||||
for key, value in zip(keys, values):
|
|
||||||
track_obj[key] = value
|
|
||||||
tracklist.append(track_obj)
|
|
||||||
|
|
||||||
final_count = len(tracklist)
|
tracklist = []
|
||||||
if original_count - final_count != 0:
|
for track in track_objects:
|
||||||
update = {"tracks": tracklist, "url": None}
|
track_keys = track._info.keys()
|
||||||
await playlist.edit(update)
|
track_values = track._info.values()
|
||||||
|
track_id = track.track_identifier
|
||||||
|
track_info = {}
|
||||||
|
for k, v in zip(track_keys, track_values):
|
||||||
|
track_info[k] = v
|
||||||
|
keys = ["track", "info"]
|
||||||
|
values = [track_id, track_info]
|
||||||
|
track_obj = {}
|
||||||
|
for key, value in zip(keys, values):
|
||||||
|
track_obj[key] = value
|
||||||
|
tracklist.append(track_obj)
|
||||||
|
|
||||||
if original_count - final_count != 0:
|
final_count = len(tracklist)
|
||||||
await self._embed_msg(
|
if original_count - final_count != 0:
|
||||||
ctx,
|
update = {"tracks": tracklist, "url": None}
|
||||||
_(
|
await playlist.edit(update)
|
||||||
"Removed {track_diff} duplicated "
|
|
||||||
"tracks from {name} (`{id}`) [**{scope}**] playlist."
|
if original_count - final_count != 0:
|
||||||
).format(
|
await self._embed_msg(
|
||||||
name=playlist.name,
|
ctx,
|
||||||
id=playlist.id,
|
_(
|
||||||
track_diff=original_count - final_count,
|
"Removed {track_diff} duplicated "
|
||||||
scope=scope_name,
|
"tracks from {name} (`{id}`) [**{scope}**] playlist."
|
||||||
),
|
).format(
|
||||||
)
|
name=playlist.name,
|
||||||
else:
|
id=playlist.id,
|
||||||
await self._embed_msg(
|
track_diff=original_count - final_count,
|
||||||
ctx,
|
scope=scope_name,
|
||||||
_("{name} (`{id}`) [**{scope}**] playlist has no duplicate tracks.").format(
|
),
|
||||||
name=playlist.name, id=playlist.id, scope=scope_name
|
)
|
||||||
),
|
return
|
||||||
)
|
else:
|
||||||
|
await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_("{name} (`{id}`) [**{scope}**] playlist has no duplicate tracks.").format(
|
||||||
|
name=playlist.name, id=playlist.id, scope=scope_name
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
@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]")
|
||||||
@ -3991,7 +4017,7 @@ class Audio(commands.Cog):
|
|||||||
spaces = "\N{EN SPACE}" * (len(str(len(playlist.tracks))) + 2)
|
spaces = "\N{EN SPACE}" * (len(str(len(playlist.tracks))) + 2)
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
track_idx = track_idx + 1
|
track_idx = track_idx + 1
|
||||||
query = dataclasses.Query.process_input(track["info"]["uri"])
|
query = audio_dataclasses.Query.process_input(track["info"]["uri"])
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if track["info"]["title"] != "Unknown title":
|
if track["info"]["title"] != "Unknown title":
|
||||||
msg += "`{}.` **{} - {}**\n{}{}\n".format(
|
msg += "`{}.` **{} - {}**\n{}{}\n".format(
|
||||||
@ -4396,7 +4422,7 @@ class Audio(commands.Cog):
|
|||||||
return
|
return
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
tracklist = await self._playlist_tracks(
|
tracklist = await self._playlist_tracks(
|
||||||
ctx, player, dataclasses.Query.process_input(playlist_url)
|
ctx, player, audio_dataclasses.Query.process_input(playlist_url)
|
||||||
)
|
)
|
||||||
if tracklist is not None:
|
if tracklist is not None:
|
||||||
playlist = await create_playlist(
|
playlist = await create_playlist(
|
||||||
@ -4486,14 +4512,14 @@ class Audio(commands.Cog):
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{track.title} {track.author} {track.uri} "
|
f"{track.title} {track.author} {track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(track))}"
|
f"{str(audio_dataclasses.Query.process_input(track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||||
continue
|
continue
|
||||||
query = dataclasses.Query.process_input(track.uri)
|
query = audio_dataclasses.Query.process_input(track.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
local_path = dataclasses.LocalPath(track.uri)
|
local_path = audio_dataclasses.LocalPath(track.uri)
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
pass
|
pass
|
||||||
if not local_path.exists() and not local_path.is_file():
|
if not local_path.exists() and not local_path.is_file():
|
||||||
@ -4779,7 +4805,7 @@ class Audio(commands.Cog):
|
|||||||
or not match_yt_playlist(uploaded_playlist_url)
|
or not match_yt_playlist(uploaded_playlist_url)
|
||||||
or not (
|
or not (
|
||||||
await self.music_cache.lavalink_query(
|
await self.music_cache.lavalink_query(
|
||||||
ctx, player, dataclasses.Query.process_input(uploaded_playlist_url)
|
ctx, player, audio_dataclasses.Query.process_input(uploaded_playlist_url)
|
||||||
)
|
)
|
||||||
)[0].tracks
|
)[0].tracks
|
||||||
):
|
):
|
||||||
@ -4964,7 +4990,7 @@ class Audio(commands.Cog):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if database_entries and HAS_SQL:
|
if database_entries and HAS_SQL:
|
||||||
asyncio.ensure_future(self.music_cache.insert("lavalink", database_entries))
|
await self.music_cache.insert("lavalink", database_entries)
|
||||||
|
|
||||||
async def _load_v2_playlist(
|
async def _load_v2_playlist(
|
||||||
self,
|
self,
|
||||||
@ -4991,7 +5017,7 @@ class Audio(commands.Cog):
|
|||||||
track_count += 1
|
track_count += 1
|
||||||
try:
|
try:
|
||||||
result, called_api = await self.music_cache.lavalink_query(
|
result, called_api = await self.music_cache.lavalink_query(
|
||||||
ctx, player, dataclasses.Query.process_input(song_url)
|
ctx, player, audio_dataclasses.Query.process_input(song_url)
|
||||||
)
|
)
|
||||||
track = result.tracks
|
track = result.tracks
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -5039,7 +5065,7 @@ class Audio(commands.Cog):
|
|||||||
return [], [], playlist
|
return [], [], playlist
|
||||||
results = {}
|
results = {}
|
||||||
updated_tracks = await self._playlist_tracks(
|
updated_tracks = await self._playlist_tracks(
|
||||||
ctx, player, dataclasses.Query.process_input(playlist.url)
|
ctx, player, audio_dataclasses.Query.process_input(playlist.url)
|
||||||
)
|
)
|
||||||
if not updated_tracks:
|
if not updated_tracks:
|
||||||
# No Tracks available on url Lets set it to none to avoid repeated calls here
|
# No Tracks available on url Lets set it to none to avoid repeated calls here
|
||||||
@ -5104,7 +5130,7 @@ class Audio(commands.Cog):
|
|||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
player: lavalink.player_manager.Player,
|
player: lavalink.player_manager.Player,
|
||||||
query: dataclasses.Query,
|
query: audio_dataclasses.Query,
|
||||||
):
|
):
|
||||||
search = query.is_search
|
search = query.is_search
|
||||||
tracklist = []
|
tracklist = []
|
||||||
@ -5173,7 +5199,7 @@ class Audio(commands.Cog):
|
|||||||
player.queue.insert(0, bump_song)
|
player.queue.insert(0, bump_song)
|
||||||
player.queue.pop(queue_len)
|
player.queue.pop(queue_len)
|
||||||
await player.skip()
|
await player.skip()
|
||||||
query = dataclasses.Query.process_input(player.current.uri)
|
query = audio_dataclasses.Query.process_input(player.current.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
|
|
||||||
if player.current.title == "Unknown title":
|
if player.current.title == "Unknown title":
|
||||||
@ -5225,7 +5251,7 @@ class Audio(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
dur = lavalink.utils.format_time(player.current.length)
|
dur = lavalink.utils.format_time(player.current.length)
|
||||||
|
|
||||||
query = dataclasses.Query.process_input(player.current)
|
query = audio_dataclasses.Query.process_input(player.current)
|
||||||
|
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if player.current.title != "Unknown title":
|
if player.current.title != "Unknown title":
|
||||||
@ -5238,8 +5264,8 @@ class Audio(commands.Cog):
|
|||||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||||
song = song.format(
|
song = song.format(
|
||||||
track=player.current,
|
track=player.current,
|
||||||
uri=dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
uri=audio_dataclasses.LocalPath(player.current.uri).to_string_hidden()
|
||||||
if dataclasses.Query.process_input(player.current.uri).is_local
|
if audio_dataclasses.Query.process_input(player.current.uri).is_local
|
||||||
else player.current.uri,
|
else player.current.uri,
|
||||||
arrow=arrow,
|
arrow=arrow,
|
||||||
pos=pos,
|
pos=pos,
|
||||||
@ -5311,7 +5337,7 @@ class Audio(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
dur = lavalink.utils.format_time(player.current.length)
|
dur = lavalink.utils.format_time(player.current.length)
|
||||||
|
|
||||||
query = dataclasses.Query.process_input(player.current)
|
query = audio_dataclasses.Query.process_input(player.current)
|
||||||
|
|
||||||
if query.is_stream:
|
if query.is_stream:
|
||||||
queue_list += _("**Currently livestreaming:**\n")
|
queue_list += _("**Currently livestreaming:**\n")
|
||||||
@ -5325,7 +5351,7 @@ class Audio(commands.Cog):
|
|||||||
(
|
(
|
||||||
_("Playing: ")
|
_("Playing: ")
|
||||||
+ "**{current.author} - {current.title}**".format(current=player.current),
|
+ "**{current.author} - {current.title}**".format(current=player.current),
|
||||||
dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
audio_dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
||||||
_("Requested by: **{user}**\n").format(user=player.current.requester),
|
_("Requested by: **{user}**\n").format(user=player.current.requester),
|
||||||
f"{arrow}`{pos}`/`{dur}`\n\n",
|
f"{arrow}`{pos}`/`{dur}`\n\n",
|
||||||
)
|
)
|
||||||
@ -5334,7 +5360,7 @@ class Audio(commands.Cog):
|
|||||||
queue_list += "\n".join(
|
queue_list += "\n".join(
|
||||||
(
|
(
|
||||||
_("Playing: ")
|
_("Playing: ")
|
||||||
+ dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
+ audio_dataclasses.LocalPath(player.current.uri).to_string_hidden(),
|
||||||
_("Requested by: **{user}**\n").format(user=player.current.requester),
|
_("Requested by: **{user}**\n").format(user=player.current.requester),
|
||||||
f"{arrow}`{pos}`/`{dur}`\n\n",
|
f"{arrow}`{pos}`/`{dur}`\n\n",
|
||||||
)
|
)
|
||||||
@ -5355,13 +5381,13 @@ class Audio(commands.Cog):
|
|||||||
track_title = track.title
|
track_title = track.title
|
||||||
req_user = track.requester
|
req_user = track.requester
|
||||||
track_idx = i + 1
|
track_idx = i + 1
|
||||||
query = dataclasses.Query.process_input(track)
|
query = audio_dataclasses.Query.process_input(track)
|
||||||
|
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
if track.title == "Unknown title":
|
if track.title == "Unknown title":
|
||||||
queue_list += f"`{track_idx}.` " + ", ".join(
|
queue_list += f"`{track_idx}.` " + ", ".join(
|
||||||
(
|
(
|
||||||
bold(dataclasses.LocalPath(track.uri).to_string_hidden()),
|
bold(audio_dataclasses.LocalPath(track.uri).to_string_hidden()),
|
||||||
_("requested by **{user}**\n").format(user=req_user),
|
_("requested by **{user}**\n").format(user=req_user),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -5418,7 +5444,7 @@ class Audio(commands.Cog):
|
|||||||
for track in queue_list:
|
for track in queue_list:
|
||||||
queue_idx = queue_idx + 1
|
queue_idx = queue_idx + 1
|
||||||
if not match_url(track.uri):
|
if not match_url(track.uri):
|
||||||
query = dataclasses.Query.process_input(track)
|
query = audio_dataclasses.Query.process_input(track)
|
||||||
if track.title == "Unknown title":
|
if track.title == "Unknown title":
|
||||||
track_title = query.track.to_string_hidden()
|
track_title = query.track.to_string_hidden()
|
||||||
else:
|
else:
|
||||||
@ -5447,7 +5473,7 @@ class Audio(commands.Cog):
|
|||||||
):
|
):
|
||||||
track_idx = i + 1
|
track_idx = i + 1
|
||||||
if type(track) is str:
|
if type(track) is str:
|
||||||
track_location = dataclasses.LocalPath(track).to_string_hidden()
|
track_location = audio_dataclasses.LocalPath(track).to_string_hidden()
|
||||||
track_match += "`{}.` **{}**\n".format(track_idx, track_location)
|
track_match += "`{}.` **{}**\n".format(track_idx, track_location)
|
||||||
else:
|
else:
|
||||||
track_match += "`{}.` **{}**\n".format(track[0], track[1])
|
track_match += "`{}.` **{}**\n".format(track[0], track[1])
|
||||||
@ -5672,9 +5698,9 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
index -= 1
|
index -= 1
|
||||||
removed = player.queue.pop(index)
|
removed = player.queue.pop(index)
|
||||||
query = dataclasses.Query.process_input(removed.uri)
|
query = audio_dataclasses.Query.process_input(removed.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
local_path = dataclasses.LocalPath(removed.uri).to_string_hidden()
|
local_path = audio_dataclasses.LocalPath(removed.uri).to_string_hidden()
|
||||||
if removed.title == "Unknown title":
|
if removed.title == "Unknown title":
|
||||||
removed_title = local_path
|
removed_title = local_path
|
||||||
else:
|
else:
|
||||||
@ -5760,7 +5786,7 @@ class Audio(commands.Cog):
|
|||||||
await self._data_check(ctx)
|
await self._data_check(ctx)
|
||||||
|
|
||||||
if not isinstance(query, list):
|
if not isinstance(query, list):
|
||||||
query = dataclasses.Query.process_input(query)
|
query = audio_dataclasses.Query.process_input(query)
|
||||||
if query.invoked_from == "search list" or query.invoked_from == "local folder":
|
if query.invoked_from == "search list" or query.invoked_from == "local folder":
|
||||||
if query.invoked_from == "search list":
|
if query.invoked_from == "search list":
|
||||||
result, called_api = await self.music_cache.lavalink_query(ctx, player, query)
|
result, called_api = await self.music_cache.lavalink_query(ctx, player, query)
|
||||||
@ -5789,7 +5815,7 @@ class Audio(commands.Cog):
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{track.title} {track.author} {track.uri} "
|
f"{track.title} {track.author} {track.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(track))}"
|
f"{str(audio_dataclasses.Query.process_input(track))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||||
@ -5903,10 +5929,10 @@ class Audio(commands.Cog):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
search_choice = tracks[-1]
|
search_choice = tracks[-1]
|
||||||
try:
|
try:
|
||||||
query = dataclasses.Query.process_input(search_choice.uri)
|
query = audio_dataclasses.Query.process_input(search_choice.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
|
|
||||||
localtrack = dataclasses.LocalPath(search_choice.uri)
|
localtrack = audio_dataclasses.LocalPath(search_choice.uri)
|
||||||
if search_choice.title != "Unknown title":
|
if search_choice.title != "Unknown title":
|
||||||
description = "**{} - {}**\n{}".format(
|
description = "**{} - {}**\n{}".format(
|
||||||
search_choice.author, search_choice.title, localtrack.to_string_hidden()
|
search_choice.author, search_choice.title, localtrack.to_string_hidden()
|
||||||
@ -5917,7 +5943,7 @@ class Audio(commands.Cog):
|
|||||||
description = "**[{}]({})**".format(search_choice.title, search_choice.uri)
|
description = "**[{}]({})**".format(search_choice.title, search_choice.uri)
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
search_choice = dataclasses.Query.process_input(search_choice)
|
search_choice = audio_dataclasses.Query.process_input(search_choice)
|
||||||
if search_choice.track.exists() and search_choice.track.is_dir():
|
if search_choice.track.exists() and search_choice.track.is_dir():
|
||||||
return await ctx.invoke(self.search, query=search_choice)
|
return await ctx.invoke(self.search, query=search_choice)
|
||||||
elif search_choice.track.exists() and search_choice.track.is_file():
|
elif search_choice.track.exists() and search_choice.track.is_file():
|
||||||
@ -5933,7 +5959,7 @@ class Audio(commands.Cog):
|
|||||||
ctx.guild,
|
ctx.guild,
|
||||||
(
|
(
|
||||||
f"{search_choice.title} {search_choice.author} {search_choice.uri} "
|
f"{search_choice.title} {search_choice.author} {search_choice.uri} "
|
||||||
f"{str(dataclasses.Query.process_input(search_choice))}"
|
f"{str(audio_dataclasses.Query.process_input(search_choice))}"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||||
@ -5982,12 +6008,12 @@ class Audio(commands.Cog):
|
|||||||
if search_track_num == 0:
|
if search_track_num == 0:
|
||||||
search_track_num = 5
|
search_track_num = 5
|
||||||
try:
|
try:
|
||||||
query = dataclasses.Query.process_input(track.uri)
|
query = audio_dataclasses.Query.process_input(track.uri)
|
||||||
if query.is_local:
|
if query.is_local:
|
||||||
search_list += "`{0}.` **{1}**\n[{2}]\n".format(
|
search_list += "`{0}.` **{1}**\n[{2}]\n".format(
|
||||||
search_track_num,
|
search_track_num,
|
||||||
track.title,
|
track.title,
|
||||||
dataclasses.LocalPath(track.uri).to_string_hidden(),
|
audio_dataclasses.LocalPath(track.uri).to_string_hidden(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
search_list += "`{0}.` **[{1}]({2})**\n".format(
|
search_list += "`{0}.` **[{1}]({2})**\n".format(
|
||||||
@ -5995,7 +6021,7 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# query = Query.process_input(track)
|
# query = Query.process_input(track)
|
||||||
track = dataclasses.Query.process_input(track)
|
track = audio_dataclasses.Query.process_input(track)
|
||||||
if track.is_local and command != "search":
|
if track.is_local and command != "search":
|
||||||
search_list += "`{}.` **{}**\n".format(
|
search_list += "`{}.` **{}**\n".format(
|
||||||
search_track_num, track.to_string_user()
|
search_track_num, track.to_string_user()
|
||||||
@ -6717,7 +6743,9 @@ class Audio(commands.Cog):
|
|||||||
if (time.time() - stop_times[sid]) >= emptydc_timer:
|
if (time.time() - stop_times[sid]) >= emptydc_timer:
|
||||||
stop_times.pop(sid)
|
stop_times.pop(sid)
|
||||||
try:
|
try:
|
||||||
await lavalink.get_player(sid).disconnect()
|
player = lavalink.get_player(sid)
|
||||||
|
await player.stop()
|
||||||
|
await player.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error("Exception raised in Audio's emptydc_timer.", exc_info=True)
|
log.error("Exception raised in Audio's emptydc_timer.", exc_info=True)
|
||||||
pass
|
pass
|
||||||
@ -6888,6 +6916,7 @@ class Audio(commands.Cog):
|
|||||||
async def on_voice_state_update(
|
async def on_voice_state_update(
|
||||||
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
||||||
):
|
):
|
||||||
|
await self._ready_event.wait()
|
||||||
if after.channel != before.channel:
|
if after.channel != before.channel:
|
||||||
try:
|
try:
|
||||||
self.skip_votes[before.channel.guild].remove(member.id)
|
self.skip_votes[before.channel.guild].remove(member.id)
|
||||||
@ -6905,6 +6934,9 @@ class Audio(commands.Cog):
|
|||||||
if self._connect_task:
|
if self._connect_task:
|
||||||
self._connect_task.cancel()
|
self._connect_task.cancel()
|
||||||
|
|
||||||
|
if self._init_task:
|
||||||
|
self._init_task.cancel()
|
||||||
|
|
||||||
lavalink.unregister_event_listener(self.event_handler)
|
lavalink.unregister_event_listener(self.event_handler)
|
||||||
self.bot.loop.create_task(lavalink.close())
|
self.bot.loop.create_task(lavalink.close())
|
||||||
if self._manager is not None:
|
if self._manager is not None:
|
||||||
|
|||||||
@ -381,14 +381,17 @@ class Query:
|
|||||||
match = re.search(_re_youtube_index, track)
|
match = re.search(_re_youtube_index, track)
|
||||||
if match:
|
if match:
|
||||||
returning["track_index"] = int(match.group(1)) - 1
|
returning["track_index"] = int(match.group(1)) - 1
|
||||||
|
|
||||||
if all(k in track for k in ["&list=", "watch?"]):
|
if all(k in track for k in ["&list=", "watch?"]):
|
||||||
returning["track_index"] = 0
|
returning["track_index"] = 0
|
||||||
returning["playlist"] = True
|
returning["playlist"] = True
|
||||||
returning["single"] = False
|
returning["single"] = False
|
||||||
elif all(x in track for x in ["playlist?"]):
|
elif all(x in track for x in ["playlist?"]):
|
||||||
returning["playlist"] = True if not _has_index else False
|
returning["playlist"] = not _has_index
|
||||||
returning["single"] = True if _has_index else False
|
returning["single"] = _has_index
|
||||||
|
elif any(k in track for k in ["list="]):
|
||||||
|
returning["track_index"] = 0
|
||||||
|
returning["playlist"] = True
|
||||||
|
returning["single"] = False
|
||||||
else:
|
else:
|
||||||
returning["single"] = True
|
returning["single"] = True
|
||||||
elif url_domain == "spotify.com":
|
elif url_domain == "spotify.com":
|
||||||
@ -18,7 +18,7 @@ from redbot.core import data_manager
|
|||||||
from .errors import LavalinkDownloadFailed
|
from .errors import LavalinkDownloadFailed
|
||||||
|
|
||||||
JAR_VERSION = "3.2.1"
|
JAR_VERSION = "3.2.1"
|
||||||
JAR_BUILD = 823
|
JAR_BUILD = 846
|
||||||
LAVALINK_DOWNLOAD_URL = (
|
LAVALINK_DOWNLOAD_URL = (
|
||||||
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
||||||
f"Lavalink.jar"
|
f"Lavalink.jar"
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import contextlib
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import NoReturn
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@ -11,7 +10,7 @@ import lavalink
|
|||||||
|
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from . import dataclasses
|
from . import audio_dataclasses
|
||||||
|
|
||||||
from .converters import _pass_config_to_converters
|
from .converters import _pass_config_to_converters
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ def pass_config_to_dependencies(config: Config, bot: Red, localtracks_folder: st
|
|||||||
_config = config
|
_config = config
|
||||||
_pass_config_to_playlist(config, bot)
|
_pass_config_to_playlist(config, bot)
|
||||||
_pass_config_to_converters(config, bot)
|
_pass_config_to_converters(config, bot)
|
||||||
dataclasses._pass_config_to_dataclasses(config, bot, localtracks_folder)
|
audio_dataclasses._pass_config_to_dataclasses(config, bot, localtracks_folder)
|
||||||
|
|
||||||
|
|
||||||
def track_limit(track, maxlength):
|
def track_limit(track, maxlength):
|
||||||
@ -168,7 +167,7 @@ async def clear_react(bot: Red, message: discord.Message, emoji: dict = None):
|
|||||||
|
|
||||||
async def get_description(track):
|
async def get_description(track):
|
||||||
if any(x in track.uri for x in [f"{os.sep}localtracks", f"localtracks{os.sep}"]):
|
if any(x in track.uri for x in [f"{os.sep}localtracks", f"localtracks{os.sep}"]):
|
||||||
local_track = dataclasses.LocalPath(track.uri)
|
local_track = audio_dataclasses.LocalPath(track.uri)
|
||||||
if track.title != "Unknown title":
|
if track.title != "Unknown title":
|
||||||
return "**{} - {}**\n{}".format(
|
return "**{} - {}**\n{}".format(
|
||||||
track.author, track.title, local_track.to_string_hidden()
|
track.author, track.title, local_track.to_string_hidden()
|
||||||
@ -389,7 +388,7 @@ class Notifier:
|
|||||||
key: str = None,
|
key: str = None,
|
||||||
seconds_key: str = None,
|
seconds_key: str = None,
|
||||||
seconds: str = None,
|
seconds: str = None,
|
||||||
) -> NoReturn:
|
):
|
||||||
"""
|
"""
|
||||||
This updates an existing message.
|
This updates an existing message.
|
||||||
Based on the message found in :variable:`Notifier.updates` as per the `key` param
|
Based on the message found in :variable:`Notifier.updates` as per the `key` param
|
||||||
@ -410,14 +409,14 @@ class Notifier:
|
|||||||
except discord.errors.NotFound:
|
except discord.errors.NotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def update_text(self, text: str) -> NoReturn:
|
async def update_text(self, text: str):
|
||||||
embed2 = discord.Embed(colour=self.color, title=text)
|
embed2 = discord.Embed(colour=self.color, title=text)
|
||||||
try:
|
try:
|
||||||
await self.message.edit(embed=embed2)
|
await self.message.edit(embed=embed2)
|
||||||
except discord.errors.NotFound:
|
except discord.errors.NotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def update_embed(self, embed: discord.Embed) -> NoReturn:
|
async def update_embed(self, embed: discord.Embed):
|
||||||
try:
|
try:
|
||||||
await self.message.edit(embed=embed)
|
await self.message.edit(embed=embed)
|
||||||
self.last_msg_time = time.time()
|
self.last_msg_time = time.time()
|
||||||
|
|||||||
@ -21,7 +21,7 @@ REPO_INSTALL_MSG = _(
|
|||||||
_ = T_
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
async def do_install_agreement(ctx: commands.Context):
|
async def do_install_agreement(ctx: commands.Context) -> bool:
|
||||||
downloader = ctx.cog
|
downloader = ctx.cog
|
||||||
if downloader is None or downloader.already_agreed:
|
if downloader is None or downloader.already_agreed:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
from .installable import Installable
|
from .installable import InstalledModule
|
||||||
|
|
||||||
_ = Translator("Koala", __file__)
|
_ = Translator("Koala", __file__)
|
||||||
|
|
||||||
|
|
||||||
class InstalledCog(Installable):
|
class InstalledCog(InstalledModule):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def convert(cls, ctx: commands.Context, arg: str) -> Installable:
|
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
downloader = ctx.bot.get_cog("Downloader")
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
raise commands.CommandError(_("No Downloader cog found."))
|
raise commands.CommandError(_("No Downloader cog found."))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .repo_manager import Candidate
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DownloaderException",
|
"DownloaderException",
|
||||||
"GitException",
|
"GitException",
|
||||||
"InvalidRepoName",
|
"InvalidRepoName",
|
||||||
|
"CopyingError",
|
||||||
"ExistingGitRepo",
|
"ExistingGitRepo",
|
||||||
"MissingGitRepo",
|
"MissingGitRepo",
|
||||||
"CloningError",
|
"CloningError",
|
||||||
@ -10,6 +19,8 @@ __all__ = [
|
|||||||
"UpdateError",
|
"UpdateError",
|
||||||
"GitDiffError",
|
"GitDiffError",
|
||||||
"NoRemoteURL",
|
"NoRemoteURL",
|
||||||
|
"UnknownRevision",
|
||||||
|
"AmbiguousRevision",
|
||||||
"PipError",
|
"PipError",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -37,6 +48,15 @@ class InvalidRepoName(DownloaderException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CopyingError(DownloaderException):
|
||||||
|
"""
|
||||||
|
Throw when there was an issue
|
||||||
|
during copying of module's files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExistingGitRepo(DownloaderException):
|
class ExistingGitRepo(DownloaderException):
|
||||||
"""
|
"""
|
||||||
Thrown when trying to clone into a folder where a
|
Thrown when trying to clone into a folder where a
|
||||||
@ -105,6 +125,24 @@ class NoRemoteURL(GitException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownRevision(GitException):
|
||||||
|
"""
|
||||||
|
Thrown when specified revision cannot be found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AmbiguousRevision(GitException):
|
||||||
|
"""
|
||||||
|
Thrown when specified revision is ambiguous.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, candidates: List[Candidate]) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.candidates = candidates
|
||||||
|
|
||||||
|
|
||||||
class PipError(DownloaderException):
|
class PipError(DownloaderException):
|
||||||
"""
|
"""
|
||||||
Thrown when pip returns a non-zero return code.
|
Thrown when pip returns a non-zero return code.
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import distutils.dir_util
|
import distutils.dir_util
|
||||||
import shutil
|
import shutil
|
||||||
from enum import Enum
|
from enum import IntEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import MutableMapping, Any, TYPE_CHECKING
|
from typing import MutableMapping, Any, TYPE_CHECKING, Optional, Dict, Union, Callable, Tuple, cast
|
||||||
|
|
||||||
from .log import log
|
from .log import log
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
@ -11,10 +13,11 @@ from .json_mixins import RepoJSONMixin
|
|||||||
from redbot.core import __version__, version_info as red_version_info, VersionInfo
|
from redbot.core import __version__, version_info as red_version_info, VersionInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .repo_manager import RepoManager
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
|
|
||||||
class InstallableType(Enum):
|
class InstallableType(IntEnum):
|
||||||
|
# using IntEnum, because hot-reload breaks its identity
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
COG = 1
|
COG = 1
|
||||||
SHARED_LIBRARY = 2
|
SHARED_LIBRARY = 2
|
||||||
@ -34,6 +37,10 @@ class Installable(RepoJSONMixin):
|
|||||||
----------
|
----------
|
||||||
repo_name : `str`
|
repo_name : `str`
|
||||||
Name of the repository which this package belongs to.
|
Name of the repository which this package belongs to.
|
||||||
|
repo : Repo, optional
|
||||||
|
Repo object of the Installable, if repo is missing this will be `None`
|
||||||
|
commit : `str`, optional
|
||||||
|
Installable's commit. This is not the same as ``repo.commit``
|
||||||
author : `tuple` of `str`, optional
|
author : `tuple` of `str`, optional
|
||||||
Name(s) of the author(s).
|
Name(s) of the author(s).
|
||||||
bot_version : `tuple` of `int`
|
bot_version : `tuple` of `int`
|
||||||
@ -58,30 +65,36 @@ class Installable(RepoJSONMixin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, location: Path):
|
def __init__(self, location: Path, repo: Optional[Repo] = None, commit: str = ""):
|
||||||
"""Base installable initializer.
|
"""Base installable initializer.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
location : pathlib.Path
|
location : pathlib.Path
|
||||||
Location (file or folder) to the installable.
|
Location (file or folder) to the installable.
|
||||||
|
repo : Repo, optional
|
||||||
|
Repo object of the Installable, if repo is missing this will be `None`
|
||||||
|
commit : str
|
||||||
|
Installable's commit. This is not the same as ``repo.commit``
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super().__init__(location)
|
super().__init__(location)
|
||||||
|
|
||||||
self._location = location
|
self._location = location
|
||||||
|
|
||||||
|
self.repo = repo
|
||||||
self.repo_name = self._location.parent.stem
|
self.repo_name = self._location.parent.stem
|
||||||
|
self.commit = commit
|
||||||
|
|
||||||
self.author = ()
|
self.author: Tuple[str, ...] = ()
|
||||||
self.min_bot_version = red_version_info
|
self.min_bot_version = red_version_info
|
||||||
self.max_bot_version = red_version_info
|
self.max_bot_version = red_version_info
|
||||||
self.min_python_version = (3, 5, 1)
|
self.min_python_version = (3, 5, 1)
|
||||||
self.hidden = False
|
self.hidden = False
|
||||||
self.disabled = False
|
self.disabled = False
|
||||||
self.required_cogs = {} # Cog name -> repo URL
|
self.required_cogs: Dict[str, str] = {} # Cog name -> repo URL
|
||||||
self.requirements = ()
|
self.requirements: Tuple[str, ...] = ()
|
||||||
self.tags = ()
|
self.tags: Tuple[str, ...] = ()
|
||||||
self.type = InstallableType.UNKNOWN
|
self.type = InstallableType.UNKNOWN
|
||||||
|
|
||||||
if self._info_file.exists():
|
if self._info_file.exists():
|
||||||
@ -90,15 +103,15 @@ class Installable(RepoJSONMixin):
|
|||||||
if self._info == {}:
|
if self._info == {}:
|
||||||
self.type = InstallableType.COG
|
self.type = InstallableType.COG
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: Any) -> bool:
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
return self._location == other._location
|
return self._location == other._location
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(self._location)
|
return hash(self._location)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""`str` : The name of this package."""
|
"""`str` : The name of this package."""
|
||||||
return self._location.stem
|
return self._location.stem
|
||||||
|
|
||||||
@ -111,6 +124,7 @@ class Installable(RepoJSONMixin):
|
|||||||
:return: Status of installation
|
:return: Status of installation
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
|
copy_func: Callable[..., Any]
|
||||||
if self._location.is_file():
|
if self._location.is_file():
|
||||||
copy_func = shutil.copy2
|
copy_func = shutil.copy2
|
||||||
else:
|
else:
|
||||||
@ -121,18 +135,20 @@ class Installable(RepoJSONMixin):
|
|||||||
# 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:
|
except: # noqa: E722
|
||||||
log.exception("Error occurred when copying path: {}".format(self._location))
|
log.exception("Error occurred when copying path: {}".format(self._location))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _read_info_file(self):
|
def _read_info_file(self) -> None:
|
||||||
super()._read_info_file()
|
super()._read_info_file()
|
||||||
|
|
||||||
if self._info_file.exists():
|
if self._info_file.exists():
|
||||||
self._process_info_file()
|
self._process_info_file()
|
||||||
|
|
||||||
def _process_info_file(self, info_file_path: Path = None) -> MutableMapping[str, Any]:
|
def _process_info_file(
|
||||||
|
self, info_file_path: Optional[Path] = None
|
||||||
|
) -> MutableMapping[str, Any]:
|
||||||
"""
|
"""
|
||||||
Processes an information file. Loads dependencies among other
|
Processes an information file. Loads dependencies among other
|
||||||
information into this object.
|
information into this object.
|
||||||
@ -145,7 +161,7 @@ class Installable(RepoJSONMixin):
|
|||||||
if info_file_path is None or not info_file_path.is_file():
|
if info_file_path is None or not info_file_path.is_file():
|
||||||
raise ValueError("No valid information file path was found.")
|
raise ValueError("No valid information file path was found.")
|
||||||
|
|
||||||
info = {}
|
info: Dict[str, Any] = {}
|
||||||
with info_file_path.open(encoding="utf-8") as f:
|
with info_file_path.open(encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
@ -174,7 +190,7 @@ class Installable(RepoJSONMixin):
|
|||||||
self.max_bot_version = max_bot_version
|
self.max_bot_version = max_bot_version
|
||||||
|
|
||||||
try:
|
try:
|
||||||
min_python_version = tuple(info.get("min_python_version", [3, 5, 1]))
|
min_python_version = tuple(info.get("min_python_version", (3, 5, 1)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
min_python_version = self.min_python_version
|
min_python_version = self.min_python_version
|
||||||
self.min_python_version = min_python_version
|
self.min_python_version = min_python_version
|
||||||
@ -212,14 +228,51 @@ class Installable(RepoJSONMixin):
|
|||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
return {"repo_name": self.repo_name, "cog_name": self.name}
|
class InstalledModule(Installable):
|
||||||
|
"""Base class for installed modules,
|
||||||
|
this is basically instance of installed `Installable`
|
||||||
|
used by Downloader.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
pinned : `bool`
|
||||||
|
Whether or not this cog is pinned, always `False` if module is not a cog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
location: Path,
|
||||||
|
repo: Optional[Repo] = None,
|
||||||
|
commit: str = "",
|
||||||
|
pinned: bool = False,
|
||||||
|
json_repo_name: str = "",
|
||||||
|
):
|
||||||
|
super().__init__(location=location, repo=repo, commit=commit)
|
||||||
|
self.pinned: bool = pinned if self.type == InstallableType.COG else False
|
||||||
|
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
|
||||||
|
self._json_repo_name = json_repo_name
|
||||||
|
|
||||||
|
def to_json(self) -> Dict[str, Union[str, bool]]:
|
||||||
|
module_json: Dict[str, Union[str, bool]] = {
|
||||||
|
"repo_name": self.repo_name,
|
||||||
|
"module_name": self.name,
|
||||||
|
"commit": self.commit,
|
||||||
|
}
|
||||||
|
if self.type == InstallableType.COG:
|
||||||
|
module_json["pinned"] = self.pinned
|
||||||
|
return module_json
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data: dict, repo_mgr: "RepoManager"):
|
def from_json(
|
||||||
repo_name = data["repo_name"]
|
cls, data: Dict[str, Union[str, bool]], repo_mgr: RepoManager
|
||||||
cog_name = data["cog_name"]
|
) -> InstalledModule:
|
||||||
|
repo_name = cast(str, data["repo_name"])
|
||||||
|
cog_name = cast(str, data["module_name"])
|
||||||
|
commit = cast(str, data.get("commit", ""))
|
||||||
|
pinned = cast(bool, data.get("pinned", False))
|
||||||
|
|
||||||
|
# TypedDict, where are you :/
|
||||||
repo = repo_mgr.get_repo(repo_name)
|
repo = repo_mgr.get_repo(repo_name)
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
repo_folder = repo.folder_path
|
repo_folder = repo.folder_path
|
||||||
@ -228,4 +281,12 @@ class Installable(RepoJSONMixin):
|
|||||||
|
|
||||||
location = repo_folder / cog_name
|
location = repo_folder / cog_name
|
||||||
|
|
||||||
return cls(location=location)
|
return cls(
|
||||||
|
location=location, repo=repo, commit=commit, pinned=pinned, json_repo_name=repo_name
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_installable(cls, module: Installable, *, pinned: bool = False) -> InstalledModule:
|
||||||
|
return cls(
|
||||||
|
location=module._location, repo=module.repo, commit=module.commit, pinned=pinned
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class RepoJSONMixin:
|
class RepoJSONMixin:
|
||||||
@ -8,18 +9,18 @@ class RepoJSONMixin:
|
|||||||
def __init__(self, repo_folder: Path):
|
def __init__(self, repo_folder: Path):
|
||||||
self._repo_folder = repo_folder
|
self._repo_folder = repo_folder
|
||||||
|
|
||||||
self.author = None
|
self.author: Optional[Tuple[str, ...]] = None
|
||||||
self.install_msg = None
|
self.install_msg: Optional[str] = None
|
||||||
self.short = None
|
self.short: Optional[str] = None
|
||||||
self.description = None
|
self.description: Optional[str] = None
|
||||||
|
|
||||||
self._info_file = repo_folder / self.INFO_FILE_NAME
|
self._info_file = repo_folder / self.INFO_FILE_NAME
|
||||||
if self._info_file.exists():
|
if self._info_file.exists():
|
||||||
self._read_info_file()
|
self._read_info_file()
|
||||||
|
|
||||||
self._info = {}
|
self._info: Dict[str, Any] = {}
|
||||||
|
|
||||||
def _read_info_file(self):
|
def _read_info_file(self) -> None:
|
||||||
if not (self._info_file.exists() or self._info_file.is_file()):
|
if not (self._info_file.exists() or self._info_file.is_file()):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -299,6 +299,14 @@ class Permissions(commands.Cog):
|
|||||||
if not who_or_what:
|
if not who_or_what:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"This command is designated as being always available and "
|
||||||
|
"cannot be modified by permission rules."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
for w in who_or_what:
|
for w in who_or_what:
|
||||||
await self._add_rule(
|
await self._add_rule(
|
||||||
rule=cast(bool, allow_or_deny),
|
rule=cast(bool, allow_or_deny),
|
||||||
@ -334,6 +342,14 @@ class Permissions(commands.Cog):
|
|||||||
if not who_or_what:
|
if not who_or_what:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
if isinstance(cog_or_command.obj, commands.commands._AlwaysAvailableCommand):
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"This command is designated as being always available and "
|
||||||
|
"cannot be modified by permission rules."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
for w in who_or_what:
|
for w in who_or_what:
|
||||||
await self._add_rule(
|
await self._add_rule(
|
||||||
rule=cast(bool, allow_or_deny),
|
rule=cast(bool, allow_or_deny),
|
||||||
@ -544,7 +560,7 @@ class Permissions(commands.Cog):
|
|||||||
|
|
||||||
Handles config.
|
Handles config.
|
||||||
"""
|
"""
|
||||||
self.bot.clear_permission_rules(guild_id)
|
self.bot.clear_permission_rules(guild_id, preserve_default_rule=False)
|
||||||
for category in (COG, COMMAND):
|
for category in (COG, COMMAND):
|
||||||
async with self.config.custom(category).all() as all_rules:
|
async with self.config.custom(category).all() as all_rules:
|
||||||
for name, rules in all_rules.items():
|
for name, rules in all_rules.items():
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from enum import Enum
|
|||||||
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
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
@ -132,7 +133,6 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
self._main_dir = bot_dir
|
self._main_dir = bot_dir
|
||||||
self._cog_mgr = CogManager()
|
self._cog_mgr = CogManager()
|
||||||
|
|
||||||
super().__init__(*args, help_command=None, **kwargs)
|
super().__init__(*args, help_command=None, **kwargs)
|
||||||
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
||||||
# for a documented API. The internals of this object are still subject to change.
|
# for a documented API. The internals of this object are still subject to change.
|
||||||
@ -325,6 +325,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
get_embed_colour = get_embed_color
|
get_embed_colour = get_embed_color
|
||||||
|
|
||||||
|
# start config migrations
|
||||||
async def _maybe_update_config(self):
|
async def _maybe_update_config(self):
|
||||||
"""
|
"""
|
||||||
This should be run prior to loading cogs or connecting to discord.
|
This should be run prior to loading cogs or connecting to discord.
|
||||||
@ -375,6 +376,57 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
await self._config.guild(guild_obj).admin_role.set(admin_roles)
|
await self._config.guild(guild_obj).admin_role.set(admin_roles)
|
||||||
log.info("Done updating guild configs to support multiple mod/admin roles")
|
log.info("Done updating guild configs to support multiple mod/admin roles")
|
||||||
|
|
||||||
|
# end Config migrations
|
||||||
|
|
||||||
|
async def pre_flight(self, cli_flags):
|
||||||
|
"""
|
||||||
|
This should only be run once, prior to connecting to discord.
|
||||||
|
"""
|
||||||
|
await self._maybe_update_config()
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
|
||||||
|
if cli_flags.no_cogs is False:
|
||||||
|
packages.extend(await self._config.packages())
|
||||||
|
|
||||||
|
if cli_flags.load_cogs:
|
||||||
|
packages.extend(cli_flags.load_cogs)
|
||||||
|
|
||||||
|
if packages:
|
||||||
|
# Load permissions first, for security reasons
|
||||||
|
try:
|
||||||
|
packages.remove("permissions")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
packages.insert(0, "permissions")
|
||||||
|
|
||||||
|
to_remove = []
|
||||||
|
print("Loading packages...")
|
||||||
|
for package in packages:
|
||||||
|
try:
|
||||||
|
spec = await self._cog_mgr.find_cog(package)
|
||||||
|
await asyncio.wait_for(self.load_extension(spec), 30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
log.exception("Failed to load package %s (timeout)", package)
|
||||||
|
to_remove.append(package)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Failed to load package {}".format(package), exc_info=e)
|
||||||
|
await self.remove_loaded_package(package)
|
||||||
|
to_remove.append(package)
|
||||||
|
for package in to_remove:
|
||||||
|
packages.remove(package)
|
||||||
|
if packages:
|
||||||
|
print("Loaded packages: " + ", ".join(packages))
|
||||||
|
|
||||||
|
if self.rpc_enabled:
|
||||||
|
await self.rpc.initialize(self.rpc_port)
|
||||||
|
|
||||||
|
async def start(self, *args, **kwargs):
|
||||||
|
cli_flags = kwargs.pop("cli_flags")
|
||||||
|
await self.pre_flight(cli_flags=cli_flags)
|
||||||
|
return await super().start(*args, **kwargs)
|
||||||
|
|
||||||
async def send_help_for(
|
async def send_help_for(
|
||||||
self, ctx: commands.Context, help_for: Union[commands.Command, commands.GroupMixin, str]
|
self, ctx: commands.Context, help_for: Union[commands.Command, commands.GroupMixin, str]
|
||||||
):
|
):
|
||||||
@ -531,6 +583,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
|
|
||||||
async with self._config.custom(SHARED_API_TOKENS, service_name).all() as group:
|
async with self._config.custom(SHARED_API_TOKENS, service_name).all() as group:
|
||||||
group.update(tokens)
|
group.update(tokens)
|
||||||
|
self.dispatch("red_api_tokens_update", service_name, MappingProxyType(group))
|
||||||
|
|
||||||
async def remove_shared_api_tokens(self, service_name: str, *token_names: str):
|
async def remove_shared_api_tokens(self, service_name: str, *token_names: str):
|
||||||
"""
|
"""
|
||||||
@ -653,7 +706,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
``True`` if immune
|
``True`` if immune
|
||||||
|
|
||||||
"""
|
"""
|
||||||
guild = to_check.guild
|
guild = getattr(to_check, "guild", None)
|
||||||
if not guild:
|
if not guild:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -666,7 +719,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# webhook messages are a user not member,
|
# webhook messages are a user not member,
|
||||||
# cheaper than isinstance
|
# cheaper than isinstance
|
||||||
return True # webhooks require significant permissions to enable.
|
if author.bot and author.discriminator == "0000":
|
||||||
|
return True # webhooks require significant permissions to enable.
|
||||||
else:
|
else:
|
||||||
ids_to_check.append(author.id)
|
ids_to_check.append(author.id)
|
||||||
|
|
||||||
@ -779,7 +833,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
for subcommand in set(command.walk_commands()):
|
for subcommand in set(command.walk_commands()):
|
||||||
subcommand.requires.reset()
|
subcommand.requires.reset()
|
||||||
|
|
||||||
def clear_permission_rules(self, guild_id: Optional[int]) -> None:
|
def clear_permission_rules(self, guild_id: Optional[int], **kwargs) -> None:
|
||||||
"""Clear all permission overrides in a scope.
|
"""Clear all permission overrides in a scope.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -789,11 +843,15 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d
|
|||||||
``None``, this will clear all global rules and leave all
|
``None``, this will clear all global rules and leave all
|
||||||
guild rules untouched.
|
guild rules untouched.
|
||||||
|
|
||||||
|
**kwargs
|
||||||
|
Keyword arguments to be passed to each required call of
|
||||||
|
``commands.Requires.clear_all_rules``
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for cog in self.cogs.values():
|
for cog in self.cogs.values():
|
||||||
cog.requires.clear_all_rules(guild_id)
|
cog.requires.clear_all_rules(guild_id, **kwargs)
|
||||||
for command in self.walk_commands():
|
for command in self.walk_commands():
|
||||||
command.requires.clear_all_rules(guild_id)
|
command.requires.clear_all_rules(guild_id, **kwargs)
|
||||||
|
|
||||||
def add_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
def add_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
||||||
"""Add a permissions hook.
|
"""Add a permissions hook.
|
||||||
|
|||||||
@ -1,17 +1,42 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def confirm(m=""):
|
def confirm(text: str, default: Optional[bool] = None) -> bool:
|
||||||
return input(m).lower().strip() in ("y", "yes")
|
if default is None:
|
||||||
|
options = "y/n"
|
||||||
|
elif default is True:
|
||||||
|
options = "Y/n"
|
||||||
|
elif default is False:
|
||||||
|
options = "y/N"
|
||||||
|
else:
|
||||||
|
raise TypeError(f"expected bool, not {type(default)}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
value = input(f"{text}: [{options}] ").lower().strip()
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print("\nAborted!")
|
||||||
|
sys.exit(1)
|
||||||
|
if value in ("y", "yes"):
|
||||||
|
return True
|
||||||
|
if value in ("n", "no"):
|
||||||
|
return False
|
||||||
|
if value == "":
|
||||||
|
if default is not None:
|
||||||
|
return default
|
||||||
|
print("Error: invalid input")
|
||||||
|
|
||||||
|
|
||||||
def interactive_config(red, token_set, prefix_set):
|
def interactive_config(red, token_set, prefix_set, *, print_header=True):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
token = ""
|
token = ""
|
||||||
|
|
||||||
print("Red - Discord Bot | Configuration process\n")
|
if print_header:
|
||||||
|
print("Red - Discord Bot | Configuration process\n")
|
||||||
|
|
||||||
if not token_set:
|
if not token_set:
|
||||||
print("Please enter a valid token:")
|
print("Please enter a valid token:")
|
||||||
@ -35,8 +60,7 @@ def interactive_config(red, token_set, prefix_set):
|
|||||||
while not prefix:
|
while not prefix:
|
||||||
prefix = input("Prefix> ")
|
prefix = input("Prefix> ")
|
||||||
if len(prefix) > 10:
|
if len(prefix) > 10:
|
||||||
print("Your prefix seems overly long. Are you sure that it's correct? (y/n)")
|
if not confirm("Your prefix seems overly long. Are you sure that it's correct?"):
|
||||||
if not confirm("> "):
|
|
||||||
prefix = ""
|
prefix = ""
|
||||||
if prefix:
|
if prefix:
|
||||||
loop.run_until_complete(red._config.prefix.set([prefix]))
|
loop.run_until_complete(red._config.prefix.set([prefix]))
|
||||||
@ -54,6 +78,37 @@ def parse_cli_flags(args):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="List all instance names setup with 'redbot-setup'",
|
help="List all instance names setup with 'redbot-setup'",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--edit",
|
||||||
|
action="store_true",
|
||||||
|
help="Edit the instance. This can be done without console interaction "
|
||||||
|
"by passing --no-prompt and arguments that you want to change (available arguments: "
|
||||||
|
"--edit-instance-name, --edit-data-path, --copy-data, --owner, --token).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--edit-instance-name",
|
||||||
|
type=str,
|
||||||
|
help="New name for the instance. This argument only works with --edit argument passed.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--overwrite-existing-instance",
|
||||||
|
action="store_true",
|
||||||
|
help="Confirm overwriting of existing instance when changing name."
|
||||||
|
" This argument only works with --edit argument passed.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--edit-data-path",
|
||||||
|
type=str,
|
||||||
|
help=(
|
||||||
|
"New data path for the instance. This argument only works with --edit argument passed."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--copy-data",
|
||||||
|
action="store_true",
|
||||||
|
help="Copy data from old location. This argument only works "
|
||||||
|
"with --edit and --edit-data-path arguments passed.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--owner",
|
"--owner",
|
||||||
type=int,
|
type=int,
|
||||||
@ -65,7 +120,7 @@ def parse_cli_flags(args):
|
|||||||
"--co-owner",
|
"--co-owner",
|
||||||
type=int,
|
type=int,
|
||||||
default=[],
|
default=[],
|
||||||
nargs="*",
|
nargs="+",
|
||||||
help="ID of a co-owner. Only people who have access "
|
help="ID of a co-owner. Only people who have access "
|
||||||
"to the system that is hosting Red should be "
|
"to the system that is hosting Red should be "
|
||||||
"co-owners, as this gives them complete access "
|
"co-owners, as this gives them complete access "
|
||||||
@ -87,7 +142,7 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--load-cogs",
|
"--load-cogs",
|
||||||
type=str,
|
type=str,
|
||||||
nargs="*",
|
nargs="+",
|
||||||
help="Force loading specified cogs from the installed packages. "
|
help="Force loading specified cogs from the installed packages. "
|
||||||
"Can be used with the --no-cogs flag to load these cogs exclusively.",
|
"Can be used with the --no-cogs flag to load these cogs exclusively.",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -699,3 +699,29 @@ def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitabl
|
|||||||
|
|
||||||
__command_disablers[guild] = disabler
|
__command_disablers[guild] = disabler
|
||||||
return disabler
|
return disabler
|
||||||
|
|
||||||
|
|
||||||
|
# This is intentionally left out of `__all__` as it is not intended for general use
|
||||||
|
class _AlwaysAvailableCommand(Command):
|
||||||
|
"""
|
||||||
|
This should be used only for informational commands
|
||||||
|
which should not be disabled or removed
|
||||||
|
|
||||||
|
These commands cannot belong to a cog.
|
||||||
|
|
||||||
|
These commands do not respect most forms of checks, and
|
||||||
|
should only be used with that in mind.
|
||||||
|
|
||||||
|
This particular class is not supported for 3rd party use
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.cog is not None:
|
||||||
|
raise TypeError("This command may not be added to a cog")
|
||||||
|
|
||||||
|
async def can_run(self, ctx, *args, **kwargs) -> bool:
|
||||||
|
return not ctx.author.bot
|
||||||
|
|
||||||
|
async def _verify_checks(self, ctx) -> bool:
|
||||||
|
return not ctx.author.bot
|
||||||
|
|||||||
@ -398,11 +398,9 @@ class Requires:
|
|||||||
else:
|
else:
|
||||||
rules[model_id] = rule
|
rules[model_id] = rule
|
||||||
|
|
||||||
def clear_all_rules(self, guild_id: int) -> None:
|
def clear_all_rules(self, guild_id: int, *, preserve_default_rule: bool = True) -> None:
|
||||||
"""Clear all rules of a particular scope.
|
"""Clear all rules of a particular scope.
|
||||||
|
|
||||||
This will preserve the default rule, if set.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
guild_id : int
|
guild_id : int
|
||||||
@ -410,6 +408,12 @@ class Requires:
|
|||||||
`Requires.GLOBAL`, this will clear all global rules and
|
`Requires.GLOBAL`, this will clear all global rules and
|
||||||
leave all guild rules untouched.
|
leave all guild rules untouched.
|
||||||
|
|
||||||
|
Other Parameters
|
||||||
|
----------------
|
||||||
|
preserve_default_rule : bool
|
||||||
|
Whether to preserve the default rule or not.
|
||||||
|
This defaults to being preserved
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if guild_id:
|
if guild_id:
|
||||||
rules = self._guild_rules.setdefault(guild_id, _RulesDict())
|
rules = self._guild_rules.setdefault(guild_id, _RulesDict())
|
||||||
@ -417,7 +421,7 @@ class Requires:
|
|||||||
rules = self._global_rules
|
rules = self._global_rules
|
||||||
default = rules.get(self.DEFAULT, None)
|
default = rules.get(self.DEFAULT, None)
|
||||||
rules.clear()
|
rules.clear()
|
||||||
if default is not None:
|
if default is not None and preserve_default_rule:
|
||||||
rules[self.DEFAULT] = default
|
rules[self.DEFAULT] = default
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
|
|||||||
@ -33,7 +33,14 @@ from . import (
|
|||||||
)
|
)
|
||||||
from .utils import create_backup
|
from .utils import create_backup
|
||||||
from .utils.predicates import MessagePredicate
|
from .utils.predicates import MessagePredicate
|
||||||
from .utils.chat_formatting import humanize_timedelta, pagify, box, inline, humanize_list
|
from .utils.chat_formatting import (
|
||||||
|
box,
|
||||||
|
humanize_list,
|
||||||
|
humanize_number,
|
||||||
|
humanize_timedelta,
|
||||||
|
inline,
|
||||||
|
pagify,
|
||||||
|
)
|
||||||
from .commands.requires import PrivilegeLevel
|
from .commands.requires import PrivilegeLevel
|
||||||
|
|
||||||
|
|
||||||
@ -293,7 +300,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
data = await r.json()
|
data = await r.json()
|
||||||
outdated = VersionInfo.from_str(data["info"]["version"]) > red_version_info
|
outdated = VersionInfo.from_str(data["info"]["version"]) > red_version_info
|
||||||
about = _(
|
about = _(
|
||||||
"This is an instance of [Red, an open source Discord bot]({}) "
|
"This bot is an instance of [Red, an open source Discord bot]({}) "
|
||||||
"created by [Twentysix]({}) and [improved by many]({}).\n\n"
|
"created by [Twentysix]({}) and [improved by many]({}).\n\n"
|
||||||
"Red is backed by a passionate community who contributes and "
|
"Red is backed by a passionate community who contributes and "
|
||||||
"creates content for everyone to enjoy. [Join us today]({}) "
|
"creates content for everyone to enjoy. [Join us today]({}) "
|
||||||
@ -1136,7 +1143,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter):
|
async def api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter):
|
||||||
"""Set various external API tokens.
|
"""Set various external API tokens.
|
||||||
|
|
||||||
This setting will be asked for by some 3rd party cogs and some core cogs.
|
This setting will be asked for by some 3rd party cogs and some core cogs.
|
||||||
|
|
||||||
To add the keys provide the service name and the tokens as a comma separated
|
To add the keys provide the service name and the tokens as a comma separated
|
||||||
@ -1162,7 +1169,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
Allows the help command to be sent as a paginated menu instead of seperate
|
Allows the help command to be sent as a paginated menu instead of seperate
|
||||||
messages.
|
messages.
|
||||||
|
|
||||||
This defaults to False.
|
This defaults to False.
|
||||||
Using this without a setting will toggle.
|
Using this without a setting will toggle.
|
||||||
"""
|
"""
|
||||||
if use_menus is None:
|
if use_menus is None:
|
||||||
@ -1316,8 +1323,9 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def backup(self, ctx: commands.Context, *, backup_dir: str = None):
|
async def backup(self, ctx: commands.Context, *, backup_dir: str = None):
|
||||||
"""Creates a backup of all data for the instance.
|
"""Creates a backup of all data for this bot instance.
|
||||||
|
|
||||||
|
This backs up the bot's data and settings.
|
||||||
You may provide a path to a directory for the backup archive to
|
You may provide a path to a directory for the backup archive to
|
||||||
be placed in. If the directory does not exist, the bot will
|
be placed in. If the directory does not exist, the bot will
|
||||||
attempt to create it.
|
attempt to create it.
|
||||||
@ -1877,6 +1885,53 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""Manage the bot's commands."""
|
"""Manage the bot's commands."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@command_manager.group(name="listdisabled", invoke_without_command=True)
|
||||||
|
async def list_disabled(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
List disabled commands.
|
||||||
|
|
||||||
|
If you're the bot owner, this will show global disabled commands by default.
|
||||||
|
"""
|
||||||
|
# Select the scope based on the author's privileges
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
await ctx.invoke(self.list_disabled_global)
|
||||||
|
else:
|
||||||
|
await ctx.invoke(self.list_disabled_guild)
|
||||||
|
|
||||||
|
@list_disabled.command(name="global")
|
||||||
|
async def list_disabled_global(self, ctx: commands.Context):
|
||||||
|
"""List disabled commands globally."""
|
||||||
|
disabled_list = await self.bot._config.disabled_commands()
|
||||||
|
if not disabled_list:
|
||||||
|
return await ctx.send(_("There aren't any globally disabled commands."))
|
||||||
|
|
||||||
|
if len(disabled_list) > 1:
|
||||||
|
header = _("{} commands are disabled globally.\n").format(
|
||||||
|
humanize_number(len(disabled_list))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header = _("1 command is disabled globally.\n")
|
||||||
|
paged = [box(x) for x in pagify(humanize_list(disabled_list), page_length=1000)]
|
||||||
|
paged[0] = header + paged[0]
|
||||||
|
await ctx.send_interactive(paged)
|
||||||
|
|
||||||
|
@list_disabled.command(name="guild")
|
||||||
|
async def list_disabled_guild(self, ctx: commands.Context):
|
||||||
|
"""List disabled commands in this server."""
|
||||||
|
disabled_list = await self.bot._config.guild(ctx.guild).disabled_commands()
|
||||||
|
if not disabled_list:
|
||||||
|
return await ctx.send(_("There aren't any disabled commands in {}.").format(ctx.guild))
|
||||||
|
|
||||||
|
if len(disabled_list) > 1:
|
||||||
|
header = _("{} commands are disabled in {}.\n").format(
|
||||||
|
humanize_number(len(disabled_list)), ctx.guild
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header = _("1 command is disabled in {}.\n").format(ctx.guild)
|
||||||
|
paged = [box(x) for x in pagify(humanize_list(disabled_list), page_length=1000)]
|
||||||
|
paged[0] = header + paged[0]
|
||||||
|
await ctx.send_interactive(paged)
|
||||||
|
|
||||||
@command_manager.group(name="disable", invoke_without_command=True)
|
@command_manager.group(name="disable", invoke_without_command=True)
|
||||||
async def command_disable(self, ctx: commands.Context, *, command: str):
|
async def command_disable(self, ctx: commands.Context, *, command: str):
|
||||||
"""Disable a command.
|
"""Disable a command.
|
||||||
@ -1907,6 +1962,12 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if isinstance(command_obj, commands.commands._AlwaysAvailableCommand):
|
||||||
|
await ctx.send(
|
||||||
|
_("This command is designated as being always available and cannot be disabled.")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
async with ctx.bot._config.disabled_commands() as disabled_commands:
|
async with ctx.bot._config.disabled_commands() as disabled_commands:
|
||||||
if command not in disabled_commands:
|
if command not in disabled_commands:
|
||||||
disabled_commands.append(command_obj.qualified_name)
|
disabled_commands.append(command_obj.qualified_name)
|
||||||
@ -1935,6 +1996,12 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if isinstance(command_obj, commands.commands._AlwaysAvailableCommand):
|
||||||
|
await ctx.send(
|
||||||
|
_("This command is designated as being always available and cannot be disabled.")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if command_obj.requires.privilege_level > await PrivilegeLevel.from_ctx(ctx):
|
if command_obj.requires.privilege_level > await PrivilegeLevel.from_ctx(ctx):
|
||||||
await ctx.send(_("You are not allowed to disable that command."))
|
await ctx.send(_("You are not allowed to disable that command."))
|
||||||
return
|
return
|
||||||
@ -2215,3 +2282,21 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
async def rpc_reload(self, request):
|
async def rpc_reload(self, request):
|
||||||
await self.rpc_unload(request)
|
await self.rpc_unload(request)
|
||||||
await self.rpc_load(request)
|
await self.rpc_load(request)
|
||||||
|
|
||||||
|
|
||||||
|
# Removing this command from forks is a violation of the GPLv3 under which it is licensed.
|
||||||
|
# Otherwise interfering with the ability for this command to be accessible is also a violation.
|
||||||
|
@commands.command(cls=commands.commands._AlwaysAvailableCommand, name="licenseinfo", i18n=_)
|
||||||
|
async def license_info_command(ctx):
|
||||||
|
"""
|
||||||
|
Get info about Red's licenses
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = (
|
||||||
|
"This bot is an instance of Red-DiscordBot (hereafter refered to as Red)\n"
|
||||||
|
"Red is a free and open source application made available to the public and "
|
||||||
|
"licensed under the GNU GPLv3. The full text of this license is available to you at "
|
||||||
|
"<https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/LICENSE>"
|
||||||
|
)
|
||||||
|
await ctx.send(message)
|
||||||
|
# We need a link which contains a thank you to other projects which we use at some point.
|
||||||
|
|||||||
@ -4,7 +4,6 @@ from typing import Optional, Type
|
|||||||
from .. import data_manager
|
from .. import data_manager
|
||||||
from .base import IdentifierData, BaseDriver, ConfigCategory
|
from .base import IdentifierData, BaseDriver, ConfigCategory
|
||||||
from .json import JsonDriver
|
from .json import JsonDriver
|
||||||
from .mongo import MongoDriver
|
|
||||||
from .postgres import PostgresDriver
|
from .postgres import PostgresDriver
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -13,7 +12,6 @@ __all__ = [
|
|||||||
"IdentifierData",
|
"IdentifierData",
|
||||||
"BaseDriver",
|
"BaseDriver",
|
||||||
"JsonDriver",
|
"JsonDriver",
|
||||||
"MongoDriver",
|
|
||||||
"PostgresDriver",
|
"PostgresDriver",
|
||||||
"BackendType",
|
"BackendType",
|
||||||
]
|
]
|
||||||
@ -21,16 +19,13 @@ __all__ = [
|
|||||||
|
|
||||||
class BackendType(enum.Enum):
|
class BackendType(enum.Enum):
|
||||||
JSON = "JSON"
|
JSON = "JSON"
|
||||||
MONGO = "MongoDBV2"
|
|
||||||
MONGOV1 = "MongoDB"
|
|
||||||
POSTGRES = "Postgres"
|
POSTGRES = "Postgres"
|
||||||
|
# Dead drivrs below retained for error handling.
|
||||||
|
MONGOV1 = "MongoDB"
|
||||||
|
MONGO = "MongoDBV2"
|
||||||
|
|
||||||
|
|
||||||
_DRIVER_CLASSES = {
|
_DRIVER_CLASSES = {BackendType.JSON: JsonDriver, BackendType.POSTGRES: PostgresDriver}
|
||||||
BackendType.JSON: JsonDriver,
|
|
||||||
BackendType.MONGO: MongoDriver,
|
|
||||||
BackendType.POSTGRES: PostgresDriver,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_driver_class(storage_type: Optional[BackendType] = None) -> Type[BaseDriver]:
|
def get_driver_class(storage_type: Optional[BackendType] = None) -> Type[BaseDriver]:
|
||||||
@ -86,7 +81,7 @@ def get_driver(
|
|||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
RuntimeError
|
RuntimeError
|
||||||
If the storage type is MongoV1 or invalid.
|
If the storage type is MongoV1, Mongo, or invalid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if storage_type is None:
|
if storage_type is None:
|
||||||
@ -98,12 +93,10 @@ def get_driver(
|
|||||||
try:
|
try:
|
||||||
driver_cls: Type[BaseDriver] = get_driver_class(storage_type)
|
driver_cls: Type[BaseDriver] = get_driver_class(storage_type)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if storage_type == BackendType.MONGOV1:
|
if storage_type in (BackendType.MONGOV1, BackendType.MONGO):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Please convert to JSON first to continue using the bot."
|
"Please convert to JSON first to continue using the bot."
|
||||||
" This is a required conversion prior to using the new Mongo driver."
|
"Mongo support was removed in 3.2."
|
||||||
" This message will be updated with a link to the update docs once those"
|
|
||||||
" docs have been created."
|
|
||||||
) from None
|
) from None
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Invalid driver type: '{storage_type}'") from None
|
raise RuntimeError(f"Invalid driver type: '{storage_type}'") from None
|
||||||
|
|||||||
@ -221,7 +221,7 @@ def _save_json(path: Path, data: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
On windows, it is not available in entirety.
|
On windows, it is not available in entirety.
|
||||||
If a windows user ends up with tons of temp files, they should consider hosting on
|
If a windows user ends up with tons of temp files, they should consider hosting on
|
||||||
something POSIX compatible, or using the mongo backend instead.
|
something POSIX compatible, or using a different backend instead.
|
||||||
|
|
||||||
Most users wont encounter this issue, but with high write volumes,
|
Most users wont encounter this issue, but with high write volumes,
|
||||||
without the fsync on both the temp file, and after the replace on the directory,
|
without the fsync on both the temp file, and after the replace on the directory,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user