mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-10 21:28:55 -05:00
Merge remote-tracking branch 'release/V3/develop' into V3/develop
This commit is contained in:
commit
71955becb1
@ -126,11 +126,6 @@ Join us on our [Official Discord Server](https://discord.gg/red)!
|
|||||||
|
|
||||||
Released under the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
Released under the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
||||||
|
|
||||||
This project vendors the
|
|
||||||
[discord.py library by Rapptz](https://github.com/Rapptz/discord.py/tree/rewrite), which is
|
|
||||||
licensed under the [MIT License](https://opensource.org/licenses/MIT). This amounts to everything
|
|
||||||
within the *discord* folder of this repository.
|
|
||||||
|
|
||||||
Red is named after the main character of "Transistor", a video game by
|
Red is named after the main character of "Transistor", a video game by
|
||||||
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,41 @@
|
|||||||
.. v3.1.0 Changelog
|
.. v3.1.0 Changelog
|
||||||
|
|
||||||
================
|
####################
|
||||||
|
v3.1.0 Release Notes
|
||||||
|
####################
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Mongo Driver Migration
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Due to the required changes of the Mongo driver for Config, all existing Mongo users will need to
|
||||||
|
complete the below instructions to continue to use Mongo after updating to 3.1.
|
||||||
|
This includes **all** users, regardless of any prior migration attempt to a development version of
|
||||||
|
3.1.
|
||||||
|
|
||||||
|
#. Upgrade to 3.1
|
||||||
|
#. Convert all existing Mongo instances to JSON using the new converters
|
||||||
|
#. Start each bot instance while using JSON and load any and all cogs you have in order to successfully preserve data.
|
||||||
|
#. Turn each instance off and convert back to Mongo.
|
||||||
|
**NOTE:** No data is wiped from your Mongo database when converting to JSON.
|
||||||
|
You may want to use a *new* database name when converting back to Mongo in order to not have duplicate data.
|
||||||
|
|
||||||
|
-------------
|
||||||
|
Setup Utility
|
||||||
|
-------------
|
||||||
|
|
||||||
|
New commands were introduced to simplify the conversion/editing/removal process both on our end and the users end.
|
||||||
|
Please use ``redbot-setup --help`` to learn how to use the new features.
|
||||||
|
|
||||||
|
.. HINT::
|
||||||
|
|
||||||
|
Converting to JSON: ``redbot-setup convert <instance_name> json``
|
||||||
|
|
||||||
|
Converting to Mongo: ``redbot-setup convert <instance_name> mongo``
|
||||||
|
|
||||||
|
################
|
||||||
v3.1.0 Changelog
|
v3.1.0 Changelog
|
||||||
================
|
################
|
||||||
|
|
||||||
-----
|
-----
|
||||||
Audio
|
Audio
|
||||||
@ -33,8 +66,15 @@ Audio
|
|||||||
Core
|
Core
|
||||||
----
|
----
|
||||||
|
|
||||||
|
* New Event dispatch: ``on_message_without_command`` (`#2338`_)
|
||||||
|
* Improve output format of cooldown messages (`#2412`_)
|
||||||
* Delete cooldown messages when expired (`#2469`_)
|
* Delete cooldown messages when expired (`#2469`_)
|
||||||
|
* Fix local blacklist/whitelist management (`#2531`_)
|
||||||
|
* ``[p]set locale`` now only accepts actual locales (`#2553`_)
|
||||||
|
* ``[p]listlocales`` now displays ``en-US`` (`#2553`_)
|
||||||
* ``redbot --version`` will now give you current version of Red (`#2567`_)
|
* ``redbot --version`` will now give you current version of Red (`#2567`_)
|
||||||
|
* Default locale changed from ``en`` to ``en-US`` (`#2642`_)
|
||||||
|
* New command ``[p]datapath`` that prints the bot's datapath (`#2652`_)
|
||||||
|
|
||||||
------
|
------
|
||||||
Config
|
Config
|
||||||
@ -45,24 +85,50 @@ Config
|
|||||||
* We now record custom group primary key lengths in the core config object (`#2550`_)
|
* We now record custom group primary key lengths in the core config object (`#2550`_)
|
||||||
* Migrated internal UUIDs to maintain cross platform consistency (`#2604`_)
|
* Migrated internal UUIDs to maintain cross platform consistency (`#2604`_)
|
||||||
|
|
||||||
|
-------------
|
||||||
|
DataConverter
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* It's dead jim (Removal) (`#2554`_)
|
||||||
|
|
||||||
----------
|
----------
|
||||||
discord.py
|
discord.py
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* No longer vendoring discord.py (`#2587`_)
|
||||||
|
* Upgraded discord.py dependency to version 1.0.1 (`#2587`_)
|
||||||
|
|
||||||
----------
|
----------
|
||||||
Downloader
|
Downloader
|
||||||
----------
|
----------
|
||||||
|
|
||||||
* ``[p]cog install`` will now tell user that cog has to be loaded (`#2523`_)
|
* ``[p]cog install`` will now tell user that cog has to be loaded (`#2523`_)
|
||||||
|
* The message when libraries fail to install is now formatted (`#2576`_)
|
||||||
* Fixed bug, that caused Downloader to include submodules on cog list (`#2590`_)
|
* Fixed bug, that caused Downloader to include submodules on cog list (`#2590`_)
|
||||||
* ``[p]cog uninstall`` allows to uninstall multiple cogs now (`#2592`_)
|
* ``[p]cog uninstall`` allows to uninstall multiple cogs now (`#2592`_)
|
||||||
* ``[p]cog uninstall`` will now remove cog from installed cogs even if it can't find the cog in install path anymore (`#2595`_)
|
* ``[p]cog uninstall`` will now remove cog from installed cogs even if it can't find the cog in install path anymore (`#2595`_)
|
||||||
|
* ``[p]cog install`` will not allow to install cogs which aren't suitable for installed version of Red anymore (`#2605`_)
|
||||||
|
* Cog Developers now have to use ``min_bot_version`` in form of version string instead of ``bot_version`` in info.json and they can also use ``max_bot_version`` to specify maximum version of Red, more in :doc:`framework_downloader`. (`#2605`_)
|
||||||
|
|
||||||
|
------
|
||||||
|
Filter
|
||||||
|
------
|
||||||
|
|
||||||
|
* Filter performs significantly better on large servers. (`#2509`_)
|
||||||
|
|
||||||
---
|
---
|
||||||
Mod
|
Mod
|
||||||
---
|
---
|
||||||
|
|
||||||
* Admins can now decide how many times message has to be repeated before ``deleterepeats`` removes it (`#2437`_)
|
* Admins can now decide how many times message has to be repeated before ``deleterepeats`` removes it (`#2437`_)
|
||||||
|
* Fix: make ``[p]ban [days]`` optional as per the doc (`#2602`_)
|
||||||
|
* Added the command ``voicekick`` to kick members from a voice channel with optional mod case. (`#2639`_)
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Permissions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
* Removed: ``p`` alias for ``permissions`` command (`#2467`_)
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
Setup Scripts
|
Setup Scripts
|
||||||
@ -72,6 +138,13 @@ Setup Scripts
|
|||||||
* ``redbot-setup convert`` now used to convert between libraries (`#2579`_)
|
* ``redbot-setup convert`` now used to convert between libraries (`#2579`_)
|
||||||
* Backup support for Mongo is currently broken (`#2579`_)
|
* Backup support for Mongo is currently broken (`#2579`_)
|
||||||
|
|
||||||
|
-------
|
||||||
|
Streams
|
||||||
|
-------
|
||||||
|
|
||||||
|
* Add support for custom stream alert messages per guild (`#2600`_)
|
||||||
|
* Add ability to exclude rerun Twitch streams, and note rerun streams in embed status (`#2620`_)
|
||||||
|
|
||||||
-----
|
-----
|
||||||
Tests
|
Tests
|
||||||
-----
|
-----
|
||||||
@ -88,15 +161,20 @@ Trivia
|
|||||||
Utility Functions
|
Utility Functions
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
* New: ``chat_formatting.humaize_timedelta`` (`#2412`_)
|
||||||
* ``Tunnel`` - Spelling correction of method name - changed ``files_from_attatch`` to ``files_from_attach`` (old name is left for backwards compatibility) (`#2496`_)
|
* ``Tunnel`` - Spelling correction of method name - changed ``files_from_attatch`` to ``files_from_attach`` (old name is left for backwards compatibility) (`#2496`_)
|
||||||
* ``Tunnel`` - fixed behavior of ``react_close()``, now when tunnel closes message will be sent to other end (`#2507`_)
|
* ``Tunnel`` - fixed behavior of ``react_close()``, now when tunnel closes message will be sent to other end (`#2507`_)
|
||||||
|
* ``chat_formatting.humanize_list`` - Improved error handling of empty lists (`#2597`_)
|
||||||
|
|
||||||
.. _#2328: https://github.com/Cog-Creators/Red-DiscordBot/pull/2328
|
.. _#2328: https://github.com/Cog-Creators/Red-DiscordBot/pull/2328
|
||||||
|
.. _#2338: https://github.com/Cog-Creators/Red-DiscordBot/pull/2338
|
||||||
|
.. _#2412: https://github.com/Cog-Creators/Red-DiscordBot/pull/2412
|
||||||
.. _#2437: https://github.com/Cog-Creators/Red-DiscordBot/pull/2437
|
.. _#2437: https://github.com/Cog-Creators/Red-DiscordBot/pull/2437
|
||||||
.. _#2457: https://github.com/Cog-Creators/Red-DiscordBot/pull/2457
|
.. _#2457: https://github.com/Cog-Creators/Red-DiscordBot/pull/2457
|
||||||
.. _#2461: https://github.com/Cog-Creators/Red-DiscordBot/pull/2461
|
.. _#2461: https://github.com/Cog-Creators/Red-DiscordBot/pull/2461
|
||||||
.. _#2462: https://github.com/Cog-Creators/Red-DiscordBot/pull/2462
|
.. _#2462: https://github.com/Cog-Creators/Red-DiscordBot/pull/2462
|
||||||
.. _#2465: https://github.com/Cog-Creators/Red-DiscordBot/pull/2465
|
.. _#2465: https://github.com/Cog-Creators/Red-DiscordBot/pull/2465
|
||||||
|
.. _#2467: https://github.com/Cog-Creators/Red-DiscordBot/pull/2467
|
||||||
.. _#2469: https://github.com/Cog-Creators/Red-DiscordBot/pull/2469
|
.. _#2469: https://github.com/Cog-Creators/Red-DiscordBot/pull/2469
|
||||||
.. _#2470: https://github.com/Cog-Creators/Red-DiscordBot/pull/2470
|
.. _#2470: https://github.com/Cog-Creators/Red-DiscordBot/pull/2470
|
||||||
.. _#2472: https://github.com/Cog-Creators/Red-DiscordBot/pull/2472
|
.. _#2472: https://github.com/Cog-Creators/Red-DiscordBot/pull/2472
|
||||||
@ -106,24 +184,38 @@ Utility Functions
|
|||||||
.. _#2482: https://github.com/Cog-Creators/Red-DiscordBot/pull/2482
|
.. _#2482: https://github.com/Cog-Creators/Red-DiscordBot/pull/2482
|
||||||
.. _#2496: https://github.com/Cog-Creators/Red-DiscordBot/pull/2496
|
.. _#2496: https://github.com/Cog-Creators/Red-DiscordBot/pull/2496
|
||||||
.. _#2507: https://github.com/Cog-Creators/Red-DiscordBot/pull/2507
|
.. _#2507: https://github.com/Cog-Creators/Red-DiscordBot/pull/2507
|
||||||
|
.. _#2509: https://github.com/Cog-Creators/Red-DiscordBot/pull/2509
|
||||||
.. _#2513: https://github.com/Cog-Creators/Red-DiscordBot/pull/2513
|
.. _#2513: https://github.com/Cog-Creators/Red-DiscordBot/pull/2513
|
||||||
.. _#2521: https://github.com/Cog-Creators/Red-DiscordBot/pull/2521
|
.. _#2521: https://github.com/Cog-Creators/Red-DiscordBot/pull/2521
|
||||||
.. _#2523: https://github.com/Cog-Creators/Red-DiscordBot/pull/2523
|
.. _#2523: https://github.com/Cog-Creators/Red-DiscordBot/pull/2523
|
||||||
.. _#2525: https://github.com/Cog-Creators/Red-DiscordBot/pull/2525
|
.. _#2525: https://github.com/Cog-Creators/Red-DiscordBot/pull/2525
|
||||||
|
.. _#2531: https://github.com/Cog-Creators/Red-DiscordBot/pull/2531
|
||||||
.. _#2533: https://github.com/Cog-Creators/Red-DiscordBot/pull/2533
|
.. _#2533: https://github.com/Cog-Creators/Red-DiscordBot/pull/2533
|
||||||
.. _#2536: https://github.com/Cog-Creators/Red-DiscordBot/pull/2536
|
.. _#2536: https://github.com/Cog-Creators/Red-DiscordBot/pull/2536
|
||||||
.. _#2540: https://github.com/Cog-Creators/Red-DiscordBot/pull/2540
|
.. _#2540: https://github.com/Cog-Creators/Red-DiscordBot/pull/2540
|
||||||
.. _#2545: https://github.com/Cog-Creators/Red-DiscordBot/pull/2545
|
.. _#2545: https://github.com/Cog-Creators/Red-DiscordBot/pull/2545
|
||||||
.. _#2550: https://github.com/Cog-Creators/Red-DiscordBot/pull/2550
|
.. _#2550: https://github.com/Cog-Creators/Red-DiscordBot/pull/2550
|
||||||
|
.. _#2553: https://github.com/Cog-Creators/Red-DiscordBot/pull/2553
|
||||||
|
.. _#2554: https://github.com/Cog-Creators/Red-DiscordBot/pull/2554
|
||||||
.. _#2556: https://github.com/Cog-Creators/Red-DiscordBot/pull/2556
|
.. _#2556: https://github.com/Cog-Creators/Red-DiscordBot/pull/2556
|
||||||
.. _#2557: https://github.com/Cog-Creators/Red-DiscordBot/pull/2557
|
.. _#2557: https://github.com/Cog-Creators/Red-DiscordBot/pull/2557
|
||||||
.. _#2565: https://github.com/Cog-Creators/Red-DiscordBot/pull/2565
|
.. _#2565: https://github.com/Cog-Creators/Red-DiscordBot/pull/2565
|
||||||
.. _#2567: https://github.com/Cog-Creators/Red-DiscordBot/pull/2567
|
.. _#2567: https://github.com/Cog-Creators/Red-DiscordBot/pull/2567
|
||||||
|
.. _#2576: https://github.com/Cog-Creators/Red-DiscordBot/pull/2576
|
||||||
.. _#2579: https://github.com/Cog-Creators/Red-DiscordBot/pull/2579
|
.. _#2579: https://github.com/Cog-Creators/Red-DiscordBot/pull/2579
|
||||||
.. _#2586: https://github.com/Cog-Creators/Red-DiscordBot/pull/2586
|
.. _#2586: https://github.com/Cog-Creators/Red-DiscordBot/pull/2586
|
||||||
|
.. _#2587: https://github.com/Cog-Creators/Red-DiscordBot/pull/2587
|
||||||
.. _#2590: https://github.com/Cog-Creators/Red-DiscordBot/pull/2590
|
.. _#2590: https://github.com/Cog-Creators/Red-DiscordBot/pull/2590
|
||||||
.. _#2591: https://github.com/Cog-Creators/Red-DiscordBot/pull/2591
|
.. _#2591: https://github.com/Cog-Creators/Red-DiscordBot/pull/2591
|
||||||
.. _#2592: https://github.com/Cog-Creators/Red-DiscordBot/pull/2592
|
.. _#2592: https://github.com/Cog-Creators/Red-DiscordBot/pull/2592
|
||||||
.. _#2595: https://github.com/Cog-Creators/Red-DiscordBot/pull/2595
|
.. _#2595: https://github.com/Cog-Creators/Red-DiscordBot/pull/2595
|
||||||
|
.. _#2597: https://github.com/Cog-Creators/Red-DiscordBot/pull/2597
|
||||||
|
.. _#2600: https://github.com/Cog-Creators/Red-DiscordBot/pull/2600
|
||||||
|
.. _#2602: https://github.com/Cog-Creators/Red-DiscordBot/pull/2602
|
||||||
.. _#2604: https://github.com/Cog-Creators/Red-DiscordBot/pull/2604
|
.. _#2604: https://github.com/Cog-Creators/Red-DiscordBot/pull/2604
|
||||||
|
.. _#2605: https://github.com/Cog-Creators/Red-DiscordBot/pull/2605
|
||||||
.. _#2606: https://github.com/Cog-Creators/Red-DiscordBot/pull/2606
|
.. _#2606: https://github.com/Cog-Creators/Red-DiscordBot/pull/2606
|
||||||
|
.. _#2620: https://github.com/Cog-Creators/Red-DiscordBot/pull/2620
|
||||||
|
.. _#2639: https://github.com/Cog-Creators/Red-DiscordBot/pull/2639
|
||||||
|
.. _#2642: https://github.com/Cog-Creators/Red-DiscordBot/pull/2642
|
||||||
|
.. _#2652: https://github.com/Cog-Creators/Red-DiscordBot/pull/2652
|
||||||
|
|||||||
@ -203,7 +203,7 @@ linkcheck_ignore = [r"https://java.com*", r"https://chocolatey.org*"]
|
|||||||
# Intersphinx
|
# Intersphinx
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"python": ("https://docs.python.org/3", None),
|
"python": ("https://docs.python.org/3", None),
|
||||||
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
"dpy": ("https://discordpy.readthedocs.io/en/v1.0.1/", None),
|
||||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ and when accessed in the code it should be done by
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
await self.bot.db.api_tokens.get_raw("twitch", default={"client_id": None, "client_secret: None"})
|
await self.bot.db.api_tokens.get_raw("twitch", default={"client_id": None, "client_secret": None})
|
||||||
|
|
||||||
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,10 @@ Keys common to both repo and cog info.json (case sensitive)
|
|||||||
Keys specific to the cog info.json (case sensitive)
|
Keys specific to the cog info.json (case sensitive)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
- ``min_bot_version`` (string) - Min 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
|
||||||
|
|
||||||
- ``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.
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
Migrating Cogs to V3
|
Migrating Cogs to V3
|
||||||
====================
|
====================
|
||||||
|
|
||||||
First, be sure to read `discord.py's migration guide <http://discordpy.readthedocs.io/en/rewrite/migrating.html>`_
|
First, be sure to read `discord.py's migration guide <https://discordpy.readthedocs.io/en/v1.0.1/migrating.html>`_
|
||||||
as that covers all of the changes to discord.py that will affect the migration process
|
as that covers all of the changes to discord.py that will affect the migration process
|
||||||
|
|
||||||
----------------
|
----------------
|
||||||
|
|||||||
@ -92,7 +92,7 @@ one-by-one:
|
|||||||
brew install python --with-brewed-openssl
|
brew install python --with-brewed-openssl
|
||||||
brew install git
|
brew install git
|
||||||
brew tap caskroom/versions
|
brew tap caskroom/versions
|
||||||
brew cask install java8
|
brew cask install homebrew/cask-versions/adoptopenjdk8
|
||||||
|
|
||||||
It's possible you will have network issues. If so, go in your Applications folder, inside it, go in the Python 3.7 folder then double click ``Install certificates.command``
|
It's possible you will have network issues. If so, go in your Applications folder, inside it, go in the Python 3.7 folder then double click ``Install certificates.command``
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
|
|
||||||
It will walk through the initial setup, asking for your token and a prefix.
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
You can find out how to obtain a token with
|
You can find out how to obtain a token with
|
||||||
`this guide <https://discordpy.readthedocs.io/en/rewrite/discord.html#creating-a-bot-account>`_,
|
`this guide <https://discordpy.readthedocs.io/en/v1.0.1/discord.html#creating-a-bot-account>`_,
|
||||||
section "Creating a Bot Account".
|
section "Creating a Bot Account".
|
||||||
|
|
||||||
You may also run Red via the launcher, which allows you to restart the bot
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
|
|||||||
@ -111,7 +111,7 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
|
|
||||||
It will walk through the initial setup, asking for your token and a prefix.
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
You can find out how to obtain a token with
|
You can find out how to obtain a token with
|
||||||
`this guide <https://discordpy.readthedocs.io/en/rewrite/discord.html#creating-a-bot-account>`_,
|
`this guide <https://discordpy.readthedocs.io/en/v1.0.1/discord.html#creating-a-bot-account>`_,
|
||||||
section "Creating a Bot Account".
|
section "Creating a Bot Account".
|
||||||
|
|
||||||
You may also run Red via the launcher, which allows you to restart the bot
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
|
|||||||
@ -119,8 +119,14 @@ class VersionInfo:
|
|||||||
"dev_release": self.dev_release,
|
"dev_release": self.dev_release,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __lt__(self, other: "VersionInfo") -> bool:
|
def _generate_comparison_tuples(
|
||||||
tups: _List[_Tuple[int, int, int, int, int, int, int]] = []
|
self, other: "VersionInfo"
|
||||||
|
) -> _List[
|
||||||
|
_Tuple[int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float]]
|
||||||
|
]:
|
||||||
|
tups: _List[
|
||||||
|
_Tuple[int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float]]
|
||||||
|
] = []
|
||||||
for obj in (self, other):
|
for obj in (self, other):
|
||||||
tups.append(
|
tups.append(
|
||||||
(
|
(
|
||||||
@ -133,8 +139,20 @@ class VersionInfo:
|
|||||||
obj.dev_release if obj.dev_release is not None else _inf,
|
obj.dev_release if obj.dev_release is not None else _inf,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
return tups
|
||||||
|
|
||||||
|
def __lt__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
return tups[0] < tups[1]
|
return tups[0] < tups[1]
|
||||||
|
|
||||||
|
def __eq__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
|
return tups[0] == tups[1]
|
||||||
|
|
||||||
|
def __le__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
|
return tups[0] <= tups[1]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
ret = f"{self.major}.{self.minor}.{self.micro}"
|
ret = f"{self.major}.{self.minor}.{self.micro}"
|
||||||
if self.releaselevel != self.FINAL:
|
if self.releaselevel != self.FINAL:
|
||||||
|
|||||||
@ -104,7 +104,9 @@ def main():
|
|||||||
log.debug("Data Path: %s", data_manager._base_data_path())
|
log.debug("Data Path: %s", data_manager._base_data_path())
|
||||||
log.debug("Storage Type: %s", data_manager.storage_type())
|
log.debug("Storage Type: %s", data_manager.storage_type())
|
||||||
|
|
||||||
red = Red(cli_flags=cli_flags, description=description, dm_help=None)
|
red = Red(
|
||||||
|
cli_flags=cli_flags, description=description, dm_help=None, fetch_offline_members=True
|
||||||
|
)
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
red.add_cog(Core(red))
|
red.add_cog(Core(red))
|
||||||
|
|||||||
@ -1,31 +1,9 @@
|
|||||||
from pathlib import Path
|
from redbot.core import commands
|
||||||
import logging
|
|
||||||
|
|
||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
from .manager import start_lavalink_server, maybe_download_lavalink
|
|
||||||
from redbot.core import commands
|
|
||||||
from redbot.core.data_manager import cog_data_path
|
|
||||||
import redbot.core
|
|
||||||
|
|
||||||
log = logging.getLogger("red.audio")
|
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_URL = (
|
|
||||||
"https://github.com/Cog-Creators/Red-DiscordBot/releases/download/{}/Lavalink.jar"
|
|
||||||
).format(redbot.core.__version__)
|
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
|
||||||
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
|
||||||
|
|
||||||
APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
|
|
||||||
BUNDLED_APP_YML_FILE = Path(__file__).parent / "data/application.yml"
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
async def setup(bot: commands.Bot):
|
||||||
cog = Audio(bot)
|
cog = Audio(bot)
|
||||||
if not await cog.config.use_external_lavalink():
|
|
||||||
await maybe_download_lavalink(bot.loop, cog)
|
|
||||||
await start_lavalink_server(bot.loop)
|
|
||||||
|
|
||||||
await cog.initialize()
|
await cog.initialize()
|
||||||
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
import redbot.core
|
import redbot.core
|
||||||
from redbot.core import Config, commands, checks, bank
|
from redbot.core import Config, commands, checks, bank
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
@ -29,11 +30,11 @@ from redbot.core.utils.menus import (
|
|||||||
)
|
)
|
||||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from .manager import shutdown_lavalink_server, start_lavalink_server, maybe_download_lavalink
|
from .manager import ServerManager
|
||||||
|
|
||||||
_ = Translator("Audio", __file__)
|
_ = Translator("Audio", __file__)
|
||||||
|
|
||||||
__version__ = "0.0.8b"
|
__version__ = "0.0.9"
|
||||||
__author__ = ["aikaterna"]
|
__author__ = ["aikaterna"]
|
||||||
|
|
||||||
log = logging.getLogger("red.audio")
|
log = logging.getLogger("red.audio")
|
||||||
@ -43,40 +44,45 @@ log = logging.getLogger("red.audio")
|
|||||||
class Audio(commands.Cog):
|
class Audio(commands.Cog):
|
||||||
"""Play audio through voice channels."""
|
"""Play audio through voice channels."""
|
||||||
|
|
||||||
|
_default_lavalink_settings = {
|
||||||
|
"host": "localhost",
|
||||||
|
"rest_port": 2333,
|
||||||
|
"ws_port": 2333,
|
||||||
|
"password": "youshallnotpass",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
||||||
|
|
||||||
default_global = {
|
default_global = dict(
|
||||||
"host": "localhost",
|
status=False,
|
||||||
"rest_port": "2333",
|
use_external_lavalink=False,
|
||||||
"ws_port": "2332",
|
restrict=True,
|
||||||
"password": "youshallnotpass",
|
current_version=redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
|
||||||
"status": False,
|
localpath=str(cog_data_path(raw_name="Audio")),
|
||||||
"current_version": redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
|
**self._default_lavalink_settings,
|
||||||
"use_external_lavalink": False,
|
)
|
||||||
"restrict": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
default_guild = {
|
default_guild = dict(
|
||||||
"disconnect": False,
|
disconnect=False,
|
||||||
"dj_enabled": False,
|
dj_enabled=False,
|
||||||
"dj_role": None,
|
dj_role=None,
|
||||||
"emptydc_enabled": False,
|
emptydc_enabled=False,
|
||||||
"emptydc_timer": 0,
|
emptydc_timer=0,
|
||||||
"jukebox": False,
|
jukebox=False,
|
||||||
"jukebox_price": 0,
|
jukebox_price=0,
|
||||||
"maxlength": 0,
|
maxlength=0,
|
||||||
"playlists": {},
|
playlists={},
|
||||||
"notify": False,
|
notify=False,
|
||||||
"repeat": False,
|
repeat=False,
|
||||||
"shuffle": False,
|
shuffle=False,
|
||||||
"thumbnail": False,
|
thumbnail=False,
|
||||||
"volume": 100,
|
volume=100,
|
||||||
"vote_enabled": False,
|
vote_enabled=False,
|
||||||
"vote_percent": 0,
|
vote_percent=0,
|
||||||
}
|
)
|
||||||
|
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
self.config.register_global(**default_global)
|
self.config.register_global(**default_global)
|
||||||
@ -85,9 +91,24 @@ class Audio(commands.Cog):
|
|||||||
self._connect_task = None
|
self._connect_task = None
|
||||||
self._disconnect_task = None
|
self._disconnect_task = None
|
||||||
self._cleaned_up = False
|
self._cleaned_up = False
|
||||||
|
|
||||||
self.spotify_token = None
|
self.spotify_token = None
|
||||||
self.play_lock = {}
|
self.play_lock = {}
|
||||||
|
|
||||||
|
self._manager: Optional[ServerManager] = None
|
||||||
|
|
||||||
|
async def cog_before_invoke(self, ctx):
|
||||||
|
if self.llsetup in [ctx.command, ctx.command.root_parent]:
|
||||||
|
pass
|
||||||
|
elif self._connect_task.cancelled():
|
||||||
|
await ctx.send(
|
||||||
|
"You have attempted to run Audio's Lavalink server on an unsupported"
|
||||||
|
" architecture. Only settings related commands will be available."
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
"Not running audio command due to invalid machine architecture for Lavalink."
|
||||||
|
)
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
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())
|
||||||
@ -102,16 +123,33 @@ class Audio(commands.Cog):
|
|||||||
async def attempt_connect(self, timeout: int = 30):
|
async def attempt_connect(self, timeout: int = 30):
|
||||||
while True: # run until success
|
while True: # run until success
|
||||||
external = await self.config.use_external_lavalink()
|
external = await self.config.use_external_lavalink()
|
||||||
if not external:
|
if external is False:
|
||||||
shutdown_lavalink_server()
|
settings = self._default_lavalink_settings
|
||||||
await maybe_download_lavalink(self.bot.loop, self)
|
host = settings["host"]
|
||||||
await start_lavalink_server(self.bot.loop)
|
password = settings["password"]
|
||||||
try:
|
rest_port = settings["rest_port"]
|
||||||
|
ws_port = settings["ws_port"]
|
||||||
|
if self._manager is not None:
|
||||||
|
await self._manager.shutdown()
|
||||||
|
self._manager = ServerManager()
|
||||||
|
try:
|
||||||
|
await self._manager.start()
|
||||||
|
except RuntimeError as exc:
|
||||||
|
log.exception(
|
||||||
|
"Exception whilst starting internal Lavalink server, retrying...",
|
||||||
|
exc_info=exc,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
log.exception("Invalid machine architecture, cannot run Lavalink.")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
host = await self.config.host()
|
host = await self.config.host()
|
||||||
password = await self.config.password()
|
password = await self.config.password()
|
||||||
rest_port = await self.config.rest_port()
|
rest_port = await self.config.rest_port()
|
||||||
ws_port = await self.config.ws_port()
|
ws_port = await self.config.ws_port()
|
||||||
|
try:
|
||||||
await lavalink.initialize(
|
await lavalink.initialize(
|
||||||
bot=self.bot,
|
bot=self.bot,
|
||||||
host=host,
|
host=host,
|
||||||
@ -121,9 +159,10 @@ class Audio(commands.Cog):
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
return # break infinite loop
|
return # break infinite loop
|
||||||
except Exception:
|
except asyncio.TimeoutError:
|
||||||
if not external:
|
log.error("Connecting to Lavalink server timed out, retrying...")
|
||||||
shutdown_lavalink_server()
|
if external is False and self._manager is not None:
|
||||||
|
await self._manager.shutdown()
|
||||||
await asyncio.sleep(1) # prevent busylooping
|
await asyncio.sleep(1) # prevent busylooping
|
||||||
|
|
||||||
async def event_handler(self, player, event_type, extra):
|
async def event_handler(self, player, event_type, extra):
|
||||||
@ -133,19 +172,19 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
async def _players_check():
|
async def _players_check():
|
||||||
try:
|
try:
|
||||||
get_players = [p for p in lavalink.players if p.current is not None]
|
get_single_title = lavalink.active_players()[0].current.title
|
||||||
get_single_title = get_players[0].current.title
|
|
||||||
if get_single_title == "Unknown title":
|
if get_single_title == "Unknown title":
|
||||||
get_single_title = get_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"):
|
||||||
get_single_title = get_single_title.rsplit("/", 1)[-1]
|
get_single_title = get_single_title.rsplit("/", 1)[-1]
|
||||||
elif "localtracks/" in get_players[0].current.uri:
|
elif "localtracks/" in lavalink.active_players()[0].current.uri:
|
||||||
get_single_title = "{} - {}".format(
|
get_single_title = "{} - {}".format(
|
||||||
get_players[0].current.author, get_players[0].current.title
|
lavalink.active_players()[0].current.author,
|
||||||
|
lavalink.active_players()[0].current.title,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
get_single_title = get_players[0].current.title
|
get_single_title = lavalink.active_players()[0].current.title
|
||||||
playing_servers = len(get_players)
|
playing_servers = len(lavalink.active_players())
|
||||||
except IndexError:
|
except IndexError:
|
||||||
get_single_title = None
|
get_single_title = None
|
||||||
playing_servers = 0
|
playing_servers = 0
|
||||||
@ -355,6 +394,87 @@ class Audio(commands.Cog):
|
|||||||
await self.config.guild(ctx.guild).jukebox_price.set(price)
|
await self.config.guild(ctx.guild).jukebox_price.set(price)
|
||||||
await self.config.guild(ctx.guild).jukebox.set(jukebox)
|
await self.config.guild(ctx.guild).jukebox.set(jukebox)
|
||||||
|
|
||||||
|
@audioset.command()
|
||||||
|
@checks.is_owner()
|
||||||
|
async def localpath(self, ctx, local_path=None):
|
||||||
|
"""Set the localtracks path if the Lavalink.jar is not run from the Audio data folder.
|
||||||
|
|
||||||
|
Leave the path blank to reset the path to the default, the Audio data directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not local_path:
|
||||||
|
await self.config.localpath.set(str(cog_data_path(raw_name="Audio")))
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("The localtracks path location has been reset to the default location.")
|
||||||
|
)
|
||||||
|
|
||||||
|
info_msg = _(
|
||||||
|
"This setting is only for bot owners to set a localtracks folder location "
|
||||||
|
"if the Lavalink.jar is being ran from outside of the Audio data directory.\n"
|
||||||
|
"In the example below, the full path for 'ParentDirectory' must be passed to this command.\n"
|
||||||
|
"The path must not contain spaces.\n"
|
||||||
|
"```\n"
|
||||||
|
"ParentDirectory\n"
|
||||||
|
" |__ localtracks (folder)\n"
|
||||||
|
" | |__ Awesome Album Name (folder)\n"
|
||||||
|
" | |__01 Cool Song.mp3\n"
|
||||||
|
" | |__02 Groovy Song.mp3\n"
|
||||||
|
" |\n"
|
||||||
|
" |__ Lavalink.jar\n"
|
||||||
|
" |__ application.yml\n"
|
||||||
|
"```\n"
|
||||||
|
"The folder path given to this command must contain the Lavalink.jar, the application.yml, and the localtracks folder.\n"
|
||||||
|
"Use this command with no path given to reset it to the default, the Audio data directory for this bot.\n"
|
||||||
|
"Do you want to continue to set the provided path for local tracks?"
|
||||||
|
)
|
||||||
|
info = await ctx.maybe_send_embed(info_msg)
|
||||||
|
|
||||||
|
start_adding_reactions(info, ReactionPredicate.YES_OR_NO_EMOJIS)
|
||||||
|
pred = ReactionPredicate.yes_or_no(info, ctx.author)
|
||||||
|
await ctx.bot.wait_for("reaction_add", check=pred)
|
||||||
|
|
||||||
|
if not pred.result:
|
||||||
|
try:
|
||||||
|
await info.delete()
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.getcwd() != local_path:
|
||||||
|
os.chdir(local_path)
|
||||||
|
os.listdir(local_path)
|
||||||
|
except OSError:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_("{local_path} does not seem like a valid path.").format(local_path=local_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
jar_check = os.path.isfile(local_path + "/Lavalink.jar")
|
||||||
|
yml_check = os.path.isfile(local_path + "/application.yml")
|
||||||
|
|
||||||
|
if not jar_check and not yml_check:
|
||||||
|
filelist = "a Lavalink.jar and an application.yml"
|
||||||
|
elif jar_check and not yml_check:
|
||||||
|
filelist = "an application.yml"
|
||||||
|
elif not jar_check and yml_check:
|
||||||
|
filelist = "a Lavalink.jar"
|
||||||
|
else:
|
||||||
|
filelist = None
|
||||||
|
if filelist is not None:
|
||||||
|
warn_msg = _(
|
||||||
|
"The path that was entered does not have {filelist} file in "
|
||||||
|
"that location. The path will still be saved, but please check the path and "
|
||||||
|
"the file location before attempting to play local tracks or start your "
|
||||||
|
"Lavalink.jar."
|
||||||
|
).format(filelist=filelist)
|
||||||
|
await self._embed_msg(ctx, warn_msg)
|
||||||
|
|
||||||
|
await self.config.localpath.set(local_path)
|
||||||
|
await self._embed_msg(
|
||||||
|
ctx, _("Localtracks path set to: {local_path}.").format(local_path=local_path)
|
||||||
|
)
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def maxlength(self, ctx, seconds):
|
async def maxlength(self, ctx, seconds):
|
||||||
@ -412,6 +532,7 @@ class Audio(commands.Cog):
|
|||||||
@audioset.command()
|
@audioset.command()
|
||||||
async def settings(self, ctx):
|
async def settings(self, ctx):
|
||||||
"""Show the current settings."""
|
"""Show the current settings."""
|
||||||
|
is_owner = ctx.author.id == self.bot.owner_id
|
||||||
data = await self.config.guild(ctx.guild).all()
|
data = await self.config.guild(ctx.guild).all()
|
||||||
global_data = await self.config.all()
|
global_data = await self.config.all()
|
||||||
dj_role_obj = ctx.guild.get_role(data["dj_role"])
|
dj_role_obj = ctx.guild.get_role(data["dj_role"])
|
||||||
@ -458,8 +579,10 @@ class Audio(commands.Cog):
|
|||||||
"---Lavalink Settings--- \n"
|
"---Lavalink Settings--- \n"
|
||||||
"Cog version: [{version}]\n"
|
"Cog version: [{version}]\n"
|
||||||
"Jar build: [{jarbuild}]\n"
|
"Jar build: [{jarbuild}]\n"
|
||||||
"External server: [{use_external_lavalink}]"
|
"External server: [{use_external_lavalink}]\n"
|
||||||
).format(version=__version__, jarbuild=jarbuild, **global_data)
|
).format(version=__version__, jarbuild=jarbuild, **global_data)
|
||||||
|
if is_owner:
|
||||||
|
msg += _("Localtracks path: [{localpath}]\n").format(**global_data)
|
||||||
|
|
||||||
embed = discord.Embed(colour=await ctx.embed_colour(), description=box(msg, lang="ini"))
|
embed = discord.Embed(colour=await ctx.embed_colour(), description=box(msg, lang="ini"))
|
||||||
return await ctx.send(embed=embed)
|
return await ctx.send(embed=embed)
|
||||||
@ -545,11 +668,11 @@ class Audio(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def audiostats(self, ctx):
|
async def audiostats(self, ctx):
|
||||||
"""Audio stats."""
|
"""Audio stats."""
|
||||||
server_num = len([p for p in lavalink.players if p.current is not None])
|
server_num = len(lavalink.active_players())
|
||||||
total_num = len([p for p in lavalink.players])
|
total_num = len(lavalink.all_players())
|
||||||
|
|
||||||
msg = ""
|
msg = ""
|
||||||
for p in lavalink.players:
|
for p in lavalink.all_players():
|
||||||
connect_start = p.fetch("connect")
|
connect_start = p.fetch("connect")
|
||||||
connect_dur = self._dynamic_time(
|
connect_dur = self._dynamic_time(
|
||||||
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
||||||
@ -778,11 +901,10 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
track_listing = []
|
track_listing = []
|
||||||
if ctx.invoked_with == "search":
|
if ctx.invoked_with == "search":
|
||||||
|
local_path = await self.config.localpath()
|
||||||
for localtrack_location in folder_list:
|
for localtrack_location in folder_list:
|
||||||
track_listing.append(
|
track_listing.append(
|
||||||
localtrack_location.replace(
|
localtrack_location.replace("{}/localtracks/".format(local_path), "")
|
||||||
"{}/localtracks/".format(cog_data_path(raw_name="Audio")), ""
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for localtrack_location in folder_list:
|
for localtrack_location in folder_list:
|
||||||
@ -808,15 +930,18 @@ class Audio(commands.Cog):
|
|||||||
await ctx.invoke(self.search, query=("folder:" + folder))
|
await ctx.invoke(self.search, query=("folder:" + folder))
|
||||||
|
|
||||||
async def _localtracks_check(self, ctx):
|
async def _localtracks_check(self, ctx):
|
||||||
audio_data = cog_data_path(raw_name="Audio")
|
audio_data = await self.config.localpath()
|
||||||
if os.getcwd() != audio_data:
|
if os.getcwd() != audio_data:
|
||||||
os.chdir(audio_data)
|
os.chdir(audio_data)
|
||||||
localtracks_folder = any(
|
localtracks_folder = any(
|
||||||
f for f in os.listdir(os.getcwd()) if not os.path.isfile(f) if f == "localtracks"
|
f for f in os.listdir(os.getcwd()) if not os.path.isfile(f) if f == "localtracks"
|
||||||
)
|
)
|
||||||
if not localtracks_folder:
|
if not localtracks_folder:
|
||||||
await self._embed_msg(ctx, _("No localtracks folder."))
|
if ctx.invoked_with == "start":
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
await self._embed_msg(ctx, _("No localtracks folder."))
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1072,10 +1197,9 @@ class Audio(commands.Cog):
|
|||||||
return await self._get_spotify_tracks(ctx, query)
|
return await self._get_spotify_tracks(ctx, query)
|
||||||
|
|
||||||
if query.startswith("localtrack:"):
|
if query.startswith("localtrack:"):
|
||||||
|
local_path = await self.config.localpath()
|
||||||
await self._localtracks_check(ctx)
|
await self._localtracks_check(ctx)
|
||||||
query = query.replace("localtrack:", "").replace(
|
query = query.replace("localtrack:", "").replace(((local_path) + "/"), "")
|
||||||
(str(cog_data_path(raw_name="Audio")) + "/"), ""
|
|
||||||
)
|
|
||||||
allowed_files = (".mp3", ".flac", ".ogg")
|
allowed_files = (".mp3", ".flac", ".ogg")
|
||||||
if not self._match_url(query) and not (query.lower().endswith(allowed_files)):
|
if not self._match_url(query) and not (query.lower().endswith(allowed_files)):
|
||||||
query = "ytsearch:{}".format(query)
|
query = "ytsearch:{}".format(query)
|
||||||
@ -1333,17 +1457,13 @@ class Audio(commands.Cog):
|
|||||||
song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"])
|
song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"])
|
||||||
try:
|
try:
|
||||||
track_url = await self._youtube_api_search(yt_key, song_info)
|
track_url = await self._youtube_api_search(yt_key, song_info)
|
||||||
except:
|
except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError):
|
||||||
error_embed = discord.Embed(
|
error_embed = discord.Embed(
|
||||||
colour=await ctx.embed_colour(),
|
colour=await ctx.embed_colour(),
|
||||||
title=_(
|
title=_("The connection was reset while loading the playlist."),
|
||||||
"The YouTube API key has not been set properly.\n"
|
|
||||||
"Use `{prefix}audioset youtubeapi` for instructions."
|
|
||||||
).format(prefix=ctx.prefix),
|
|
||||||
)
|
)
|
||||||
await playlist_msg.edit(embed=error_embed)
|
await playlist_msg.edit(embed=error_embed)
|
||||||
return None
|
return None
|
||||||
# let's complain about errors
|
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
yt_track = await player.get_tracks(track_url)
|
yt_track = await player.get_tracks(track_url)
|
||||||
@ -1831,6 +1951,8 @@ class Audio(commands.Cog):
|
|||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
for track in playlists[playlist_name]["tracks"]:
|
for track in playlists[playlist_name]["tracks"]:
|
||||||
if track["info"]["uri"].startswith("localtracks/"):
|
if track["info"]["uri"].startswith("localtracks/"):
|
||||||
|
if not await self._localtracks_check(ctx):
|
||||||
|
pass
|
||||||
if not os.path.isfile(track["info"]["uri"]):
|
if not os.path.isfile(track["info"]["uri"]):
|
||||||
continue
|
continue
|
||||||
if maxlength > 0:
|
if maxlength > 0:
|
||||||
@ -1914,7 +2036,10 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
playlist_msg = await ctx.send(embed=embed1)
|
playlist_msg = await ctx.send(embed=embed1)
|
||||||
for song_url in v2_playlist["playlist"]:
|
for song_url in v2_playlist["playlist"]:
|
||||||
track = await player.get_tracks(song_url)
|
try:
|
||||||
|
track = await player.get_tracks(song_url)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
track_obj = self._track_creator(player, other_track=track[0])
|
track_obj = self._track_creator(player, other_track=track[0])
|
||||||
track_list.append(track_obj)
|
track_list.append(track_obj)
|
||||||
@ -2251,9 +2376,8 @@ class Audio(commands.Cog):
|
|||||||
):
|
):
|
||||||
track_idx = i + 1
|
track_idx = i + 1
|
||||||
if command == "search":
|
if command == "search":
|
||||||
track_location = track.replace(
|
local_path = await self.config.localpath()
|
||||||
"localtrack:{}/localtracks/".format(cog_data_path(raw_name="Audio")), ""
|
track_location = track.replace("localtrack:{}/localtracks/".format(local_path), "")
|
||||||
)
|
|
||||||
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])
|
||||||
@ -2271,11 +2395,13 @@ class Audio(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _queue_clear(self, ctx):
|
async def _queue_clear(self, ctx):
|
||||||
"""Clears the queue."""
|
"""Clears the queue."""
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
try:
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
except KeyError:
|
||||||
|
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
||||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||||
if not self._player_check(ctx) or not player.queue:
|
if not self._player_check(ctx) or not player.queue:
|
||||||
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
|
||||||
if dj_enabled:
|
if dj_enabled:
|
||||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
|
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
|
||||||
ctx, ctx.author
|
ctx, ctx.author
|
||||||
@ -2288,7 +2414,10 @@ class Audio(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _queue_clean(self, ctx):
|
async def _queue_clean(self, ctx):
|
||||||
"""Removes songs from the queue if the requester is not in the voice channel."""
|
"""Removes songs from the queue if the requester is not in the voice channel."""
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
try:
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
except KeyError:
|
||||||
|
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
||||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||||
if not self._player_check(ctx) or not player.queue:
|
if not self._player_check(ctx) or not player.queue:
|
||||||
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
return await self._embed_msg(ctx, _("There's nothing in the queue."))
|
||||||
@ -2572,7 +2701,8 @@ class Audio(commands.Cog):
|
|||||||
if command == "search":
|
if command == "search":
|
||||||
return await ctx.invoke(self.play, query=("localtracks/{}".format(search_choice)))
|
return await ctx.invoke(self.play, query=("localtracks/{}".format(search_choice)))
|
||||||
search_choice = search_choice.replace("localtrack:", "")
|
search_choice = search_choice.replace("localtrack:", "")
|
||||||
if not search_choice.startswith(str(cog_data_path(raw_name="Audio"))):
|
local_path = await self.config.localpath()
|
||||||
|
if not search_choice.startswith(local_path):
|
||||||
return await ctx.invoke(
|
return await ctx.invoke(
|
||||||
self.search, query=("localfolder:{}".format(search_choice))
|
self.search, query=("localfolder:{}".format(search_choice))
|
||||||
)
|
)
|
||||||
@ -2633,14 +2763,10 @@ class Audio(commands.Cog):
|
|||||||
search_list += "`{}.` **{}**\n".format(search_track_num, track)
|
search_list += "`{}.` **{}**\n".format(search_track_num, track)
|
||||||
folder = False
|
folder = False
|
||||||
else:
|
else:
|
||||||
|
local_path = await self.config.localpath()
|
||||||
search_list += "`{}.` **{}**\n".format(
|
search_list += "`{}.` **{}**\n".format(
|
||||||
search_track_num,
|
search_track_num,
|
||||||
track.replace(
|
track.replace("localtrack:{}/localtracks/".format(local_path), ""),
|
||||||
"localtrack:{}/localtracks/".format(
|
|
||||||
str(cog_data_path(raw_name="Audio"))
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
folder = False
|
folder = False
|
||||||
try:
|
try:
|
||||||
@ -2773,8 +2899,8 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def skip(self, ctx):
|
async def skip(self, ctx, skip_to_track: int = None):
|
||||||
"""Skip to the next track."""
|
"""Skip to the next track, or to a given track number."""
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
return await self._embed_msg(ctx, _("Nothing playing."))
|
return await self._embed_msg(ctx, _("Nothing playing."))
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
@ -2791,6 +2917,10 @@ class Audio(commands.Cog):
|
|||||||
return await self._embed_msg(ctx, _("You need the DJ role to skip tracks."))
|
return await self._embed_msg(ctx, _("You need the DJ role to skip tracks."))
|
||||||
if vote_enabled:
|
if vote_enabled:
|
||||||
if not await self._can_instaskip(ctx, ctx.author):
|
if not await self._can_instaskip(ctx, ctx.author):
|
||||||
|
if skip_to_track is not None:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("Can't skip to a specific track in vote mode without the DJ role.")
|
||||||
|
)
|
||||||
if ctx.author.id in self.skip_votes[ctx.message.guild]:
|
if ctx.author.id in self.skip_votes[ctx.message.guild]:
|
||||||
self.skip_votes[ctx.message.guild].remove(ctx.author.id)
|
self.skip_votes[ctx.message.guild].remove(ctx.author.id)
|
||||||
reply = _("I removed your vote to skip.")
|
reply = _("I removed your vote to skip.")
|
||||||
@ -2823,9 +2953,9 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
return await self._embed_msg(ctx, reply)
|
return await self._embed_msg(ctx, reply)
|
||||||
else:
|
else:
|
||||||
return await self._skip_action(ctx)
|
return await self._skip_action(ctx, skip_to_track)
|
||||||
else:
|
else:
|
||||||
return await self._skip_action(ctx)
|
return await self._skip_action(ctx, skip_to_track)
|
||||||
|
|
||||||
async def _can_instaskip(self, ctx, member):
|
async def _can_instaskip(self, ctx, member):
|
||||||
mod_role = await ctx.bot.db.guild(ctx.guild).mod_role()
|
mod_role = await ctx.bot.db.guild(ctx.guild).mod_role()
|
||||||
@ -2884,7 +3014,7 @@ class Audio(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _skip_action(self, ctx):
|
async def _skip_action(self, ctx, skip_to_track: int = None):
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
if not player.queue:
|
if not player.queue:
|
||||||
try:
|
try:
|
||||||
@ -2909,23 +3039,57 @@ class Audio(commands.Cog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return await ctx.send(embed=embed)
|
return await ctx.send(embed=embed)
|
||||||
|
queue_to_append = []
|
||||||
|
if skip_to_track is not None and skip_to_track != 1:
|
||||||
|
if skip_to_track < 1:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("Track number must be equal to or greater than 1.")
|
||||||
|
)
|
||||||
|
elif skip_to_track > len(player.queue):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_(
|
||||||
|
"There are only {queuelen} songs currently queued.".format(
|
||||||
|
queuelen=len(player.queue)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif player.shuffle:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("Can't skip to a track while shuffle is enabled.")
|
||||||
|
)
|
||||||
|
nexttrack = player.queue[min(skip_to_track - 1, len(player.queue) - 1)]
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
title=_("{skip_to_track} Tracks Skipped".format(skip_to_track=skip_to_track)),
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
if player.repeat:
|
||||||
|
queue_to_append = player.queue[0 : min(skip_to_track - 1, len(player.queue) - 1)]
|
||||||
|
player.queue = player.queue[
|
||||||
|
min(skip_to_track - 1, len(player.queue) - 1) : len(player.queue)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
title=_("Track Skipped"),
|
||||||
|
description=await self._get_description(player.current),
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
if "localtracks" in player.current.uri:
|
await player.play()
|
||||||
if not player.current.title == "Unknown title":
|
player.queue += queue_to_append
|
||||||
description = "**{} - {}**\n{}".format(
|
|
||||||
player.current.author,
|
async def _get_description(self, track):
|
||||||
player.current.title,
|
if "localtracks" in track.uri:
|
||||||
player.current.uri.replace("localtracks/", ""),
|
if not track.title == "Unknown title":
|
||||||
|
return "**{} - {}**\n{}".format(
|
||||||
|
track.author, track.title, track.uri.replace("localtracks/", "")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
description = "{}".format(player.current.uri.replace("localtracks/", ""))
|
return "{}".format(track.uri.replace("localtracks/", ""))
|
||||||
else:
|
else:
|
||||||
description = "**[{}]({})**".format(player.current.title, player.current.uri)
|
return "**[{}]({})**".format(track.title, track.uri)
|
||||||
embed = discord.Embed(
|
|
||||||
colour=await ctx.embed_colour(), title=_("Track Skipped"), description=description
|
|
||||||
)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
await player.skip()
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -3020,19 +3184,16 @@ class Audio(commands.Cog):
|
|||||||
await self.config.use_external_lavalink.set(not external)
|
await self.config.use_external_lavalink.set(not external)
|
||||||
|
|
||||||
if external:
|
if external:
|
||||||
await self.config.host.set("localhost")
|
|
||||||
await self.config.password.set("youshallnotpass")
|
|
||||||
await self.config.rest_port.set(2333)
|
|
||||||
await self.config.ws_port.set(2332)
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
colour=await ctx.embed_colour(),
|
colour=await ctx.embed_colour(),
|
||||||
title=_("External lavalink server: {true_or_false}.").format(
|
title=_("External lavalink server: {true_or_false}.").format(
|
||||||
true_or_false=not external
|
true_or_false=not external
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
embed.set_footer(text=_("Defaults reset."))
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
else:
|
else:
|
||||||
|
if self._manager is not None:
|
||||||
|
await self._manager.shutdown()
|
||||||
await self._embed_msg(
|
await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
_("External lavalink server: {true_or_false}.").format(true_or_false=not external),
|
_("External lavalink server: {true_or_false}.").format(true_or_false=not external),
|
||||||
@ -3106,7 +3267,10 @@ class Audio(commands.Cog):
|
|||||||
self._restart_connect()
|
self._restart_connect()
|
||||||
|
|
||||||
async def _channel_check(self, ctx):
|
async def _channel_check(self, ctx):
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
try:
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
in_channel = sum(
|
in_channel = sum(
|
||||||
not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members
|
not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members
|
||||||
@ -3145,6 +3309,8 @@ class Audio(commands.Cog):
|
|||||||
async def _check_external(self):
|
async def _check_external(self):
|
||||||
external = await self.config.use_external_lavalink()
|
external = await self.config.use_external_lavalink()
|
||||||
if not external:
|
if not external:
|
||||||
|
if self._manager is not None:
|
||||||
|
await self._manager.shutdown()
|
||||||
await self.config.use_external_lavalink.set(True)
|
await self.config.use_external_lavalink.set(True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -3191,7 +3357,7 @@ class Audio(commands.Cog):
|
|||||||
stop_times = {}
|
stop_times = {}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
for p in lavalink.players:
|
for p in lavalink.all_players():
|
||||||
server = p.channel.guild
|
server = p.channel.guild
|
||||||
|
|
||||||
if [self.bot.user] == p.channel.members:
|
if [self.bot.user] == p.channel.members:
|
||||||
@ -3266,13 +3432,6 @@ class Audio(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
return self.bot.color
|
return self.bot.color
|
||||||
|
|
||||||
async def _get_playing(self, ctx):
|
|
||||||
if self._player_check(ctx):
|
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
|
||||||
return len([player for p in lavalink.players if p.is_playing])
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
async def _localtracks_folders(self, ctx):
|
async def _localtracks_folders(self, ctx):
|
||||||
if not await self._localtracks_check(ctx):
|
if not await self._localtracks_check(ctx):
|
||||||
return
|
return
|
||||||
@ -3315,6 +3474,8 @@ class Audio(commands.Cog):
|
|||||||
try:
|
try:
|
||||||
lavalink.get_player(ctx.guild.id)
|
lavalink.get_player(ctx.guild.id)
|
||||||
return True
|
return True
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -3422,11 +3583,14 @@ class Audio(commands.Cog):
|
|||||||
async def _youtube_api_search(self, yt_key, query):
|
async def _youtube_api_search(self, yt_key, query):
|
||||||
params = {"q": query, "part": "id", "key": yt_key, "maxResults": 1, "type": "video"}
|
params = {"q": query, "part": "id", "key": yt_key, "maxResults": 1, "type": "video"}
|
||||||
yt_url = "https://www.googleapis.com/youtube/v3/search"
|
yt_url = "https://www.googleapis.com/youtube/v3/search"
|
||||||
async with self.session.request("GET", yt_url, params=params) as r:
|
try:
|
||||||
if r.status == 400:
|
async with self.session.request("GET", yt_url, params=params) as r:
|
||||||
return None
|
if r.status == 400:
|
||||||
else:
|
return None
|
||||||
search_response = await r.json()
|
else:
|
||||||
|
search_response = await r.json()
|
||||||
|
except RuntimeError:
|
||||||
|
return None
|
||||||
for search_result in search_response.get("items", []):
|
for search_result in search_response.get("items", []):
|
||||||
if search_result["id"]["kind"] == "youtube#video":
|
if search_result["id"]["kind"] == "youtube#video":
|
||||||
return "https://www.youtube.com/watch?v={}".format(search_result["id"]["videoId"])
|
return "https://www.youtube.com/watch?v={}".format(search_result["id"]["videoId"])
|
||||||
@ -3503,7 +3667,7 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
def cog_unload(self):
|
def cog_unload(self):
|
||||||
if not self._cleaned_up:
|
if not self._cleaned_up:
|
||||||
self.session.detach()
|
self.bot.loop.create_task(self.session.close())
|
||||||
|
|
||||||
if self._disconnect_task:
|
if self._disconnect_task:
|
||||||
self._disconnect_task.cancel()
|
self._disconnect_task.cancel()
|
||||||
@ -3513,25 +3677,8 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
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())
|
||||||
shutdown_lavalink_server()
|
if self._manager is not None:
|
||||||
|
self.bot.loop.create_task(self._manager.shutdown())
|
||||||
self._cleaned_up = True
|
self._cleaned_up = True
|
||||||
|
|
||||||
__del__ = cog_unload
|
__del__ = cog_unload
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_guild_remove(self, guild: discord.Guild):
|
|
||||||
"""
|
|
||||||
This is to clean up players when
|
|
||||||
the bot either leaves or is removed from a guild
|
|
||||||
"""
|
|
||||||
channels = {
|
|
||||||
x # x is a voice_channel
|
|
||||||
for y in [g.voice_channels for g in self.bot.guilds]
|
|
||||||
for x in y # y is a list of voice channels
|
|
||||||
} # Yes, this is ugly. It's also the most performant and commented.
|
|
||||||
|
|
||||||
zombie_players = {p for p in lavalink.player_manager.players if p.channel not in channels}
|
|
||||||
# Do not unroll to combine with next line.
|
|
||||||
# Can result in iterator changing size during context switching.
|
|
||||||
for zombie in zombie_players:
|
|
||||||
await zombie.destroy()
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
server:
|
server:
|
||||||
|
host: "localhost"
|
||||||
port: 2333 # REST server
|
port: 2333 # REST server
|
||||||
lavalink:
|
lavalink:
|
||||||
server:
|
server:
|
||||||
password: "youshallnotpass"
|
password: "youshallnotpass"
|
||||||
ws:
|
|
||||||
host: "localhost"
|
|
||||||
port: 2332
|
|
||||||
sources:
|
sources:
|
||||||
youtube: true
|
youtube: true
|
||||||
bandcamp: true
|
bandcamp: true
|
||||||
|
|||||||
@ -1,172 +1,243 @@
|
|||||||
import shlex
|
import itertools
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncio.subprocess
|
import asyncio.subprocess
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from subprocess import Popen, DEVNULL
|
import tempfile
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, ClassVar, List
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
import aiohttp
|
||||||
|
|
||||||
import redbot.core
|
from redbot.core import data_manager
|
||||||
|
|
||||||
_JavaVersion = Tuple[int, int]
|
JAR_VERSION = "3.2.0.3"
|
||||||
|
JAR_BUILD = 751
|
||||||
|
LAVALINK_DOWNLOAD_URL = (
|
||||||
|
f"https://github.com/Cog-Creators/Lavalink-Jars/releases/download/{JAR_VERSION}_{JAR_BUILD}/"
|
||||||
|
f"Lavalink.jar"
|
||||||
|
)
|
||||||
|
LAVALINK_DOWNLOAD_DIR = data_manager.cog_data_path(raw_name="Audio")
|
||||||
|
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
||||||
|
|
||||||
|
BUNDLED_APP_YML = pathlib.Path(__file__).parent / "data" / "application.yml"
|
||||||
|
LAVALINK_APP_YML = LAVALINK_DOWNLOAD_DIR / "application.yml"
|
||||||
|
|
||||||
|
READY_LINE_RE = re.compile(rb"Started Launcher in \S+ seconds")
|
||||||
|
BUILD_LINE_RE = re.compile(rb"Build:\s+(?P<build>\d+)")
|
||||||
|
|
||||||
log = logging.getLogger("red.audio.manager")
|
log = logging.getLogger("red.audio.manager")
|
||||||
|
|
||||||
proc = None
|
|
||||||
shutdown = False
|
|
||||||
|
|
||||||
|
class ServerManager:
|
||||||
|
|
||||||
def has_java_error(pid):
|
_java_available: ClassVar[Optional[bool]] = None
|
||||||
from . import LAVALINK_DOWNLOAD_DIR
|
_java_version: ClassVar[Optional[Tuple[int, int]]] = None
|
||||||
|
_up_to_date: ClassVar[Optional[bool]] = None
|
||||||
|
|
||||||
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(pid)
|
_blacklisted_archs = ["armv6l", "aarch32", "aarch64"]
|
||||||
return poss_error_file.exists()
|
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.ready = asyncio.Event()
|
||||||
|
|
||||||
async def monitor_lavalink_server(loop):
|
self._proc: Optional[asyncio.subprocess.Process] = None
|
||||||
global shutdown
|
self._monitor_task: Optional[asyncio.Task] = None
|
||||||
while shutdown is False:
|
self._shutdown: bool = False
|
||||||
if proc.poll() is not None:
|
|
||||||
break
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
if shutdown is False:
|
async def start(self) -> None:
|
||||||
# Lavalink was shut down by something else
|
arch_name = platform.machine()
|
||||||
log.info("Lavalink jar shutdown.")
|
if arch_name in self._blacklisted_archs:
|
||||||
shutdown = True
|
raise asyncio.CancelledError(
|
||||||
if not has_java_error(proc.pid):
|
"You are attempting to run Lavalink audio on an unsupported machine architecture."
|
||||||
log.info("Restarting Lavalink jar.")
|
|
||||||
await start_lavalink_server(loop)
|
|
||||||
else:
|
|
||||||
log.error(
|
|
||||||
"Your Java is borked. Please find the hs_err_pid{}.log file"
|
|
||||||
" in the Audio data folder and report this issue.".format(proc.pid)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._proc is not None:
|
||||||
|
if self._proc.returncode is None:
|
||||||
|
raise RuntimeError("Internal Lavalink server is already running")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Server manager has already been used - create another one")
|
||||||
|
|
||||||
async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
|
await self.maybe_download_jar()
|
||||||
java_available = shutil.which("java") is not None
|
|
||||||
if not java_available:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
version = await get_java_version(loop)
|
# Copy the application.yml across.
|
||||||
return (2, 0) > version >= (1, 8) or version >= (8, 0), version
|
# For people to customise their Lavalink server configuration they need to run it
|
||||||
|
# externally
|
||||||
|
shutil.copyfile(BUNDLED_APP_YML, LAVALINK_APP_YML)
|
||||||
|
|
||||||
|
args = await self._get_jar_args()
|
||||||
|
self._proc = await asyncio.subprocess.create_subprocess_exec(
|
||||||
|
*args,
|
||||||
|
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
|
||||||
async def get_java_version(loop) -> _JavaVersion:
|
log.info("Internal Lavalink server started. PID: %s", self._proc.pid)
|
||||||
"""
|
|
||||||
This assumes we've already checked that java exists.
|
|
||||||
"""
|
|
||||||
_proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
|
|
||||||
"java",
|
|
||||||
"-version",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
loop=loop,
|
|
||||||
)
|
|
||||||
# java -version outputs to stderr
|
|
||||||
_, err = await _proc.communicate()
|
|
||||||
|
|
||||||
version_info: str = err.decode("utf-8")
|
try:
|
||||||
# We expect the output to look something like:
|
await asyncio.wait_for(self._wait_for_launcher(), timeout=120)
|
||||||
# $ java -version
|
except asyncio.TimeoutError:
|
||||||
# ...
|
log.warning("Timeout occurred whilst waiting for internal Lavalink server to be ready")
|
||||||
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
|
||||||
# ...
|
|
||||||
# We only care about the major and minor parts though.
|
|
||||||
version_line_re = re.compile(
|
|
||||||
r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?(?:-[A-Za-z0-9]+)?"'
|
|
||||||
)
|
|
||||||
short_version_re = re.compile(r'version "(?P<major>\d+)"')
|
|
||||||
|
|
||||||
lines = version_info.splitlines()
|
self._monitor_task = asyncio.create_task(self._monitor())
|
||||||
for line in lines:
|
|
||||||
match = version_line_re.search(line)
|
|
||||||
short_match = short_version_re.search(line)
|
|
||||||
if match:
|
|
||||||
return int(match["major"]), int(match["minor"])
|
|
||||||
elif short_match:
|
|
||||||
return int(short_match["major"]), 0
|
|
||||||
|
|
||||||
raise RuntimeError(
|
@classmethod
|
||||||
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
async def _get_jar_args(cls) -> List[str]:
|
||||||
"issue tracker."
|
java_available, java_version = await cls._has_java()
|
||||||
)
|
if not java_available:
|
||||||
|
raise RuntimeError("You must install Java 1.8+ for Lavalink to run.")
|
||||||
|
|
||||||
|
if java_version == (1, 8):
|
||||||
|
extra_flags = ["-Dsun.zip.disableMemoryMapping=true"]
|
||||||
|
elif java_version >= (11, 0):
|
||||||
|
extra_flags = ["-Djdk.tls.client.protocols=TLSv1.2"]
|
||||||
|
else:
|
||||||
|
extra_flags = []
|
||||||
|
|
||||||
async def start_lavalink_server(loop):
|
return ["java", *extra_flags, "-jar", str(LAVALINK_JAR_FILE)]
|
||||||
java_available, java_version = await has_java(loop)
|
|
||||||
if not java_available:
|
|
||||||
raise RuntimeError("You must install Java 1.8+ for Lavalink to run.")
|
|
||||||
|
|
||||||
if java_version == (1, 8):
|
@classmethod
|
||||||
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
|
async def _has_java(cls) -> Tuple[bool, Optional[Tuple[int, int]]]:
|
||||||
elif java_version >= (11, 0):
|
if cls._java_available is not None:
|
||||||
extra_flags = "-Djdk.tls.client.protocols=TLSv1.2"
|
# Return cached value if we've checked this before
|
||||||
else:
|
return cls._java_available, cls._java_version
|
||||||
extra_flags = ""
|
java_available = shutil.which("java") is not None
|
||||||
|
if not java_available:
|
||||||
|
cls.java_available = False
|
||||||
|
cls.java_version = None
|
||||||
|
else:
|
||||||
|
cls._java_version = version = await cls._get_java_version()
|
||||||
|
cls._java_available = (2, 0) > version >= (1, 8) or version >= (8, 0)
|
||||||
|
return cls._java_available, cls._java_version
|
||||||
|
|
||||||
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
|
@staticmethod
|
||||||
|
async def _get_java_version() -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
This assumes we've already checked that java exists.
|
||||||
|
"""
|
||||||
|
_proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
|
||||||
|
"java", "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
# java -version outputs to stderr
|
||||||
|
_, err = await _proc.communicate()
|
||||||
|
|
||||||
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
|
version_info: str = err.decode("utf-8")
|
||||||
|
# We expect the output to look something like:
|
||||||
|
# $ java -version
|
||||||
|
# ...
|
||||||
|
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
||||||
|
# ...
|
||||||
|
# We only care about the major and minor parts though.
|
||||||
|
version_line_re = re.compile(
|
||||||
|
r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?(?:-[A-Za-z0-9]+)?"'
|
||||||
|
)
|
||||||
|
short_version_re = re.compile(r'version "(?P<major>\d+)"')
|
||||||
|
|
||||||
global proc
|
lines = version_info.splitlines()
|
||||||
|
for line in lines:
|
||||||
|
match = version_line_re.search(line)
|
||||||
|
short_match = short_version_re.search(line)
|
||||||
|
if match:
|
||||||
|
return int(match["major"]), int(match["minor"])
|
||||||
|
elif short_match:
|
||||||
|
return int(short_match["major"]), 0
|
||||||
|
|
||||||
if proc and proc.poll() is None:
|
raise RuntimeError(
|
||||||
return # already running
|
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
||||||
|
"issue tracker."
|
||||||
|
)
|
||||||
|
|
||||||
proc = Popen(
|
async def _wait_for_launcher(self) -> None:
|
||||||
shlex.split(start_cmd, posix=os.name == "posix"),
|
log.debug("Waiting for Lavalink server to be ready")
|
||||||
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
for i in itertools.cycle(range(50)):
|
||||||
stdout=DEVNULL,
|
line = await self._proc.stdout.readline()
|
||||||
stderr=DEVNULL,
|
if READY_LINE_RE.search(line):
|
||||||
)
|
self.ready.set()
|
||||||
|
break
|
||||||
|
if self._proc.returncode is not None:
|
||||||
|
log.critical("Internal lavalink server exited early")
|
||||||
|
if i == 49:
|
||||||
|
# Sleep after 50 lines to prevent busylooping
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
log.info("Lavalink jar started. PID: {}".format(proc.pid))
|
async def _monitor(self) -> None:
|
||||||
global shutdown
|
while self._proc.returncode is None:
|
||||||
shutdown = False
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
loop.create_task(monitor_lavalink_server(loop))
|
# This task hasn't been cancelled - Lavalink was shut down by something else
|
||||||
|
log.info("Internal Lavalink jar shutdown unexpectedly")
|
||||||
|
if not self._has_java_error():
|
||||||
|
log.info("Restarting internal Lavalink server")
|
||||||
|
await self.start()
|
||||||
|
else:
|
||||||
|
log.critical(
|
||||||
|
"Your Java is borked. Please find the hs_err_pid{}.log file"
|
||||||
|
" in the Audio data folder and report this issue.",
|
||||||
|
self._proc.pid,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _has_java_error(self) -> bool:
|
||||||
|
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(self._proc.pid)
|
||||||
|
return poss_error_file.exists()
|
||||||
|
|
||||||
def shutdown_lavalink_server():
|
async def shutdown(self) -> None:
|
||||||
global shutdown
|
if self._shutdown is True or self._proc is None:
|
||||||
shutdown = True
|
# For convenience, calling this method more than once or calling it before starting it
|
||||||
global proc
|
# does nothing.
|
||||||
if proc is not None:
|
return
|
||||||
log.info("Shutting down lavalink server.")
|
log.info("Shutting down internal Lavalink server")
|
||||||
proc.terminate()
|
if self._monitor_task is not None:
|
||||||
proc.wait()
|
self._monitor_task.cancel()
|
||||||
proc = None
|
self._proc.terminate()
|
||||||
|
await self._proc.wait()
|
||||||
|
self._shutdown = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _download_jar() -> None:
|
||||||
|
log.info("Downloading Lavalink.jar...")
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(LAVALINK_DOWNLOAD_URL) as response:
|
||||||
|
if response.status == 404:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Lavalink jar version {JAR_VERSION}_{JAR_BUILD} hasn't been published"
|
||||||
|
)
|
||||||
|
fd, path = tempfile.mkstemp()
|
||||||
|
file = open(fd, "wb")
|
||||||
|
try:
|
||||||
|
chunk = await response.content.read(1024)
|
||||||
|
while chunk:
|
||||||
|
file.write(chunk)
|
||||||
|
chunk = await response.content.read(1024)
|
||||||
|
file.flush()
|
||||||
|
finally:
|
||||||
|
file.close()
|
||||||
|
pathlib.Path(path).replace(LAVALINK_JAR_FILE)
|
||||||
|
|
||||||
async def download_lavalink(session):
|
@classmethod
|
||||||
from . import LAVALINK_DOWNLOAD_URL, LAVALINK_JAR_FILE
|
async def _is_up_to_date(cls):
|
||||||
|
if cls._up_to_date is True:
|
||||||
|
# Return cached value if we've checked this before
|
||||||
|
return True
|
||||||
|
args = await cls._get_jar_args()
|
||||||
|
args.append("--version")
|
||||||
|
_proc = await asyncio.subprocess.create_subprocess_exec(
|
||||||
|
*args,
|
||||||
|
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
stdout = (await _proc.communicate())[0]
|
||||||
|
match = BUILD_LINE_RE.search(stdout)
|
||||||
|
if not match:
|
||||||
|
# Output is unexpected, suspect corrupted jarfile
|
||||||
|
return False
|
||||||
|
build = int(match["build"])
|
||||||
|
cls._up_to_date = build == JAR_BUILD
|
||||||
|
return cls._up_to_date
|
||||||
|
|
||||||
with LAVALINK_JAR_FILE.open(mode="wb") as f:
|
@classmethod
|
||||||
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
|
async def maybe_download_jar(cls):
|
||||||
while True:
|
if not (LAVALINK_JAR_FILE.exists() and await cls._is_up_to_date()):
|
||||||
chunk = await resp.content.read(512)
|
await cls._download_jar()
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
|
|
||||||
async def maybe_download_lavalink(loop, cog):
|
|
||||||
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE, BUNDLED_APP_YML_FILE, APP_YML_FILE
|
|
||||||
|
|
||||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
|
||||||
current_build = redbot.VersionInfo.from_json(await cog.config.current_version())
|
|
||||||
|
|
||||||
if not jar_exists or current_build < redbot.core.version_info:
|
|
||||||
log.info("Downloading Lavalink.jar")
|
|
||||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
async with ClientSession(loop=loop) as session:
|
|
||||||
await download_lavalink(session)
|
|
||||||
await cog.config.current_version.set(redbot.core.version_info.to_json())
|
|
||||||
|
|
||||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from redbot.core.i18n import Translator, cog_i18n
|
|||||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||||
from redbot.cogs.mod.log import log
|
from redbot.cogs.mod.log import log
|
||||||
from redbot.core.utils.predicates import MessagePredicate
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
from .converters import RawMessageIds
|
||||||
|
|
||||||
_ = Translator("Cleanup", __file__)
|
_ = Translator("Cleanup", __file__)
|
||||||
|
|
||||||
@ -211,7 +212,9 @@ class Cleanup(commands.Cog):
|
|||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
async def after(
|
||||||
|
self, ctx: commands.Context, message_id: RawMessageIds, delete_pinned: bool = False
|
||||||
|
):
|
||||||
"""Delete all messages after a specified message.
|
"""Delete all messages after a specified message.
|
||||||
|
|
||||||
To get a message id, enable developer mode in Discord's
|
To get a message id, enable developer mode in Discord's
|
||||||
@ -242,7 +245,11 @@ class Cleanup(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def before(
|
async def before(
|
||||||
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
message_id: RawMessageIds,
|
||||||
|
number: int,
|
||||||
|
delete_pinned: bool = False,
|
||||||
):
|
):
|
||||||
"""Deletes X messages before specified message.
|
"""Deletes X messages before specified message.
|
||||||
|
|
||||||
@ -255,7 +262,7 @@ class Cleanup(commands.Cog):
|
|||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
try:
|
try:
|
||||||
before = await channel.get_message(message_id)
|
before = await channel.fetch_message(message_id)
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
return await ctx.send(_("Message not found."))
|
return await ctx.send(_("Message not found."))
|
||||||
|
|
||||||
@ -271,6 +278,48 @@ class Cleanup(commands.Cog):
|
|||||||
|
|
||||||
await mass_purge(to_delete, channel)
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def between(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
one: RawMessageIds,
|
||||||
|
two: RawMessageIds,
|
||||||
|
delete_pinned: bool = False,
|
||||||
|
):
|
||||||
|
"""Delete the messages between Messsage One and Message Two, providing the messages IDs.
|
||||||
|
|
||||||
|
The first message ID should be the older message and the second one the newer.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`[p]cleanup between 123456789123456789 987654321987654321`
|
||||||
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
try:
|
||||||
|
mone = await channel.fetch_message(one)
|
||||||
|
except discord.errors.Notfound:
|
||||||
|
return await ctx.send(
|
||||||
|
_("Could not find a message with the ID of {id}.".format(id=one))
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
mtwo = await channel.fetch_message(two)
|
||||||
|
except discord.errors.Notfound:
|
||||||
|
return await ctx.send(
|
||||||
|
_("Could not find a message with the ID of {id}.".format(id=two))
|
||||||
|
)
|
||||||
|
to_delete = await self.get_messages_for_deletion(
|
||||||
|
channel=channel, before=mtwo, after=mone, delete_pinned=delete_pinned
|
||||||
|
)
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
|
author.name, author.id, len(to_delete), channel.name
|
||||||
|
)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
|||||||
12
redbot/cogs/cleanup/converters.py
Normal file
12
redbot/cogs/cleanup/converters.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from redbot.core.commands import Converter, BadArgument
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("Cleanup", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class RawMessageIds(Converter):
|
||||||
|
async def convert(self, ctx, argument):
|
||||||
|
if argument.isnumeric() and len(argument) >= 17:
|
||||||
|
return int(argument)
|
||||||
|
|
||||||
|
raise BadArgument(_("{} doesn't look like a valid message ID.").format(argument))
|
||||||
@ -466,7 +466,7 @@ class CustomCommands(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# wrap the command here so it won't register with the bot
|
# wrap the command here so it won't register with the bot
|
||||||
fake_cc = commands.Command(ctx.invoked_with, self.cc_callback)
|
fake_cc = commands.command(name=ctx.invoked_with)(self.cc_callback)
|
||||||
fake_cc.params = self.prepare_args(raw_response)
|
fake_cc.params = self.prepare_args(raw_response)
|
||||||
ctx.command = fake_cc
|
ctx.command = fake_cc
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from sys import path as syspath
|
|||||||
from typing import Tuple, Union, Iterable
|
from typing import Tuple, Union, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import checks, commands, Config
|
from redbot.core import checks, commands, Config, version_info as red_version_info
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
@ -303,6 +303,26 @@ class Downloader(commands.Cog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
ignore_max = cog.min_bot_version > cog.max_bot_version
|
||||||
|
if (
|
||||||
|
cog.min_bot_version > red_version_info
|
||||||
|
or not ignore_max
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
await ctx.send(
|
||||||
|
_("This cog requires at least Red version {min_version}").format(
|
||||||
|
min_version=cog.min_bot_version
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
""
|
||||||
|
if ignore_max
|
||||||
|
else _(" and at most {max_version}").format(max_version=cog.max_bot_version)
|
||||||
|
)
|
||||||
|
+ _(", but you have {current_version}, aborting install.").format(
|
||||||
|
current_version=red_version_info
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if not await repo.install_requirements(cog, self.LIB_PATH):
|
if not await repo.install_requirements(cog, self.LIB_PATH):
|
||||||
libraries = humanize_list(tuple(map(inline, cog.requirements)))
|
libraries = humanize_list(tuple(map(inline, cog.requirements)))
|
||||||
@ -344,7 +364,8 @@ class Downloader(commands.Cog):
|
|||||||
|
|
||||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||||
if poss_installed_path.exists():
|
if poss_installed_path.exists():
|
||||||
ctx.bot.unload_extension(real_name)
|
with contextlib.suppress(commands.ExtensionNotLoaded):
|
||||||
|
ctx.bot.unload_extension(real_name)
|
||||||
await self._delete_cog(poss_installed_path)
|
await self._delete_cog(poss_installed_path)
|
||||||
uninstalled_cogs.append(inline(real_name))
|
uninstalled_cogs.append(inline(real_name))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -8,6 +8,8 @@ from typing import MutableMapping, Any, TYPE_CHECKING
|
|||||||
from .log import log
|
from .log import log
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@ -72,7 +74,8 @@ class Installable(RepoJSONMixin):
|
|||||||
self.repo_name = self._location.parent.stem
|
self.repo_name = self._location.parent.stem
|
||||||
|
|
||||||
self.author = ()
|
self.author = ()
|
||||||
self.bot_version = (3, 0, 0)
|
self.min_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
|
||||||
@ -157,10 +160,16 @@ class Installable(RepoJSONMixin):
|
|||||||
self.author = author
|
self.author = author
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot_version = tuple(info.get("bot_version", [3, 0, 0]))
|
min_bot_version = VersionInfo.from_str(str(info.get("min_bot_version", __version__)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
bot_version = self.bot_version
|
min_bot_version = self.min_bot_version
|
||||||
self.bot_version = bot_version
|
self.min_bot_version = min_bot_version
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_bot_version = VersionInfo.from_str(str(info.get("max_bot_version", __version__)))
|
||||||
|
except ValueError:
|
||||||
|
max_bot_version = self.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]))
|
||||||
|
|||||||
@ -355,34 +355,56 @@ class Economy(commands.Cog):
|
|||||||
author = ctx.author
|
author = ctx.author
|
||||||
if top < 1:
|
if top < 1:
|
||||||
top = 10
|
top = 10
|
||||||
if (
|
if await bank.is_global() and show_global:
|
||||||
await bank.is_global() and show_global
|
# show_global is only applicable if bank is global
|
||||||
): # show_global is only applicable if bank is global
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=None)
|
||||||
guild = None
|
|
||||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
|
||||||
header = "{pound:4}{name:36}{score:2}\n".format(
|
|
||||||
pound="#", name=_("Name"), score=_("Score")
|
|
||||||
)
|
|
||||||
highscores = [
|
|
||||||
(
|
|
||||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
|
||||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
|
||||||
)
|
|
||||||
if acc[0] != author.id
|
|
||||||
else (
|
|
||||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
|
||||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
|
||||||
)
|
|
||||||
for pos, acc in enumerate(bank_sorted, 1)
|
|
||||||
]
|
|
||||||
if highscores:
|
|
||||||
pages = [
|
|
||||||
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
|
||||||
for x in range(0, len(highscores), 10)
|
|
||||||
]
|
|
||||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no accounts in the bank."))
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||||
|
try:
|
||||||
|
bal_len = len(str(bank_sorted[0][1]["balance"]))
|
||||||
|
# first user is the largest we'll see
|
||||||
|
except IndexError:
|
||||||
|
return await ctx.send(_("There are no accounts in the bank."))
|
||||||
|
pound_len = len(str(len(bank_sorted)))
|
||||||
|
header = "{pound:{pound_len}}{score:{bal_len}}{name:2}\n".format(
|
||||||
|
pound="#",
|
||||||
|
name=_("Name"),
|
||||||
|
score=_("Score"),
|
||||||
|
bal_len=bal_len + 6,
|
||||||
|
pound_len=pound_len + 3,
|
||||||
|
)
|
||||||
|
highscores = []
|
||||||
|
pos = 1
|
||||||
|
temp_msg = header
|
||||||
|
for acc in bank_sorted:
|
||||||
|
try:
|
||||||
|
name = guild.get_member(acc[0]).display_name
|
||||||
|
except AttributeError:
|
||||||
|
user_id = ""
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
user_id = f"({str(acc[0])})"
|
||||||
|
name = f"{acc[1]['name']} {user_id}"
|
||||||
|
balance = acc[1]["balance"]
|
||||||
|
|
||||||
|
if acc[0] != author.id:
|
||||||
|
temp_msg += f"{f'{pos}.': <{pound_len+2}} {balance: <{bal_len + 5}} {name}\n"
|
||||||
|
|
||||||
|
else:
|
||||||
|
temp_msg += (
|
||||||
|
f"{f'{pos}.': <{pound_len+2}} "
|
||||||
|
f"{balance: <{bal_len + 5}} "
|
||||||
|
f"<<{author.display_name}>>\n"
|
||||||
|
)
|
||||||
|
if pos % 10 == 0:
|
||||||
|
highscores.append(box(temp_msg, lang="md"))
|
||||||
|
temp_msg = header
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if temp_msg != header:
|
||||||
|
highscores.append(box(temp_msg, lang="md"))
|
||||||
|
|
||||||
|
if highscores:
|
||||||
|
await menu(ctx, highscores, DEFAULT_CONTROLS)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config
|
from redbot.core import Config, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +20,13 @@ class MixinMeta(ABC):
|
|||||||
self.ban_queue: List[Tuple[int, int]]
|
self.ban_queue: List[Tuple[int, int]]
|
||||||
self.unban_queue: List[Tuple[int, int]]
|
self.unban_queue: List[Tuple[int, int]]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
async def _voice_perm_check(
|
||||||
|
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
||||||
|
) -> bool:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_audit_entry_info(
|
async def get_audit_entry_info(
|
||||||
|
|||||||
@ -97,4 +97,10 @@ CASETYPES = [
|
|||||||
"case_str": "Server Unmute",
|
"case_str": "Server Unmute",
|
||||||
"audit_type": "overwrite_update",
|
"audit_type": "overwrite_update",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "vkick",
|
||||||
|
"default_setting": False,
|
||||||
|
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
|
||||||
|
"case_str": "Voice Kick",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -495,6 +495,56 @@ class KickBanMixin(MixinMeta):
|
|||||||
await ctx.send(e)
|
await ctx.send(e)
|
||||||
await ctx.send(_("Done. Enough chaos."))
|
await ctx.send(_("Done. Enough chaos."))
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.mod_or_permissions(move_members=True)
|
||||||
|
async def voicekick(
|
||||||
|
self, ctx: commands.Context, member: discord.Member, *, reason: str = None
|
||||||
|
):
|
||||||
|
"""Kick a member from a voice channel."""
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
user_voice_state: discord.VoiceState = member.voice
|
||||||
|
|
||||||
|
if await self._voice_perm_check(ctx, user_voice_state, move_members=True) is False:
|
||||||
|
return
|
||||||
|
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, member):
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"I cannot let you do that. You are "
|
||||||
|
"not higher than the user in the role "
|
||||||
|
"hierarchy."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
case_channel = member.voice.channel
|
||||||
|
# Store this channel for the case channel.
|
||||||
|
|
||||||
|
try:
|
||||||
|
await member.move_to(discord.Object(id=None))
|
||||||
|
# Work around till we get D.py 1.1.0, whereby we can directly do None.
|
||||||
|
except discord.Forbidden: # Very unlikely that this will ever occur
|
||||||
|
await ctx.send(_("I am unable to kick this member from the voice channel."))
|
||||||
|
return
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send(_("Something went wrong while attempting to kick that member"))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await modlog.create_case(
|
||||||
|
self.bot,
|
||||||
|
guild,
|
||||||
|
ctx.message.created_at,
|
||||||
|
"vkick",
|
||||||
|
member,
|
||||||
|
author,
|
||||||
|
reason,
|
||||||
|
until=None,
|
||||||
|
channel=case_channel,
|
||||||
|
)
|
||||||
|
except RuntimeError as e:
|
||||||
|
await ctx.send(e)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
@commands.bot_has_permissions(ban_members=True)
|
||||||
@ -508,8 +558,9 @@ class KickBanMixin(MixinMeta):
|
|||||||
click the user and select 'Copy ID'."""
|
click the user and select 'Copy ID'."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
user = await self.bot.fetch_user(user_id)
|
try:
|
||||||
if not user:
|
user = await self.bot.fetch_user(user_id)
|
||||||
|
except discord.errors.NotFound:
|
||||||
await ctx.send(_("Couldn't find a user with that ID!"))
|
await ctx.send(_("Couldn't find a user with that ID!"))
|
||||||
return
|
return
|
||||||
audit_reason = get_audit_reason(ctx.author, reason)
|
audit_reason = get_audit_reason(ctx.author, reason)
|
||||||
|
|||||||
@ -44,6 +44,7 @@ class Streams(commands.Cog):
|
|||||||
"mention_here": False,
|
"mention_here": False,
|
||||||
"live_message_mention": False,
|
"live_message_mention": False,
|
||||||
"live_message_nomention": False,
|
"live_message_nomention": False,
|
||||||
|
"ignore_reruns": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
role_defaults = {"mention": False}
|
role_defaults = {"mention": False}
|
||||||
@ -461,6 +462,19 @@ class Streams(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Notifications will no longer be deleted."))
|
await ctx.send(_("Notifications will no longer be deleted."))
|
||||||
|
|
||||||
|
@streamset.command(name="ignorereruns")
|
||||||
|
@commands.guild_only()
|
||||||
|
async def ignore_reruns(self, ctx: commands.Context):
|
||||||
|
"""Toggle excluding rerun streams from alerts."""
|
||||||
|
guild = ctx.guild
|
||||||
|
current_setting = await self.db.guild(guild).ignore_reruns()
|
||||||
|
if current_setting:
|
||||||
|
await self.db.guild(guild).ignore_reruns.set(False)
|
||||||
|
await ctx.send(_("Streams of type 'rerun' will be included in alerts."))
|
||||||
|
else:
|
||||||
|
await self.db.guild(guild).ignore_reruns.set(True)
|
||||||
|
await ctx.send(_("Streams of type 'rerun' will no longer send an alert."))
|
||||||
|
|
||||||
async def add_or_remove(self, ctx: commands.Context, stream):
|
async def add_or_remove(self, ctx: commands.Context, stream):
|
||||||
if ctx.channel.id not in stream.channels:
|
if ctx.channel.id not in stream.channels:
|
||||||
stream.channels.append(ctx.channel.id)
|
stream.channels.append(ctx.channel.id)
|
||||||
@ -524,7 +538,7 @@ class Streams(commands.Cog):
|
|||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
try:
|
try:
|
||||||
embed = await stream.is_online()
|
embed, is_rerun = await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
if not stream._messages_cache:
|
if not stream._messages_cache:
|
||||||
continue
|
continue
|
||||||
@ -540,6 +554,9 @@ class Streams(commands.Cog):
|
|||||||
continue
|
continue
|
||||||
for channel_id in stream.channels:
|
for channel_id in stream.channels:
|
||||||
channel = self.bot.get_channel(channel_id)
|
channel = self.bot.get_channel(channel_id)
|
||||||
|
ignore_reruns = await self.db.guild(channel.guild).ignore_reruns()
|
||||||
|
if ignore_reruns and is_rerun:
|
||||||
|
continue
|
||||||
mention_str, edited_roles = await self._get_mention_str(channel.guild)
|
mention_str, edited_roles = await self._get_mention_str(channel.guild)
|
||||||
|
|
||||||
if mention_str:
|
if mention_str:
|
||||||
|
|||||||
@ -174,7 +174,8 @@ class TwitchStream(Stream):
|
|||||||
# self.already_online = True
|
# self.already_online = True
|
||||||
# In case of rename
|
# In case of rename
|
||||||
self.name = data["stream"]["channel"]["name"]
|
self.name = data["stream"]["channel"]["name"]
|
||||||
return self.make_embed(data)
|
is_rerun = True if data["stream"]["stream_type"] == "rerun" else False
|
||||||
|
return self.make_embed(data), is_rerun
|
||||||
elif r.status == 400:
|
elif r.status == 400:
|
||||||
raise InvalidTwitchCredentials()
|
raise InvalidTwitchCredentials()
|
||||||
elif r.status == 404:
|
elif r.status == 404:
|
||||||
@ -204,6 +205,7 @@ class TwitchStream(Stream):
|
|||||||
|
|
||||||
def make_embed(self, data):
|
def make_embed(self, data):
|
||||||
channel = data["stream"]["channel"]
|
channel = data["stream"]["channel"]
|
||||||
|
is_rerun = data["stream"]["stream_type"] == "rerun"
|
||||||
url = channel["url"]
|
url = channel["url"]
|
||||||
logo = channel["logo"]
|
logo = channel["logo"]
|
||||||
if logo is None:
|
if logo is None:
|
||||||
@ -211,6 +213,8 @@ class TwitchStream(Stream):
|
|||||||
status = channel["status"]
|
status = channel["status"]
|
||||||
if not status:
|
if not status:
|
||||||
status = "Untitled broadcast"
|
status = "Untitled broadcast"
|
||||||
|
if is_rerun:
|
||||||
|
status += " - Rerun"
|
||||||
embed = discord.Embed(title=status, url=url)
|
embed = discord.Embed(title=status, url=url)
|
||||||
embed.set_author(name=channel["display_name"])
|
embed.set_author(name=channel["display_name"])
|
||||||
embed.add_field(name="Followers", value=channel["followers"])
|
embed.add_field(name="Followers", value=channel["followers"])
|
||||||
|
|||||||
@ -45,7 +45,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
owner=None,
|
owner=None,
|
||||||
whitelist=[],
|
whitelist=[],
|
||||||
blacklist=[],
|
blacklist=[],
|
||||||
locale="en",
|
locale="en-US",
|
||||||
embeds=True,
|
embeds=True,
|
||||||
color=15158332,
|
color=15158332,
|
||||||
fuzzy=False,
|
fuzzy=False,
|
||||||
@ -190,6 +190,21 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
async def get_context(self, message, *, cls=commands.Context):
|
async def get_context(self, message, *, cls=commands.Context):
|
||||||
return await super().get_context(message, cls=cls)
|
return await super().get_context(message, cls=cls)
|
||||||
|
|
||||||
|
async def process_commands(self, message: discord.Message):
|
||||||
|
"""
|
||||||
|
modification from the base to do the same thing in the command case
|
||||||
|
|
||||||
|
but dispatch an additional event for cogs which want to handle normal messages
|
||||||
|
differently to command messages,
|
||||||
|
without the overhead of additional get_context calls per cog
|
||||||
|
"""
|
||||||
|
if not message.author.bot:
|
||||||
|
ctx = await self.get_context(message)
|
||||||
|
if ctx.valid:
|
||||||
|
return await self.invoke(ctx)
|
||||||
|
|
||||||
|
self.dispatch("message_without_command", message)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_packages():
|
def list_packages():
|
||||||
"""Lists packages present in the cogs the folder"""
|
"""Lists packages present in the cogs the folder"""
|
||||||
|
|||||||
@ -156,6 +156,13 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
self._help_override = kwargs.pop("help_override", None)
|
self._help_override = kwargs.pop("help_override", None)
|
||||||
self.translator = kwargs.pop("i18n", None)
|
self.translator = kwargs.pop("i18n", None)
|
||||||
|
|
||||||
|
def _ensure_assignment_on_copy(self, other):
|
||||||
|
super()._ensure_assignment_on_copy(other)
|
||||||
|
|
||||||
|
# Red specific
|
||||||
|
other.requires = self.requires
|
||||||
|
return other
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def help(self):
|
def help(self):
|
||||||
"""Help string for this command.
|
"""Help string for this command.
|
||||||
|
|||||||
@ -47,9 +47,9 @@ class APIToken(discord.ext.commands.Converter):
|
|||||||
This will parse the input argument separating the key value pairs into a
|
This will parse the input argument separating the key value pairs into a
|
||||||
format to be used for the core bots API token storage.
|
format to be used for the core bots API token storage.
|
||||||
|
|
||||||
This will split the argument by eiher `;` or `,` and return a dict
|
This will split the argument by either `;` or `,` and return a dict
|
||||||
to be stored. Since all API's are different and have different naming convention,
|
to be stored. Since all API's are different and have different naming convention,
|
||||||
this leaves the owness on the cog creator to clearly define how to setup the correct
|
this leaves the onus on the cog creator to clearly define how to setup the correct
|
||||||
credential names for their cogs.
|
credential names for their cogs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import itertools
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import traceback
|
import traceback
|
||||||
@ -38,13 +39,6 @@ __all__ = ["Core"]
|
|||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
|
|
||||||
OWNER_DISCLAIMER = (
|
|
||||||
"⚠ **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.** ⚠"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_ = i18n.Translator("Core", __file__)
|
_ = i18n.Translator("Core", __file__)
|
||||||
|
|
||||||
@ -301,42 +295,41 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
async with session.get("{}/json".format(red_pypi)) as r:
|
async with session.get("{}/json".format(red_pypi)) as r:
|
||||||
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 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]({}) "
|
||||||
"and help us improve!\n\n"
|
"and help us improve!\n\n"
|
||||||
"".format(red_repo, author_repo, org_repo, support_server_url)
|
).format(red_repo, author_repo, org_repo, support_server_url)
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(color=(await ctx.embed_colour()))
|
embed = discord.Embed(color=(await ctx.embed_colour()))
|
||||||
embed.add_field(name="Instance owned by", value=str(owner))
|
embed.add_field(name=_("Instance owned by"), value=str(owner))
|
||||||
embed.add_field(name="Python", value=python_version)
|
embed.add_field(name="Python", value=python_version)
|
||||||
embed.add_field(name="discord.py", value=dpy_version)
|
embed.add_field(name="discord.py", value=dpy_version)
|
||||||
embed.add_field(name="Red version", value=red_version)
|
embed.add_field(name=_("Red version"), value=red_version)
|
||||||
if outdated:
|
if outdated:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Outdated", value="Yes, {} is available".format(data["info"]["version"])
|
name=_("Outdated"), value=_("Yes, {} is available").format(data["info"]["version"])
|
||||||
)
|
)
|
||||||
if custom_info:
|
if custom_info:
|
||||||
embed.add_field(name="About this instance", value=custom_info, inline=False)
|
embed.add_field(name=_("About this instance"), value=custom_info, inline=False)
|
||||||
embed.add_field(name="About Red", value=about, inline=False)
|
embed.add_field(name=_("About Red"), value=about, inline=False)
|
||||||
|
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text="Bringing joy since 02 Jan 2016 (over {} days ago!)".format(days_since)
|
text=_("Bringing joy since 02 Jan 2016 (over {} days ago!)").format(days_since)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send("I need the `Embed links` permission to send this")
|
await ctx.send(_("I need the `Embed links` permission to send this"))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def uptime(self, ctx: commands.Context):
|
async def uptime(self, ctx: commands.Context):
|
||||||
"""Shows Red's uptime"""
|
"""Shows Red's uptime"""
|
||||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
passed = self.get_bot_uptime()
|
passed = self.get_bot_uptime()
|
||||||
await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since))
|
await ctx.send(_("Been up for: **{}** (since {} UTC)").format(passed, since))
|
||||||
|
|
||||||
def get_bot_uptime(self, *, brief: bool = False):
|
def get_bot_uptime(self, *, brief: bool = False):
|
||||||
# Courtesy of Danny
|
# Courtesy of Danny
|
||||||
@ -348,13 +341,13 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
if not brief:
|
if not brief:
|
||||||
if days:
|
if days:
|
||||||
fmt = "{d} days, {h} hours, {m} minutes, and {s} seconds"
|
fmt = _("{d} days, {h} hours, {m} minutes, and {s} seconds")
|
||||||
else:
|
else:
|
||||||
fmt = "{h} hours, {m} minutes, and {s} seconds"
|
fmt = _("{h} hours, {m} minutes, and {s} seconds")
|
||||||
else:
|
else:
|
||||||
fmt = "{h}h {m}m {s}s"
|
fmt = _("{h}h {m}m {s}s")
|
||||||
if days:
|
if days:
|
||||||
fmt = "{d}d " + fmt
|
fmt = _("{d}d ") + fmt
|
||||||
|
|
||||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
||||||
|
|
||||||
@ -369,14 +362,14 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
use embeds.
|
use embeds.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
text = "Embed settings:\n\n"
|
text = _("Embed settings:\n\n")
|
||||||
global_default = await self.bot.db.embeds()
|
global_default = await self.bot.db.embeds()
|
||||||
text += "Global default: {}\n".format(global_default)
|
text += _("Global default: {}\n").format(global_default)
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
guild_setting = await self.bot.db.guild(ctx.guild).embeds()
|
guild_setting = await self.bot.db.guild(ctx.guild).embeds()
|
||||||
text += "Guild setting: {}\n".format(guild_setting)
|
text += _("Guild setting: {}\n").format(guild_setting)
|
||||||
user_setting = await self.bot.db.user(ctx.author).embeds()
|
user_setting = await self.bot.db.user(ctx.author).embeds()
|
||||||
text += "User setting: {}".format(user_setting)
|
text += _("User setting: {}").format(user_setting)
|
||||||
await ctx.send(box(text))
|
await ctx.send(box(text))
|
||||||
|
|
||||||
@embedset.command(name="global")
|
@embedset.command(name="global")
|
||||||
@ -392,7 +385,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
current = await self.bot.db.embeds()
|
current = await self.bot.db.embeds()
|
||||||
await self.bot.db.embeds.set(not current)
|
await self.bot.db.embeds.set(not current)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} by default.").format("disabled" if current else "enabled")
|
_("Embeds are now {} by default.").format(_("disabled") if current else _("enabled"))
|
||||||
)
|
)
|
||||||
|
|
||||||
@embedset.command(name="guild")
|
@embedset.command(name="guild")
|
||||||
@ -415,7 +408,9 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
await ctx.send(_("Embeds will now fall back to the global setting."))
|
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} for this guild.").format("enabled" if enabled else "disabled")
|
_("Embeds are now {} for this guild.").format(
|
||||||
|
_("enabled") if enabled else _("disabled")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@embedset.command(name="user")
|
@embedset.command(name="user")
|
||||||
@ -436,7 +431,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
await ctx.send(_("Embeds will now fall back to the global setting."))
|
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} for you.").format("enabled" if enabled else "disabled")
|
_("Embeds are now {} for you.").format(_("enabled") if enabled else _("disabled"))
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@ -454,7 +449,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
for page in pagify(self.bot._last_exception, shorten_by=10):
|
for page in pagify(self.bot._last_exception, shorten_by=10):
|
||||||
await destination.send(box(page, lang="py"))
|
await destination.send(box(page, lang="py"))
|
||||||
else:
|
else:
|
||||||
await ctx.send("No exception has occurred yet")
|
await ctx.send(_("No exception has occurred yet"))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -467,21 +462,21 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def leave(self, ctx: commands.Context):
|
async def leave(self, ctx: commands.Context):
|
||||||
"""Leaves server"""
|
"""Leaves server"""
|
||||||
await ctx.send("Are you sure you want me to leave this server? (y/n)")
|
await ctx.send(_("Are you sure you want me to leave this server? (y/n)"))
|
||||||
|
|
||||||
pred = MessagePredicate.yes_or_no(ctx)
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
try:
|
try:
|
||||||
await self.bot.wait_for("message", check=pred)
|
await self.bot.wait_for("message", check=pred)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("Response timed out.")
|
await ctx.send(_("Response timed out."))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if pred.result is True:
|
if pred.result is True:
|
||||||
await ctx.send("Alright. Bye :wave:")
|
await ctx.send(_("Alright. Bye :wave:"))
|
||||||
log.debug("Leaving guild '{}'".format(ctx.guild.name))
|
log.debug(_("Leaving guild '{}'").format(ctx.guild.name))
|
||||||
await ctx.guild.leave()
|
await ctx.guild.leave()
|
||||||
else:
|
else:
|
||||||
await ctx.send("Alright, I'll stay then :)")
|
await ctx.send(_("Alright, I'll stay then :)"))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -497,7 +492,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
for page in pagify(msg, ["\n"]):
|
for page in pagify(msg, ["\n"]):
|
||||||
await ctx.send(page)
|
await ctx.send(page)
|
||||||
|
|
||||||
query = await ctx.send("To leave a server, just type its number.")
|
query = await ctx.send(_("To leave a server, just type its number."))
|
||||||
|
|
||||||
pred = MessagePredicate.contained_in(responses, ctx)
|
pred = MessagePredicate.contained_in(responses, ctx)
|
||||||
try:
|
try:
|
||||||
@ -512,21 +507,21 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
async def leave_confirmation(self, guild, ctx):
|
async def leave_confirmation(self, guild, ctx):
|
||||||
if guild.owner.id == ctx.bot.user.id:
|
if guild.owner.id == ctx.bot.user.id:
|
||||||
await ctx.send("I cannot leave a guild I am the owner of.")
|
await ctx.send(_("I cannot leave a guild I am the owner of."))
|
||||||
return
|
return
|
||||||
|
|
||||||
await ctx.send("Are you sure you want me to leave {}? (yes/no)".format(guild.name))
|
await ctx.send(_("Are you sure you want me to leave {}? (yes/no)").format(guild.name))
|
||||||
pred = MessagePredicate.yes_or_no(ctx)
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
try:
|
try:
|
||||||
await self.bot.wait_for("message", check=pred, timeout=15)
|
await self.bot.wait_for("message", check=pred, timeout=15)
|
||||||
if pred.result is True:
|
if pred.result is True:
|
||||||
await guild.leave()
|
await guild.leave()
|
||||||
if guild != ctx.guild:
|
if guild != ctx.guild:
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
else:
|
else:
|
||||||
await ctx.send("Alright then.")
|
await ctx.send(_("Alright then."))
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("Response timed out.")
|
await ctx.send(_("Response timed out."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -538,17 +533,17 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs)
|
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs)
|
||||||
|
|
||||||
if loaded:
|
if loaded:
|
||||||
fmt = "Loaded {packs}."
|
fmt = _("Loaded {packs}.")
|
||||||
formed = self._get_package_strings(loaded, fmt)
|
formed = self._get_package_strings(loaded, fmt)
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if already_loaded:
|
if already_loaded:
|
||||||
fmt = "The package{plural} {packs} {other} already loaded."
|
fmt = _("The package{plural} {packs} {other} already loaded.")
|
||||||
formed = self._get_package_strings(already_loaded, fmt, ("is", "are"))
|
formed = self._get_package_strings(already_loaded, fmt, (_("is"), _("are")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
fmt = (
|
fmt = _(
|
||||||
"Failed to load package{plural} {packs}. Check your console or "
|
"Failed to load package{plural} {packs}. Check your console or "
|
||||||
"logs for details."
|
"logs for details."
|
||||||
)
|
)
|
||||||
@ -556,17 +551,17 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if not_found:
|
if not_found:
|
||||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
fmt = _("The package{plural} {packs} {other} not found in any cog path.")
|
||||||
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
formed = self._get_package_strings(not_found, fmt, (_("was"), _("were")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed_with_reason:
|
if failed_with_reason:
|
||||||
fmt = (
|
fmt = _(
|
||||||
"{other} package{plural} could not be loaded for the following reason{plural}:\n\n"
|
"{other} package{plural} could not be loaded for the following reason{plural}:\n\n"
|
||||||
)
|
)
|
||||||
reasons = "\n".join([f"`{x}`: {y}" for x, y in failed_with_reason])
|
reasons = "\n".join([f"`{x}`: {y}" for x, y in failed_with_reason])
|
||||||
formed = self._get_package_strings(
|
formed = self._get_package_strings(
|
||||||
[x for x, y in failed_with_reason], fmt, ("This", "These")
|
[x for x, y in failed_with_reason], fmt, (_("This"), _("These"))
|
||||||
)
|
)
|
||||||
await ctx.send(formed + reasons)
|
await ctx.send(formed + reasons)
|
||||||
|
|
||||||
@ -579,13 +574,13 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
unloaded, failed = await self._unload(cogs)
|
unloaded, failed = await self._unload(cogs)
|
||||||
|
|
||||||
if unloaded:
|
if unloaded:
|
||||||
fmt = "Package{plural} {packs} {other} unloaded."
|
fmt = _("Package{plural} {packs} {other} unloaded.")
|
||||||
formed = self._get_package_strings(unloaded, fmt, ("was", "were"))
|
formed = self._get_package_strings(unloaded, fmt, (_("was"), _("were")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
fmt = "The package{plural} {packs} {other} not loaded."
|
fmt = _("The package{plural} {packs} {other} not loaded.")
|
||||||
formed = self._get_package_strings(failed, fmt, ("is", "are"))
|
formed = self._get_package_strings(failed, fmt, (_("is"), _("are")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
@commands.command(name="reload")
|
@commands.command(name="reload")
|
||||||
@ -600,25 +595,27 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if loaded:
|
if loaded:
|
||||||
fmt = "Package{plural} {packs} {other} reloaded."
|
fmt = _("Package{plural} {packs} {other} reloaded.")
|
||||||
formed = self._get_package_strings(loaded, fmt, ("was", "were"))
|
formed = self._get_package_strings(loaded, fmt, (_("was"), _("were")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
fmt = "Failed to reload package{plural} {packs}. Check your logs for details"
|
fmt = _("Failed to reload package{plural} {packs}. Check your logs for details")
|
||||||
formed = self._get_package_strings(failed, fmt)
|
formed = self._get_package_strings(failed, fmt)
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if not_found:
|
if not_found:
|
||||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
fmt = _("The package{plural} {packs} {other} not found in any cog path.")
|
||||||
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
formed = self._get_package_strings(not_found, fmt, (_("was"), _("were")))
|
||||||
await ctx.send(formed)
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed_with_reason:
|
if failed_with_reason:
|
||||||
fmt = "{other} package{plural} could not be reloaded for the following reason{plural}:\n\n"
|
fmt = _(
|
||||||
|
"{other} package{plural} could not be reloaded for the following reason{plural}:\n\n"
|
||||||
|
)
|
||||||
reasons = "\n".join([f"`{x}`: {y}" for x, y in failed_with_reason])
|
reasons = "\n".join([f"`{x}`: {y}" for x, y in failed_with_reason])
|
||||||
formed = self._get_package_strings(
|
formed = self._get_package_strings(
|
||||||
[x for x, y in failed_with_reason], fmt, ("This", "These")
|
[x for x, y in failed_with_reason], fmt, (_("This"), _("These"))
|
||||||
)
|
)
|
||||||
await ctx.send(formed + reasons)
|
await ctx.send(formed + reasons)
|
||||||
|
|
||||||
@ -659,7 +656,9 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
guild.get_role(await ctx.bot.db.guild(ctx.guild).mod_role()) or "Not set"
|
guild.get_role(await ctx.bot.db.guild(ctx.guild).mod_role()) or "Not set"
|
||||||
)
|
)
|
||||||
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
||||||
guild_settings = f"Admin role: {admin_role}\nMod role: {mod_role}\n"
|
guild_settings = _("Admin role: {admin}\nMod role: {mod}\n").format(
|
||||||
|
admin=admin_role, mod=mod_role
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
guild_settings = ""
|
guild_settings = ""
|
||||||
prefixes = None # This is correct. The below can happen in a guild.
|
prefixes = None # This is correct. The below can happen in a guild.
|
||||||
@ -668,11 +667,16 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
locale = await ctx.bot.db.locale()
|
locale = await ctx.bot.db.locale()
|
||||||
|
|
||||||
prefix_string = " ".join(prefixes)
|
prefix_string = " ".join(prefixes)
|
||||||
settings = (
|
settings = _(
|
||||||
f"{ctx.bot.user.name} Settings:\n\n"
|
"{bot_name} Settings:\n\n"
|
||||||
f"Prefixes: {prefix_string}\n"
|
"Prefixes: {prefixes}\n"
|
||||||
f"{guild_settings}"
|
"{guild_settings}"
|
||||||
f"Locale: {locale}"
|
"Locale: {locale}"
|
||||||
|
).format(
|
||||||
|
bot_name=ctx.bot.user.name,
|
||||||
|
prefixes=prefix_string,
|
||||||
|
guild_settings=guild_settings,
|
||||||
|
locale=locale,
|
||||||
)
|
)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
|
|
||||||
@ -905,7 +909,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I lack the permissions to change my own nickname."))
|
await ctx.send(_("I lack the permissions to change my own nickname."))
|
||||||
else:
|
else:
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@_set.command(aliases=["prefixes"])
|
@_set.command(aliases=["prefixes"])
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -942,11 +946,17 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
token += random.choice(chars)
|
token += random.choice(chars)
|
||||||
log.info("{0} ({0.id}) requested to be set as owner.".format(ctx.author))
|
log.info(_("{0} ({0.id}) requested to be set as owner.").format(ctx.author))
|
||||||
print(_("\nVerification token:"))
|
print(_("\nVerification token:"))
|
||||||
print(token)
|
print(token)
|
||||||
|
|
||||||
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
|
owner_disclaimer = _(
|
||||||
|
"⚠ **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.** ⚠"
|
||||||
|
)
|
||||||
|
await ctx.send(_("Remember:\n") + owner_disclaimer)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@ -997,7 +1007,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await ctx.bot.db.token.set(token)
|
await ctx.bot.db.token.set(token)
|
||||||
await ctx.send("Token set. Restart me.")
|
await ctx.send(_("Token set. Restart me."))
|
||||||
|
|
||||||
@_set.command()
|
@_set.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -1147,7 +1157,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
locale_list.append("en-US")
|
locale_list.append("en-US")
|
||||||
locale_list = sorted(set(locale_list))
|
locale_list = sorted(set(locale_list))
|
||||||
if not locale_list:
|
if not locale_list:
|
||||||
await ctx.send("No languages found.")
|
await ctx.send(_("No languages found."))
|
||||||
return
|
return
|
||||||
pages = pagify("\n".join(locale_list), shorten_by=26)
|
pages = pagify("\n".join(locale_list), shorten_by=26)
|
||||||
|
|
||||||
@ -1157,6 +1167,12 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def backup(self, ctx: commands.Context, *, backup_path: str = None):
|
async def backup(self, ctx: commands.Context, *, backup_path: str = None):
|
||||||
"""Creates a backup of all data for the instance."""
|
"""Creates a backup of all data for the instance."""
|
||||||
|
if backup_path:
|
||||||
|
path = pathlib.Path(backup_path)
|
||||||
|
if not (path.exists() and path.is_dir()):
|
||||||
|
return await ctx.send(
|
||||||
|
_("That path doesn't seem to exist. Please provide a valid path.")
|
||||||
|
)
|
||||||
from redbot.core.data_manager import basic_config, instance_name
|
from redbot.core.data_manager import basic_config, instance_name
|
||||||
from redbot.core.drivers.red_json import JSON
|
from redbot.core.drivers.red_json import JSON
|
||||||
|
|
||||||
@ -1220,7 +1236,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
_("A backup has been made of this instance. It is at {}.").format(backup_file)
|
_("A backup has been made of this instance. It is at {}.").format(backup_file)
|
||||||
)
|
)
|
||||||
if backup_file.stat().st_size > 8_000_000:
|
if backup_file.stat().st_size > 8_000_000:
|
||||||
await ctx.send(_("This backup is to large to send via DM."))
|
await ctx.send(_("This backup is too large to send via DM."))
|
||||||
return
|
return
|
||||||
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
||||||
|
|
||||||
@ -1364,6 +1380,16 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
@checks.is_owner()
|
||||||
|
async def datapath(self, ctx: commands.Context):
|
||||||
|
"""Prints the bot's data path."""
|
||||||
|
from redbot.core.data_manager import basic_config
|
||||||
|
|
||||||
|
data_dir = Path(basic_config["DATA_PATH"])
|
||||||
|
msg = _("Data path: {path}").format(path=data_dir)
|
||||||
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def whitelist(self, ctx: commands.Context):
|
async def whitelist(self, ctx: commands.Context):
|
||||||
@ -1452,7 +1478,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
curr_list = await ctx.bot.db.blacklist()
|
curr_list = await ctx.bot.db.blacklist()
|
||||||
|
|
||||||
msg = _("blacklisted Users:")
|
msg = _("Blacklisted Users:")
|
||||||
for user in curr_list:
|
for user in curr_list:
|
||||||
msg += "\n\t- {}".format(user)
|
msg += "\n\t- {}".format(user)
|
||||||
|
|
||||||
@ -1482,7 +1508,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
Clears the blacklist.
|
Clears the blacklist.
|
||||||
"""
|
"""
|
||||||
await ctx.bot.db.blacklist.set([])
|
await ctx.bot.db.blacklist.set([])
|
||||||
await ctx.send(_("blacklist has been cleared."))
|
await ctx.send(_("Blacklist has been cleared."))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -1596,7 +1622,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
curr_list = await ctx.bot.db.guild(ctx.guild).blacklist()
|
curr_list = await ctx.bot.db.guild(ctx.guild).blacklist()
|
||||||
|
|
||||||
msg = _("blacklisted Users and Roles:")
|
msg = _("Blacklisted Users and Roles:")
|
||||||
for obj in curr_list:
|
for obj in curr_list:
|
||||||
msg += "\n\t- {}".format(obj)
|
msg += "\n\t- {}".format(obj)
|
||||||
|
|
||||||
@ -1635,7 +1661,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
Clears the blacklist.
|
Clears the blacklist.
|
||||||
"""
|
"""
|
||||||
await ctx.bot.db.guild(ctx.guild).blacklist.set([])
|
await ctx.bot.db.guild(ctx.guild).blacklist.set([])
|
||||||
await ctx.send(_("blacklist has been cleared."))
|
await ctx.send(_("Blacklist has been cleared."))
|
||||||
|
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@commands.group(name="command")
|
@commands.group(name="command")
|
||||||
|
|||||||
@ -119,13 +119,25 @@ def init_events(bot, cli_flags):
|
|||||||
"Outdated version! {} is available "
|
"Outdated version! {} is available "
|
||||||
"but you're using {}".format(data["info"]["version"], red_version)
|
"but you're using {}".format(data["info"]["version"], red_version)
|
||||||
)
|
)
|
||||||
owner = await bot.fetch_user(bot.owner_id)
|
|
||||||
await owner.send(
|
owners = []
|
||||||
"Your Red instance is out of date! {} is the current "
|
owner = bot.get_user(bot.owner_id)
|
||||||
"version, however you are using {}!".format(
|
if owner is not None:
|
||||||
data["info"]["version"], red_version
|
owners.append(owner)
|
||||||
)
|
|
||||||
)
|
for co_owner in bot._co_owners:
|
||||||
|
co_owner = await bot.get_user(co_owner)
|
||||||
|
if co_owner is not None:
|
||||||
|
owners.append(co_owner)
|
||||||
|
|
||||||
|
for owner in owners:
|
||||||
|
with contextlib.suppress(discord.HTTPException):
|
||||||
|
await owner.send(
|
||||||
|
"Your Red instance is out of date! {} is the current "
|
||||||
|
"version, however you are using {}!".format(
|
||||||
|
data["info"]["version"], red_version
|
||||||
|
)
|
||||||
|
)
|
||||||
INFO2 = []
|
INFO2 = []
|
||||||
|
|
||||||
mongo_enabled = storage_type() != "JSON"
|
mongo_enabled = storage_type() != "JSON"
|
||||||
|
|||||||
@ -83,7 +83,8 @@ def bot_repo(event_loop):
|
|||||||
# Installable
|
# Installable
|
||||||
INFO_JSON = {
|
INFO_JSON = {
|
||||||
"author": ("tekulvw",),
|
"author": ("tekulvw",),
|
||||||
"bot_version": (3, 0, 0),
|
"min_bot_version": "3.0.0",
|
||||||
|
"max_bot_version": "3.0.2",
|
||||||
"description": "A long description",
|
"description": "A long description",
|
||||||
"hidden": False,
|
"hidden": False,
|
||||||
"install_msg": "A post-installation message",
|
"install_msg": "A post-installation message",
|
||||||
@ -96,7 +97,8 @@ INFO_JSON = {
|
|||||||
|
|
||||||
LIBRARY_INFO_JSON = {
|
LIBRARY_INFO_JSON = {
|
||||||
"author": ("seputaes",),
|
"author": ("seputaes",),
|
||||||
"bot_version": (3, 0, 0),
|
"min_bot_version": "3.0.0",
|
||||||
|
"max_bot_version": "3.0.2",
|
||||||
"description": "A long library description",
|
"description": "A long library description",
|
||||||
"hidden": False, # libraries are always hidden, this tests it will be flipped
|
"hidden": False, # libraries are always hidden, this tests it will be flipped
|
||||||
"install_msg": "A library install message",
|
"install_msg": "A library install message",
|
||||||
|
|||||||
@ -41,7 +41,7 @@ install_requires =
|
|||||||
multidict==4.5.2
|
multidict==4.5.2
|
||||||
python-levenshtein-wheels==0.13.1
|
python-levenshtein-wheels==0.13.1
|
||||||
pyyaml==3.13
|
pyyaml==3.13
|
||||||
red-lavalink==0.2.3
|
red-lavalink>=0.3.0,<0.4
|
||||||
schema==0.6.8
|
schema==0.6.8
|
||||||
yarl==1.3.0
|
yarl==1.3.0
|
||||||
discord.py==1.0.1
|
discord.py==1.0.1
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import pytest
|
|||||||
|
|
||||||
from redbot.pytest.downloader import *
|
from redbot.pytest.downloader import *
|
||||||
from redbot.cogs.downloader.installable import Installable, InstallableType
|
from redbot.cogs.downloader.installable import Installable, InstallableType
|
||||||
|
from redbot.core import VersionInfo
|
||||||
|
|
||||||
|
|
||||||
def test_process_info_file(installable):
|
def test_process_info_file(installable):
|
||||||
for k, v in INFO_JSON.items():
|
for k, v in INFO_JSON.items():
|
||||||
if k == "type":
|
if k == "type":
|
||||||
assert installable.type == InstallableType.COG
|
assert installable.type == InstallableType.COG
|
||||||
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
|
assert getattr(installable, k) == VersionInfo.from_str(v)
|
||||||
else:
|
else:
|
||||||
assert getattr(installable, k) == v
|
assert getattr(installable, k) == v
|
||||||
|
|
||||||
@ -19,6 +22,8 @@ def test_process_lib_info_file(library_installable):
|
|||||||
for k, v in LIBRARY_INFO_JSON.items():
|
for k, v in LIBRARY_INFO_JSON.items():
|
||||||
if k == "type":
|
if k == "type":
|
||||||
assert library_installable.type == InstallableType.SHARED_LIBRARY
|
assert library_installable.type == InstallableType.SHARED_LIBRARY
|
||||||
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
|
assert getattr(library_installable, k) == VersionInfo.from_str(v)
|
||||||
elif k == "hidden":
|
elif k == "hidden":
|
||||||
# libraries are always hidden, even if False
|
# libraries are always hidden, even if False
|
||||||
assert library_installable.hidden is True
|
assert library_installable.hidden is True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user