mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-11 10:45:59 -04:00
Compare commits
29 Commits
3.5.23
...
V3/develop
| Author | SHA1 | Date | |
|---|---|---|---|
| edce32364f | |||
| 7305f44f68 | |||
| cbd4643bd3 | |||
| b02fa38423 | |||
| 99babf9ad3 | |||
| 169d0eed49 | |||
| 70faa8cd52 | |||
| 2ea4c766ad | |||
| 6ceb45b35c | |||
| 4032648dcc | |||
| f70c48ec30 | |||
| fcb8bc0265 | |||
| ee1db01a2f | |||
| e2acec0862 | |||
| b83b882921 | |||
| 99d7b0e3b7 | |||
| 9270373c56 | |||
| e8f0ea0510 | |||
| b42bab4de9 | |||
| e868872214 | |||
| bee0ddbffc | |||
| 2de3d03cc9 | |||
| 056f2de557 | |||
| 34cbd15ba9 | |||
| 9a458fdd83 | |||
| 0e78051c5d | |||
| 53766173d0 | |||
| 36a5f752a2 | |||
| b2007a718d |
+7
-5
@@ -50,10 +50,6 @@
|
|||||||
- redbot/cogs/downloader/*
|
- redbot/cogs/downloader/*
|
||||||
# Docs
|
# Docs
|
||||||
- docs/cog_guides/downloader.rst
|
- docs/cog_guides/downloader.rst
|
||||||
# Tests
|
|
||||||
- redbot/pytest/downloader.py
|
|
||||||
- redbot/pytest/downloader_testrepo.*
|
|
||||||
- tests/cogs/downloader/**/*
|
|
||||||
"Category: Cogs - Economy":
|
"Category: Cogs - Economy":
|
||||||
# Source
|
# Source
|
||||||
- redbot/cogs/economy/*
|
- redbot/cogs/economy/*
|
||||||
@@ -212,6 +208,13 @@
|
|||||||
- redbot/core/_cli.py
|
- redbot/core/_cli.py
|
||||||
- redbot/core/_debuginfo.py
|
- redbot/core/_debuginfo.py
|
||||||
- redbot/setup.py
|
- redbot/setup.py
|
||||||
|
"Category: Core - Downloader":
|
||||||
|
# Source
|
||||||
|
- redbot/core/_downloader/**/*
|
||||||
|
# Tests
|
||||||
|
- redbot/pytest/downloader.py
|
||||||
|
- redbot/pytest/downloader_testrepo.*
|
||||||
|
- tests/core/_downloader/**/*
|
||||||
"Category: Core - Help":
|
"Category: Core - Help":
|
||||||
- redbot/core/commands/help.py
|
- redbot/core/commands/help.py
|
||||||
"Category: Core - i18n":
|
"Category: Core - i18n":
|
||||||
@@ -263,7 +266,6 @@
|
|||||||
- docs/framework_events.rst
|
- docs/framework_events.rst
|
||||||
- docs/guide_cog_creation.rst
|
- docs/guide_cog_creation.rst
|
||||||
- docs/guide_cog_creators.rst
|
- docs/guide_cog_creators.rst
|
||||||
- docs/guide_migration.rst
|
|
||||||
- docs/guide_publish_cogs.rst
|
- docs/guide_publish_cogs.rst
|
||||||
- docs/guide_slash_and_interactions.rst
|
- docs/guide_slash_and_interactions.rst
|
||||||
"Category: Docs - Install Guides":
|
"Category: Docs - Install Guides":
|
||||||
|
|||||||
@@ -7,18 +7,24 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: 'auto'
|
default: 'auto'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
crowdin_download_translations:
|
crowdin_download_translations:
|
||||||
|
environment: Prepare Release
|
||||||
needs: pr_stable_bump
|
needs: pr_stable_bump
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/create-github-app-token@v2
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
|
||||||
|
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Checkout repository and install Python
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -43,7 +49,7 @@ jobs:
|
|||||||
id: cpr_crowdin
|
id: cpr_crowdin
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
commit-message: Automated Crowdin downstream
|
commit-message: Automated Crowdin downstream
|
||||||
title: "Automated Crowdin downstream"
|
title: "Automated Crowdin downstream"
|
||||||
body: |
|
body: |
|
||||||
@@ -51,31 +57,32 @@ jobs:
|
|||||||
Please ensure that there are no errors or invalid files are in the PR.
|
Please ensure that there are no errors or invalid files are in the PR.
|
||||||
labels: "Automated PR, Changelog Entry: Skipped"
|
labels: "Automated PR, Changelog Entry: Skipped"
|
||||||
branch: "automated/i18n"
|
branch: "automated/i18n"
|
||||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
committer: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
|
author: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
milestone: ${{ needs.pr_stable_bump.outputs.milestone_number }}
|
milestone: ${{ needs.pr_stable_bump.outputs.milestone_number }}
|
||||||
|
|
||||||
- name: Close and reopen the PR with different token to trigger CI
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ steps.cpr_crowdin.outputs.pull-request-number }}
|
|
||||||
PR_OPERATION: ${{ steps.cpr_crowdin.outputs.pull-request-operation }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
|
|
||||||
script: |
|
|
||||||
const script = require(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
|
|
||||||
);
|
|
||||||
console.log(script({github, context}));
|
|
||||||
|
|
||||||
pr_stable_bump:
|
pr_stable_bump:
|
||||||
|
environment: Prepare Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
milestone_number: ${{ steps.get_milestone_number.outputs.result }}
|
milestone_number: ${{ steps.get_milestone_number.outputs.result }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v2
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
|
||||||
|
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
|
||||||
|
|
||||||
# Checkout repository and install Python
|
# Checkout repository and install Python
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
|
|
||||||
@@ -105,7 +112,7 @@ jobs:
|
|||||||
id: cpr_bump_stable
|
id: cpr_bump_stable
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
commit-message: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
|
commit-message: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
|
||||||
title: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
|
title: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
|
||||||
body: |
|
body: |
|
||||||
@@ -113,18 +120,10 @@ jobs:
|
|||||||
Please ensure that there are no errors or invalid files are in the PR.
|
Please ensure that there are no errors or invalid files are in the PR.
|
||||||
labels: "Automated PR, Changelog Entry: Skipped"
|
labels: "Automated PR, Changelog Entry: Skipped"
|
||||||
branch: "automated/pr_bumps/${{ steps.bump_version_stable.outputs.new_version }}"
|
branch: "automated/pr_bumps/${{ steps.bump_version_stable.outputs.new_version }}"
|
||||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
committer: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
|
author: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
milestone: ${{ steps.get_milestone_number.outputs.result }}
|
milestone: ${{ steps.get_milestone_number.outputs.result }}
|
||||||
|
|
||||||
- name: Close and reopen the PR with different token to trigger CI
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ steps.cpr_bump_stable.outputs.pull-request-number }}
|
|
||||||
PR_OPERATION: ${{ steps.cpr_bump_stable.outputs.pull-request-operation }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
|
|
||||||
script: |
|
|
||||||
const script = require(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
|
|
||||||
);
|
|
||||||
console.log(await script({github, context}));
|
|
||||||
|
|||||||
@@ -147,9 +147,7 @@ jobs:
|
|||||||
print-hash: true
|
print-hash: true
|
||||||
|
|
||||||
pr_dev_bump:
|
pr_dev_bump:
|
||||||
permissions:
|
environment: Prepare Release
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
needs: release_to_pypi
|
needs: release_to_pypi
|
||||||
name: Update Red version number to dev
|
name: Update Red version number to dev
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -160,11 +158,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "BASE_BRANCH=${TAG_BASE_BRANCH#'refs/heads/'}" >> $GITHUB_ENV
|
echo "BASE_BRANCH=${TAG_BASE_BRANCH#'refs/heads/'}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/create-github-app-token@v2
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
|
||||||
|
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.BASE_BRANCH }}
|
ref: ${{ env.BASE_BRANCH }}
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
|
|
||||||
@@ -194,7 +199,7 @@ jobs:
|
|||||||
id: cpr_bump_dev
|
id: cpr_bump_dev
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
commit-message: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
|
commit-message: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
|
||||||
title: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
|
title: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
|
||||||
body: |
|
body: |
|
||||||
@@ -202,19 +207,11 @@ jobs:
|
|||||||
Please ensure that there are no errors or invalid files are in the PR.
|
Please ensure that there are no errors or invalid files are in the PR.
|
||||||
labels: "Automated PR, Changelog Entry: Skipped"
|
labels: "Automated PR, Changelog Entry: Skipped"
|
||||||
branch: "automated/pr_bumps/${{ steps.bump_version_dev.outputs.new_version }}"
|
branch: "automated/pr_bumps/${{ steps.bump_version_dev.outputs.new_version }}"
|
||||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
committer: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
|
author: >-
|
||||||
|
${{ steps.app-token.outputs.app-slug }}[bot]
|
||||||
|
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
|
||||||
milestone: ${{ steps.get_milestone_number.outputs.result }}
|
milestone: ${{ steps.get_milestone_number.outputs.result }}
|
||||||
base: ${{ env.BASE_BRANCH }}
|
base: ${{ env.BASE_BRANCH }}
|
||||||
|
|
||||||
- name: Close and reopen the PR with different token to trigger CI
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ steps.cpr_bump_dev.outputs.pull-request-number }}
|
|
||||||
PR_OPERATION: ${{ steps.cpr_bump_dev.outputs.pull-request-operation }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
|
|
||||||
script: |
|
|
||||||
const script = require(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
|
|
||||||
);
|
|
||||||
console.log(await script({github, context}));
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ build:
|
|||||||
jobs:
|
jobs:
|
||||||
install:
|
install:
|
||||||
- pip install .[doc]
|
- pip install .[doc]
|
||||||
|
post_build:
|
||||||
|
- mkdir -p docs/_build/doctrees docs/_build/markdown "$READTHEDOCS_OUTPUT/html/_markdown"
|
||||||
|
- python -m sphinx -T -b markdown -d docs/_build/doctrees -D "language=$READTHEDOCS_LANGUAGE" docs docs/_build/markdown
|
||||||
|
- cp docs/_build/markdown/changelog.md "$READTHEDOCS_OUTPUT/html/_markdown/changelog.md"
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
|||||||
+957
-130
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,12 @@ liking, making it completely customizable. This is a *self-hosted bot* – meani
|
|||||||
to host and maintain your own instance. You can turn Red into an admin bot, music bot, trivia bot,
|
to host and maintain your own instance. You can turn Red into an admin bot, music bot, trivia bot,
|
||||||
new best friend or all of these together!
|
new best friend or all of these together!
|
||||||
|
|
||||||
|
Red is built for [Discord](https://discord.com/), a popular VOIP and instant messaging platform.
|
||||||
|
It's best suited for use in guilds (also known as servers), where it utilizes Discord's
|
||||||
|
well-documented API to communicate and deliver its many features. Discord offers its API to
|
||||||
|
encourage developers to explore their creativity by building programs, tools, and services that
|
||||||
|
enhance the Discord experience.
|
||||||
|
|
||||||
[Installation](#installation) is easy, and you do **NOT** need to know anything about coding! Aside
|
[Installation](#installation) is easy, and you do **NOT** need to know anything about coding! Aside
|
||||||
from installing and updating, every part of the bot can be controlled from within Discord.
|
from installing and updating, every part of the bot can be controlled from within Discord.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
from sphinx.util.docutils import SphinxDirective
|
||||||
|
|
||||||
|
|
||||||
|
class ChangelogContributors(SphinxDirective):
|
||||||
|
has_content = True
|
||||||
|
|
||||||
|
def run(self) -> List[nodes.Node]:
|
||||||
|
contributors = [contributor for line in self.content for contributor in line.split()]
|
||||||
|
|
||||||
|
comment_value = " ".join(contributors)
|
||||||
|
line_nodes = []
|
||||||
|
for contributor in contributors:
|
||||||
|
if line_nodes:
|
||||||
|
line_nodes.append(nodes.Text(", "))
|
||||||
|
line_nodes.append(
|
||||||
|
nodes.reference(
|
||||||
|
contributor,
|
||||||
|
f"@{contributor}",
|
||||||
|
internal=False,
|
||||||
|
refuri=f"https://github.com/sponsors/{contributor}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
node = nodes.line_block(
|
||||||
|
"",
|
||||||
|
nodes.comment("", f"RED-CHANGELOG-CONTRIBUTORS: {comment_value}"),
|
||||||
|
nodes.line("", "Thanks to all these amazing people who contributed to this release:"),
|
||||||
|
nodes.line("", "", *line_nodes),
|
||||||
|
)
|
||||||
|
return [node]
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||||
|
app.add_directive("changelog-contributors", ChangelogContributors)
|
||||||
|
return {
|
||||||
|
"version": "1.0",
|
||||||
|
"parallel_read_safe": True,
|
||||||
|
"parallel_write_safe": True,
|
||||||
|
}
|
||||||
+17
-17
@@ -3,16 +3,14 @@
|
|||||||
==========================
|
==========================
|
||||||
About Virtual Environments
|
About Virtual Environments
|
||||||
==========================
|
==========================
|
||||||
Creating a virtual environment is really easy and usually prevents many common installation
|
Creating a virtual environment is simple and helps prevent installation problems.
|
||||||
problems.
|
|
||||||
|
|
||||||
**What Are Virtual Environments For?**
|
**What Are Virtual Environments For?**
|
||||||
|
|
||||||
Virtual environments allow you to isolate Red's library dependencies, cog dependencies and python
|
Virtual environments allow you to isolate Red's library dependencies, cog dependencies, and Python
|
||||||
binaries from the rest of your system. There is no performance overhead to using virtual environment
|
binaries from the rest of your system with no performance overhead, ensuring those dependencies
|
||||||
and it saves you from a lot of troubles during setup. It also makes sure Red and its dependencies
|
and Red are installed to a predictable location. This makes uninstalling Red as simple as removing
|
||||||
are installed to a predictable location which makes uninstalling Red as simple as removing a single folder,
|
a single folder, preventing any data loss or breaking other things on your system.
|
||||||
without worrying about losing your data or other things on your system becoming broken.
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
@@ -21,19 +19,21 @@ Virtual Environments with Multiple Instances
|
|||||||
If you are running multiple instances of Red on the same machine, you have the option of either
|
If you are running multiple instances of Red on the same machine, you have the option of either
|
||||||
using the same virtual environment for all of them, or creating separate ones.
|
using the same virtual environment for all of them, or creating separate ones.
|
||||||
|
|
||||||
.. note::
|
Using a *single* virtual environment for all of your instances means you:
|
||||||
|
|
||||||
This only applies for multiple instances of V3. If you are running a V2 instance as well,
|
- Only need to update Red once for all instances.
|
||||||
you **must** use separate virtual environments.
|
- Must shut down all instances prior to updating.
|
||||||
|
- Will save space on your hard drive.
|
||||||
|
- Want all instances to share the same version/dependencies.
|
||||||
|
|
||||||
The advantages of using a *single* virtual environment for all of your V3 instances are:
|
Using *multiple* virtual environments for each individual or select groups of instances means you:
|
||||||
|
|
||||||
- When updating Red, you will only need to update it once for all instances (however you will still need to restart all instances for the changes to take effect)
|
- Need to update Red within each virtual environment separately.
|
||||||
- It will save space on your hard drive
|
- Can update Red without needing to update all instances.
|
||||||
|
- Only need to shut down the instance(s) being updated.
|
||||||
On the other hand, you may wish to update each of your instances individually.
|
- Want different Red/dependency versions on different instances.
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
Windows users with multiple instances should create *separate* virtual environments, as
|
Regardless of which option you choose, do not update while any instances within that virtual
|
||||||
updating multiple running instances at once is likely to cause errors.
|
environment are running. This is especially true for Windows, as files are locked by the system while in use.
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
Backing Up and Restoring Red
|
Backing Up and Restoring Red
|
||||||
============================
|
============================
|
||||||
|
|
||||||
Red can be backed up and restored to any device as long as it is supported operating system. See page: :ref:`end-user-guarantees`.
|
Red can be backed up and restored to any device as long as it is a supported operating system. See page: :ref:`end-user-guarantees`.
|
||||||
|
|
||||||
Backup steps are to be done in order and carefully to avoid any issues.
|
Backup steps are to be done in order and carefully to avoid any issues.
|
||||||
|
|
||||||
|
|||||||
@@ -116,18 +116,18 @@ How can I use this playlist link with playlist commands in audio?**
|
|||||||
:ref:`setting up Audio for multiple bots<multibots>`. Otherwise, another process is using the
|
:ref:`setting up Audio for multiple bots<multibots>`. Otherwise, another process is using the
|
||||||
port, so you need to figure out what is using port 2333 and terminate/disconnect it yourself.
|
port, so you need to figure out what is using port 2333 and terminate/disconnect it yourself.
|
||||||
|
|
||||||
**Q: My terminal is saying that I "must install Java 17 or 11 for Lavalink to run". How can I fix this?**
|
**Q: My terminal is saying that I "must install Java 21 or 17 for Lavalink to run". How can I fix this?**
|
||||||
|
|
||||||
You are getting this error because you have a different version of Java installed, or you don't have
|
You are getting this error because you have a different version of Java installed, or you don't have
|
||||||
Java installed at all. As the error states, Java 17 or 11 is required, and can be installed from
|
Java installed at all. As the error states, Java 21 or 17 is required, and can be installed from
|
||||||
`here <https://adoptium.net/temurin/releases/?version=17>`__.
|
`here <https://adoptium.net/temurin/releases/?version=21>`__.
|
||||||
|
|
||||||
If you have Java 17 or 11 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
|
If you have Java 21 or 17 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
|
||||||
Use ``[p]llset java <path_to_java_17_or_11_executable>``, to make Audio launch Lavalink with a
|
Use ``[p]llset java <path_to_java_21_or_17_executable>``, to make Audio launch Lavalink with a
|
||||||
specific Java binary. To do this, you will need to locate your ``java.exe``/``java`` file
|
specific Java binary. To do this, you will need to locate your ``java.exe``/``java`` file
|
||||||
in your **Java 17 or 11 install**.
|
in your **Java 21 or 17 install**.
|
||||||
|
|
||||||
Alternatively, update your PATH settings so that Java 17 or 11 is the one used by ``java``. However,
|
Alternatively, update your PATH settings so that Java 21 or 17 is the one used by ``java``. However,
|
||||||
you should confirm that nothing other than Red is running on the machine that requires Java.
|
you should confirm that nothing other than Red is running on the machine that requires Java.
|
||||||
|
|
||||||
.. _queue_commands:
|
.. _queue_commands:
|
||||||
@@ -550,7 +550,7 @@ uses OpenJDK 17 in the managed Lavalink configuration. It can be installed by ru
|
|||||||
|
|
||||||
sudo apt install openjdk-17-jre-headless -y
|
sudo apt install openjdk-17-jre-headless -y
|
||||||
|
|
||||||
Otherwise, Lavalink works well with most versions of Java 11, 13, 15, 16, 17, and 18. Azul
|
Otherwise, Lavalink works well with most versions of Java 17 and higher. Azul
|
||||||
Zulu builds are suggested, see `here <https://github.com/lavalink-devs/Lavalink/#requirements>`__ for more information.
|
Zulu builds are suggested, see `here <https://github.com/lavalink-devs/Lavalink/#requirements>`__ for more information.
|
||||||
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -3651,7 +3651,7 @@ This command shouldn't need to be used most of the time,
|
|||||||
and is only useful if the host machine has conflicting Java versions.
|
and is only useful if the host machine has conflicting Java versions.
|
||||||
|
|
||||||
If changing this make sure that the Java executable you set is supported by Audio.
|
If changing this make sure that the Java executable you set is supported by Audio.
|
||||||
The current supported versions are Java 17 and 11.
|
The current supported versions are Java 21 or 17.
|
||||||
|
|
||||||
**Arguments**
|
**Arguments**
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ Explains how to set the Twitch token.
|
|||||||
|
|
||||||
To set the Twitch API tokens, follow these steps:
|
To set the Twitch API tokens, follow these steps:
|
||||||
|
|
||||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
1. Go to this page: https://dev.twitch.tv/console/apps.
|
||||||
|
|
||||||
2. Click Register Your Application.
|
2. Click Register Your Application.
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ extensions = [
|
|||||||
"sphinx.ext.napoleon",
|
"sphinx.ext.napoleon",
|
||||||
"sphinx.ext.doctest",
|
"sphinx.ext.doctest",
|
||||||
"sphinxcontrib_trio",
|
"sphinxcontrib_trio",
|
||||||
|
"sphinx_markdown_builder",
|
||||||
"sphinx-prompt",
|
"sphinx-prompt",
|
||||||
|
"changelog_contributors",
|
||||||
"deprecated_removed",
|
"deprecated_removed",
|
||||||
"prompt_builder",
|
"prompt_builder",
|
||||||
]
|
]
|
||||||
@@ -230,6 +232,14 @@ linkcheck_ignore = [r"https://java.com*", r"https://chocolatey.org*"]
|
|||||||
linkcheck_retries = 3
|
linkcheck_retries = 3
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for markdown builder ----------------------------------------
|
||||||
|
|
||||||
|
markdown_http_base = os.environ.get(
|
||||||
|
"READTHEDOCS_CANONICAL_URL", "https://docs.discord.red/en/stable"
|
||||||
|
)
|
||||||
|
markdown_uri_doc_suffix = ".html"
|
||||||
|
|
||||||
|
|
||||||
# -- Options for extensions -----------------------------------------------
|
# -- Options for extensions -----------------------------------------------
|
||||||
|
|
||||||
if dpy_version_info.releaselevel == "final":
|
if dpy_version_info.releaselevel == "final":
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
Bank
|
Bank
|
||||||
====
|
====
|
||||||
|
|
||||||
Bank has now been separated from Economy for V3. New to bank is support for
|
|
||||||
having a global bank.
|
|
||||||
|
|
||||||
***********
|
***********
|
||||||
Basic Usage
|
Basic Usage
|
||||||
***********
|
***********
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Bot
|
|||||||
Red
|
Red
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
.. autoclass:: Red
|
.. autoclass:: Red()
|
||||||
:members:
|
:members:
|
||||||
:exclude-members: get_context, get_embed_color
|
:exclude-members: get_context, get_embed_color
|
||||||
|
|
||||||
|
|||||||
@@ -446,49 +446,6 @@ Of course, if we're less than responsible pet owners, there are consequences::
|
|||||||
"how poorly it was taken care of."
|
"how poorly it was taken care of."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
*************
|
|
||||||
V2 Data Usage
|
|
||||||
*************
|
|
||||||
There has been much conversation on how to bring V2 data into V3 and, officially, we recommend that cog developers
|
|
||||||
make use of the public interface in Config (using the categories as described in these docs) rather than simply
|
|
||||||
copying and pasting your V2 data into V3. Using Config as recommended will result in a much better experience for
|
|
||||||
you in the long run and will simplify cog creation and maintenance.
|
|
||||||
|
|
||||||
However.
|
|
||||||
|
|
||||||
We realize that many of our cog creators have expressed disinterest in writing converters for V2 to V3 style data.
|
|
||||||
As a result we have opened up config to take standard V2 data and allow cog developers to manipulate it in V3 in
|
|
||||||
much the same way they would in V2. The following examples will demonstrate how to accomplish this.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
By following this method to use V2 data in V3 you may be at risk of data corruption if your cog is used on a bot
|
|
||||||
with multiple shards. USE AT YOUR OWN RISK.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from redbot.core import Config, commands
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleCog(commands.Cog):
|
|
||||||
def __init__(self):
|
|
||||||
self.config = Config.get_conf(self, 1234567890)
|
|
||||||
self.config.init_custom("V2", 1)
|
|
||||||
self.data = {}
|
|
||||||
|
|
||||||
async def load_data(self):
|
|
||||||
self.data = await self.config.custom("V2", "V2").all()
|
|
||||||
|
|
||||||
async def save_data(self):
|
|
||||||
await self.config.custom("V2", "V2").set(self.data)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
cog = ExampleCog()
|
|
||||||
await cog.load_data()
|
|
||||||
await bot.add_cog(cog)
|
|
||||||
|
|
||||||
************************************
|
************************************
|
||||||
Best practices and performance notes
|
Best practices and performance notes
|
||||||
************************************
|
************************************
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
Mod log
|
Mod log
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Mod log has now been separated from Mod for V3.
|
|
||||||
|
|
||||||
***********
|
***********
|
||||||
Basic Usage
|
Basic Usage
|
||||||
***********
|
***********
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ RPC
|
|||||||
RPC support is included in Red on a `provisional <developer-guarantees-exclusions>` basis.
|
RPC support is included in Red on a `provisional <developer-guarantees-exclusions>` basis.
|
||||||
Backwards incompatible changes (up to and including removal of the RPC) may occur if deemed necessary.
|
Backwards incompatible changes (up to and including removal of the RPC) may occur if deemed necessary.
|
||||||
|
|
||||||
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
Red comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
||||||
Cogs must register functions to be exposed to RPC clients.
|
Cogs must register functions to be exposed to RPC clients.
|
||||||
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
.. role:: python(code)
|
.. role:: python(code)
|
||||||
:language: python
|
:language: python
|
||||||
|
|
||||||
========================
|
=====================
|
||||||
Creating cogs for Red V3
|
Creating cogs for Red
|
||||||
========================
|
=====================
|
||||||
|
|
||||||
This guide serves as a tutorial on creating cogs for Red V3.
|
This guide serves as a tutorial on creating cogs for Red.
|
||||||
It will cover the basics of setting up a package for your
|
It will cover the basics of setting up a package for your
|
||||||
cog and the basics of setting up the file structure. We will
|
cog and the basics of setting up the file structure. We will
|
||||||
also point you towards some further resources that may assist
|
also point you towards some further resources that may assist
|
||||||
@@ -111,8 +111,8 @@ Make sure that both files are saved.
|
|||||||
Testing your cog
|
Testing your cog
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
To test your cog, you will need a running instance of V3.
|
To test your cog, you will need a running instance of Red.
|
||||||
Assuming you installed V3 as outlined above, run :code:`redbot-setup`
|
Assuming you installed Red as outlined above, run :code:`redbot-setup`
|
||||||
and provide the requested information. Once that's done, run Red
|
and provide the requested information. Once that's done, run Red
|
||||||
by doing :code:`redbot <instance name> --dev` to start Red.
|
by doing :code:`redbot <instance name> --dev` to start Red.
|
||||||
Complete the initial setup by providing a valid token and setting a
|
Complete the initial setup by providing a valid token and setting a
|
||||||
@@ -169,6 +169,4 @@ Becoming an Approved Cog Creator
|
|||||||
Additional resources
|
Additional resources
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Be sure to check out the :doc:`/guide_migration` for some resources
|
If you've developed cogs for V2, you might find `incompatible_changes/v2_migration` document helpful.
|
||||||
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
|
||||||
those who developed cogs for V2.
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. Publishing cogs for V3
|
.. Publishing cogs for V3
|
||||||
|
|
||||||
Publishing cogs for Red V3
|
Publishing cogs for Red
|
||||||
==========================
|
=======================
|
||||||
|
|
||||||
Users of Red install 3rd-party cogs using Downloader cog. To make your cog available
|
Users of Red install 3rd-party cogs using Downloader cog. To make your cog available
|
||||||
to install for others, you will have to create a git repository
|
to install for others, you will have to create a git repository
|
||||||
|
|||||||
+2
-2
@@ -86,7 +86,7 @@ Average Providers
|
|||||||
| `OVH <https://us.ovhcloud.com/vps/>`_ is a company focused on providing hosting
|
| `OVH <https://us.ovhcloud.com/vps/>`_ is a company focused on providing hosting
|
||||||
and cloud services with locations in Europe, North America and Asia Pacific.
|
and cloud services with locations in Europe, North America and Asia Pacific.
|
||||||
|
|
||||||
| `Time4VPS <https://www.time4vps.eu/>`_ is a Lithuanian VPS provider mainly focused
|
| `Time4VPS <https://www.time4vps.com/>`_ is a Lithuanian VPS provider mainly focused
|
||||||
on lower cost.
|
on lower cost.
|
||||||
|
|
||||||
| `GalaxyGate <https://galaxygate.net/>`_ is a VPS and dedicated server provider
|
| `GalaxyGate <https://galaxygate.net/>`_ is a VPS and dedicated server provider
|
||||||
@@ -113,7 +113,7 @@ Average Providers
|
|||||||
| `LowEndBox <http://lowendbox.com/>`_ is a website where hosting providers are
|
| `LowEndBox <http://lowendbox.com/>`_ is a website where hosting providers are
|
||||||
discussed and curated, often with lower costs and less known providers.
|
discussed and curated, often with lower costs and less known providers.
|
||||||
|
|
||||||
| `AlphaVps <https://alphavps.com>`_ is a Bulgaria VPS and dedicated server provider
|
| `AlphaVps <https://alphavps.com>`_ is a Bulgarian VPS and dedicated server provider
|
||||||
with locations in Los Angeles, New York, England, Germany and Bulgaria.
|
with locations in Los Angeles, New York, England, Germany and Bulgaria.
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ Backward incompatible changes
|
|||||||
|
|
||||||
future
|
future
|
||||||
3.5
|
3.5
|
||||||
|
v2_migration
|
||||||
|
|||||||
@@ -38,6 +38,49 @@ per-server/member/user/role/channel or global basis. Be sure to check
|
|||||||
out :doc:`/framework_config` for the API docs for Config as well as a
|
out :doc:`/framework_config` for the API docs for Config as well as a
|
||||||
tutorial on using Config.
|
tutorial on using Config.
|
||||||
|
|
||||||
|
*************
|
||||||
|
V2 Data Usage
|
||||||
|
*************
|
||||||
|
|
||||||
|
There has been much conversation on how to bring V2 data into V3 and, officially, we recommend that cog developers
|
||||||
|
make use of the public interface in Config (using the categories as described in these docs) rather than simply
|
||||||
|
copying and pasting your V2 data into V3. Using Config as recommended will result in a much better experience for
|
||||||
|
you in the long run and will simplify cog creation and maintenance.
|
||||||
|
|
||||||
|
However.
|
||||||
|
|
||||||
|
We realize that many of our cog creators have expressed disinterest in writing converters for V2 to V3 style data.
|
||||||
|
As a result we have opened up config to take standard V2 data and allow cog developers to manipulate it in V3 in
|
||||||
|
much the same way they would in V2. The following examples will demonstrate how to accomplish this.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
By following this method to use V2 data in V3 you may be at risk of data corruption if your cog is used on a bot
|
||||||
|
with multiple shards. USE AT YOUR OWN RISK.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from redbot.core import Config, commands
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleCog(commands.Cog):
|
||||||
|
def __init__(self):
|
||||||
|
self.config = Config.get_conf(self, 1234567890)
|
||||||
|
self.config.init_custom("V2", 1)
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
async def load_data(self):
|
||||||
|
self.data = await self.config.custom("V2", "V2").all()
|
||||||
|
|
||||||
|
async def save_data(self):
|
||||||
|
await self.config.custom("V2", "V2").set(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
cog = ExampleCog()
|
||||||
|
await cog.load_data()
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
|
||||||
----
|
----
|
||||||
Bank
|
Bank
|
||||||
----
|
----
|
||||||
@@ -62,7 +62,6 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Red Development Framework Reference:
|
:caption: Red Development Framework Reference:
|
||||||
|
|
||||||
guide_migration
|
|
||||||
guide_cog_creation
|
guide_cog_creation
|
||||||
guide_slash_and_interactions
|
guide_slash_and_interactions
|
||||||
guide_publish_cogs
|
guide_publish_cogs
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ Alma Linux 8 x86-64, aarch64 2029-05-31 (`securi
|
|||||||
Alma Linux 9 x86-64, aarch64 2032-05-31 (`security support <https://wiki.almalinux.org/release-notes/>`__)
|
Alma Linux 9 x86-64, aarch64 2032-05-31 (`security support <https://wiki.almalinux.org/release-notes/>`__)
|
||||||
Amazon Linux 2023 x86-64, aarch64 2028-03-15 (`end-of-life <https://docs.aws.amazon.com/linux/al2023/release-notes/support-info-by-support-statement.html#support-info-by-support-statement-eol>`__)
|
Amazon Linux 2023 x86-64, aarch64 2028-03-15 (`end-of-life <https://docs.aws.amazon.com/linux/al2023/release-notes/support-info-by-support-statement.html#support-info-by-support-statement-eol>`__)
|
||||||
Arch Linux x86-64 forever (support is only provided for an up-to-date system)
|
Arch Linux x86-64 forever (support is only provided for an up-to-date system)
|
||||||
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`expected EOL <https://centos.org/stream9/#timeline>`__)
|
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`Expected EOL <https://centos.org/stream9/#timeline>`__)
|
||||||
Debian 12 Bookworm x86-64, aarch64, armv7l 2026-06-10 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
|
Debian 12 Bookworm x86-64, aarch64, armv7l 2026-06-10 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
|
||||||
Fedora Linux 42 x86-64, aarch64 2026-05-13 (`End of Life <https://fedorapeople.org/groups/schedule/f-42/f-42-key-tasks.html>`__)
|
Fedora Linux 42 x86-64, aarch64 2026-05-13 (`End of Life <https://fedorapeople.org/groups/schedule/f-42/f-42-key-tasks.html>`__)
|
||||||
Fedora Linux 43 x86-64, aarch64 2026-12-09 (`End of Life <https://fedorapeople.org/groups/schedule/f-43/f-43-key-tasks.html>`__)
|
Fedora Linux 43 x86-64, aarch64 2026-12-09 (`End of Life <https://fedorapeople.org/groups/schedule/f-43/f-43-key-tasks.html>`__)
|
||||||
openSUSE Leap 15.6 x86-64, aarch64 2025-12-31 (`end of maintenance life cycle <https://en.opensuse.org/Lifetime#openSUSE_Leap>`__)
|
openSUSE Leap 15.6 x86-64, aarch64 2025-12-31 (`end of maintenance lifecycle <https://en.opensuse.org/Lifetime#openSUSE_Leap>`__)
|
||||||
openSUSE Tumbleweed x86-64, aarch64 forever (support is only provided for an up-to-date system)
|
openSUSE Tumbleweed x86-64, aarch64 forever (support is only provided for an up-to-date system)
|
||||||
Oracle Linux 8 x86-64, aarch64 2029-07-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
|
Oracle Linux 8 x86-64, aarch64 2029-07-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
|
||||||
Oracle Linux 9 x86-64, aarch64 2032-06-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
|
Oracle Linux 9 x86-64, aarch64 2032-06-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
|
||||||
@@ -73,8 +73,8 @@ RHEL 8.10 x86-64, aarch64 2029-05-31 (`End of
|
|||||||
RHEL 9 (latest) x86-64, aarch64 2032-05-31 (`End of Maintenance Support <https://access.redhat.com/support/policy/updates/errata#Life_Cycle_Dates>`__)
|
RHEL 9 (latest) x86-64, aarch64 2032-05-31 (`End of Maintenance Support <https://access.redhat.com/support/policy/updates/errata#Life_Cycle_Dates>`__)
|
||||||
RHEL 9.4 x86-64, aarch64 2026-04-30 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
RHEL 9.4 x86-64, aarch64 2026-04-30 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
||||||
RHEL 9.6 x86-64, aarch64 2027-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
RHEL 9.6 x86-64, aarch64 2027-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
||||||
Rocky Linux 8 x86-64, aarch64 2029-05-31 (`(i) Planned EOL <https://rockylinux.org/download>`__)
|
Rocky Linux 8 x86-64, aarch64 2029-05-31 (`End of Life <https://wiki.rockylinux.org/rocky/version/>`__)
|
||||||
Rocky Linux 9 x86-64, aarch64 2032-05-31 (`(i) Planned EOL <https://rockylinux.org/download>`__)
|
Rocky Linux 9 x86-64, aarch64 2032-05-31 (`End of Life <https://wiki.rockylinux.org/rocky/version/>`__)
|
||||||
Ubuntu 22.04 LTS x86-64, aarch64 2027-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
|
Ubuntu 22.04 LTS x86-64, aarch64 2027-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
|
||||||
Ubuntu 24.04 LTS x86-64, aarch64 2029-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
|
Ubuntu 24.04 LTS x86-64, aarch64 2029-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
|
||||||
================================ ======================= ============================================================
|
================================ ======================= ============================================================
|
||||||
|
|||||||
+1
-15
@@ -289,19 +289,6 @@ class VersionInfo:
|
|||||||
return version("Red-DiscordBot")
|
return version("Red-DiscordBot")
|
||||||
|
|
||||||
|
|
||||||
def _update_event_loop_policy():
|
|
||||||
if _sys.implementation.name == "cpython":
|
|
||||||
# Let's not force this dependency, uvloop is much faster on cpython
|
|
||||||
try:
|
|
||||||
import uvloop
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_no_colorama():
|
def _ensure_no_colorama():
|
||||||
# a hacky way to ensure that nothing initialises colorama
|
# a hacky way to ensure that nothing initialises colorama
|
||||||
# if we're not running with legacy Windows command line mode
|
# if we're not running with legacy Windows command line mode
|
||||||
@@ -334,12 +321,11 @@ def _early_init():
|
|||||||
# This function replaces logger so we preferably (though not necessarily) want that to happen
|
# This function replaces logger so we preferably (though not necessarily) want that to happen
|
||||||
# before importing anything that calls `logging.getLogger()`, i.e. `asyncio`.
|
# before importing anything that calls `logging.getLogger()`, i.e. `asyncio`.
|
||||||
_update_logger_class()
|
_update_logger_class()
|
||||||
_update_event_loop_policy()
|
|
||||||
_ensure_no_colorama()
|
_ensure_no_colorama()
|
||||||
|
|
||||||
|
|
||||||
# This is bumped automatically by release workflow (`.github/workflows/scripts/bump_version.py`)
|
# This is bumped automatically by release workflow (`.github/workflows/scripts/bump_version.py`)
|
||||||
_VERSION = "3.5.23"
|
_VERSION = "3.5.25.dev1"
|
||||||
|
|
||||||
__version__, version_info = VersionInfo._get_version()
|
__version__, version_info = VersionInfo._get_version()
|
||||||
|
|
||||||
|
|||||||
+19
-37
@@ -25,9 +25,9 @@ import rich
|
|||||||
import redbot.logging
|
import redbot.logging
|
||||||
from redbot import __version__
|
from redbot import __version__
|
||||||
from redbot.core.bot import Red, ExitCodes, _NoOwnerSet
|
from redbot.core.bot import Red, ExitCodes, _NoOwnerSet
|
||||||
from redbot.core._cli import interactive_config, confirm, parse_cli_flags
|
from redbot.core._cli import interactive_config, confirm, parse_cli_flags, new_event_loop
|
||||||
from redbot.setup import get_data_dir, get_name, save_config
|
from redbot.setup import get_data_dir, get_name, save_config
|
||||||
from redbot.core import data_manager, _drivers
|
from redbot.core import data_manager, _drivers, _downloader
|
||||||
from redbot.core._debuginfo import DebugInfo
|
from redbot.core._debuginfo import DebugInfo
|
||||||
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
|
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
|
||||||
|
|
||||||
@@ -182,32 +182,10 @@ async def _edit_owner(red, owner, no_prompt):
|
|||||||
|
|
||||||
def _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt):
|
def _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt):
|
||||||
if new_name:
|
if new_name:
|
||||||
name = new_name
|
name = get_name(new_name, confirm_overwrite=confirm_overwrite)
|
||||||
if name in _get_instance_names() and not confirm_overwrite:
|
|
||||||
name = old_name
|
|
||||||
print(
|
|
||||||
"An instance with this name already exists.\n"
|
|
||||||
"If you want to remove the existing instance and replace it with this one,"
|
|
||||||
" run this command with --overwrite-existing-instance flag."
|
|
||||||
)
|
|
||||||
elif not no_prompt and confirm("Would you like to change the instance name?", default=False):
|
elif not no_prompt and confirm("Would you like to change the instance name?", default=False):
|
||||||
name = get_name("")
|
name = get_name(confirm_overwrite=confirm_overwrite)
|
||||||
if name in _get_instance_names():
|
print("Instance name updated.\n")
|
||||||
print(
|
|
||||||
"WARNING: An instance already exists with this name. "
|
|
||||||
"Continuing will overwrite the existing instance config."
|
|
||||||
)
|
|
||||||
if not confirm(
|
|
||||||
"Are you absolutely certain you want to continue with this instance name?",
|
|
||||||
default=False,
|
|
||||||
):
|
|
||||||
print("Instance name will remain unchanged.")
|
|
||||||
name = old_name
|
|
||||||
else:
|
|
||||||
print("Instance name updated.")
|
|
||||||
else:
|
|
||||||
print("Instance name updated.")
|
|
||||||
print()
|
|
||||||
else:
|
else:
|
||||||
name = old_name
|
name = old_name
|
||||||
return name
|
return name
|
||||||
@@ -272,7 +250,7 @@ def early_exit_runner(
|
|||||||
"""
|
"""
|
||||||
This one exists to not log all the things like it's a full run of the bot.
|
This one exists to not log all the things like it's a full run of the bot.
|
||||||
"""
|
"""
|
||||||
loop = asyncio.new_event_loop()
|
loop = new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
if not cli_flags.instance_name:
|
if not cli_flags.instance_name:
|
||||||
@@ -281,7 +259,7 @@ def early_exit_runner(
|
|||||||
return
|
return
|
||||||
|
|
||||||
data_manager.load_basic_configuration(cli_flags.instance_name)
|
data_manager.load_basic_configuration(cli_flags.instance_name)
|
||||||
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None)
|
red = Red(cli_flags=cli_flags)
|
||||||
driver_cls = _drivers.get_driver_class()
|
driver_cls = _drivers.get_driver_class()
|
||||||
loop.run_until_complete(driver_cls.initialize(**data_manager.storage_details()))
|
loop.run_until_complete(driver_cls.initialize(**data_manager.storage_details()))
|
||||||
loop.run_until_complete(func(red, cli_flags))
|
loop.run_until_complete(func(red, cli_flags))
|
||||||
@@ -317,19 +295,23 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
|
|||||||
redbot.logging.init_logging(
|
redbot.logging.init_logging(
|
||||||
level=cli_flags.logging_level,
|
level=cli_flags.logging_level,
|
||||||
location=data_manager.core_data_path() / "logs",
|
location=data_manager.core_data_path() / "logs",
|
||||||
cli_flags=cli_flags,
|
rich_logging=cli_flags.rich_logging,
|
||||||
|
rich_tracebacks=cli_flags.rich_tracebacks,
|
||||||
|
rich_traceback_extra_lines=cli_flags.rich_traceback_extra_lines,
|
||||||
|
rich_traceback_show_locals=cli_flags.rich_traceback_show_locals,
|
||||||
)
|
)
|
||||||
|
|
||||||
log.debug("====Basic Config====")
|
log.debug("====Basic Config====")
|
||||||
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())
|
||||||
|
|
||||||
|
await _downloader._init(red)
|
||||||
|
|
||||||
# lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061)
|
# lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061)
|
||||||
# We might want to change handling of requirements in Downloader at later date
|
# We might want to change handling of requirements in Downloader at later date
|
||||||
LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib"
|
lib_path = str(_downloader.LIB_PATH)
|
||||||
LIB_PATH.mkdir(parents=True, exist_ok=True)
|
if lib_path not in sys.path:
|
||||||
if str(LIB_PATH) not in sys.path:
|
sys.path.append(lib_path)
|
||||||
sys.path.append(str(LIB_PATH))
|
|
||||||
|
|
||||||
# "It's important to note that the global `working_set` object is initialized from
|
# "It's important to note that the global `working_set` object is initialized from
|
||||||
# `sys.path` when `pkg_resources` is first imported, but is only updated if you do
|
# `sys.path` when `pkg_resources` is first imported, but is only updated if you do
|
||||||
@@ -339,7 +321,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
|
|||||||
# Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects
|
# Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects
|
||||||
pkg_resources = sys.modules.get("pkg_resources")
|
pkg_resources = sys.modules.get("pkg_resources")
|
||||||
if pkg_resources is not None:
|
if pkg_resources is not None:
|
||||||
pkg_resources.working_set.add_entry(str(LIB_PATH))
|
pkg_resources.working_set.add_entry(lib_path)
|
||||||
sys.meta_path.insert(0, SharedLibImportWarner())
|
sys.meta_path.insert(0, SharedLibImportWarner())
|
||||||
|
|
||||||
if cli_flags.token:
|
if cli_flags.token:
|
||||||
@@ -478,7 +460,7 @@ def main():
|
|||||||
early_exit_runner(cli_flags, edit_instance)
|
early_exit_runner(cli_flags, edit_instance)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
loop = asyncio.new_event_loop()
|
loop = new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
if cli_flags.no_instance:
|
if cli_flags.no_instance:
|
||||||
@@ -493,7 +475,7 @@ def main():
|
|||||||
|
|
||||||
data_manager.load_basic_configuration(cli_flags.instance_name)
|
data_manager.load_basic_configuration(cli_flags.instance_name)
|
||||||
|
|
||||||
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None)
|
red = Red(cli_flags=cli_flags)
|
||||||
|
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
# None of this works on windows.
|
# None of this works on windows.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from redbot.core.i18n import Translator
|
|||||||
from redbot.core.utils.chat_formatting import box, humanize_number
|
from redbot.core.utils.chat_formatting import box, humanize_number
|
||||||
from redbot.core.utils.menus import menu, start_adding_reactions
|
from redbot.core.utils.menus import menu, start_adding_reactions
|
||||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
|
from redbot.core.utils.views import SetApiView
|
||||||
|
|
||||||
from ...audio_dataclasses import LocalPath
|
from ...audio_dataclasses import LocalPath
|
||||||
from ...converters import ScopeParser
|
from ...converters import ScopeParser
|
||||||
@@ -1280,26 +1281,38 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
"6. Click on Create Credential at the top.\n"
|
"6. Click on Create Credential at the top.\n"
|
||||||
'7. At the top click the link for "API key".\n'
|
'7. At the top click the link for "API key".\n'
|
||||||
"8. No application restrictions are needed. Click Create at the bottom.\n"
|
"8. No application restrictions are needed. Click Create at the bottom.\n"
|
||||||
"9. You now have a key to add to `{prefix}set api youtube api_key <your_api_key_here>`"
|
"9. Click the button below this message and set your API key"
|
||||||
).format(prefix=ctx.prefix)
|
" with the data shown in Google Developers Console."
|
||||||
await ctx.maybe_send_embed(message)
|
)
|
||||||
|
await ctx.send(
|
||||||
|
message,
|
||||||
|
view=SetApiView(default_service="youtube", default_keys={"api_key": ""}),
|
||||||
|
)
|
||||||
|
|
||||||
@command_audioset.command(name="spotifyapi")
|
@command_audioset.command(name="spotifyapi")
|
||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
async def command_audioset_spotifyapi(self, ctx: commands.Context):
|
async def command_audioset_spotifyapi(self, ctx: commands.Context):
|
||||||
"""Instructions to set the Spotify API tokens."""
|
"""Instructions to set the Spotify API tokens."""
|
||||||
message = _(
|
message = _(
|
||||||
"1. Go to Spotify developers and log in with your Spotify account.\n"
|
"1. Go to Spotify for Developers and log in with your Spotify account."
|
||||||
"(https://developer.spotify.com/dashboard/applications)\n"
|
" If this is your first time, you'll be asked to accept the terms and conditions.\n"
|
||||||
'2. Click "Create An App".\n'
|
"(https://developer.spotify.com/dashboard)\n"
|
||||||
"3. Fill out the form provided with your app name, etc.\n"
|
'2. Click "Create app".\n'
|
||||||
'4. When asked if you\'re developing commercial integration select "No".\n'
|
"3. Fill out the form provided with your app name and description."
|
||||||
"5. Accept the terms and conditions.\n"
|
" These can be anything you want. Website field can be left empty.\n"
|
||||||
"6. Copy your client ID and your client secret into:\n"
|
"4. Add `https://localhost` to your Redirect URIs. This will not be used"
|
||||||
"`{prefix}set api spotify client_id <your_client_id_here> "
|
" but is required when filling out the form.\n"
|
||||||
"client_secret <your_client_secret_here>`"
|
'5. Select "Web API" when asked which API/SDKs you are planning to use.\n'
|
||||||
).format(prefix=ctx.prefix)
|
"6. Confirm that you agree to the terms and conditions and save the application.\n"
|
||||||
await ctx.maybe_send_embed(message)
|
"7. Click the button below this message and set your client ID and your client secret"
|
||||||
|
" with the data shown in Spotify's dashboard."
|
||||||
|
)
|
||||||
|
await ctx.send(
|
||||||
|
message,
|
||||||
|
view=SetApiView(
|
||||||
|
default_service="spotify", default_keys={"client_id": "", "client_secret": ""}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@command_audioset.command(name="countrycode")
|
@command_audioset.command(name="countrycode")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
|||||||
+74
-67
@@ -53,7 +53,7 @@ msgstr "Não foi possível tocar a música"
|
|||||||
#: redbot/cogs/audio/core/utilities/player.py:442
|
#: redbot/cogs/audio/core/utilities/player.py:442
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:524
|
#: redbot/cogs/audio/core/utilities/player.py:524
|
||||||
msgid "Queue size limit reached."
|
msgid "Queue size limit reached."
|
||||||
msgstr ""
|
msgstr "Limite da fila atingindo."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:154
|
#: redbot/cogs/audio/core/utilities/formatting.py:154
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:599
|
#: redbot/cogs/audio/core/utilities/player.py:599
|
||||||
@@ -63,41 +63,41 @@ msgstr "Faixa Enfileirada"
|
|||||||
#: redbot/cogs/audio/core/utilities/formatting.py:168
|
#: redbot/cogs/audio/core/utilities/formatting.py:168
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:548
|
#: redbot/cogs/audio/core/utilities/player.py:548
|
||||||
msgid "This track is not allowed in this server."
|
msgid "This track is not allowed in this server."
|
||||||
msgstr ""
|
msgstr "Esta faixa não é permitida neste servidor."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:185
|
#: redbot/cogs/audio/core/utilities/formatting.py:185
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:570
|
#: redbot/cogs/audio/core/utilities/player.py:570
|
||||||
msgid "Track exceeds maximum length."
|
msgid "Track exceeds maximum length."
|
||||||
msgstr ""
|
msgstr "Faixa excede comprimento máximo."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:200
|
#: redbot/cogs/audio/core/utilities/formatting.py:200
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:602
|
#: redbot/cogs/audio/core/utilities/player.py:602
|
||||||
msgid "{time} until track playback: #{position} in queue"
|
msgid "{time} until track playback: #{position} in queue"
|
||||||
msgstr ""
|
msgstr "{time} até a reprodução da faixa: #{position} na fila"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:260
|
#: redbot/cogs/audio/core/utilities/formatting.py:260
|
||||||
msgid "Tracks Found:"
|
msgid "Tracks Found:"
|
||||||
msgstr ""
|
msgstr "Faixas Encontradas:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:261
|
#: redbot/cogs/audio/core/utilities/formatting.py:261
|
||||||
msgid "search results"
|
msgid "search results"
|
||||||
msgstr ""
|
msgstr "resultados da pesquisa"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:263
|
#: redbot/cogs/audio/core/utilities/formatting.py:263
|
||||||
msgid "Folders Found:"
|
msgid "Folders Found:"
|
||||||
msgstr ""
|
msgstr "Pastas Encontradas:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:264
|
#: redbot/cogs/audio/core/utilities/formatting.py:264
|
||||||
msgid "local folders"
|
msgid "local folders"
|
||||||
msgstr ""
|
msgstr "pastas locais"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:266
|
#: redbot/cogs/audio/core/utilities/formatting.py:266
|
||||||
msgid "Files Found:"
|
msgid "Files Found:"
|
||||||
msgstr ""
|
msgstr "Arquivos Encontrados:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:267
|
#: redbot/cogs/audio/core/utilities/formatting.py:267
|
||||||
msgid "local tracks"
|
msgid "local tracks"
|
||||||
msgstr ""
|
msgstr "faixas locais"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/formatting.py:379
|
#: redbot/cogs/audio/core/utilities/formatting.py:379
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:240
|
#: redbot/cogs/audio/core/utilities/playlists.py:240
|
||||||
@@ -122,15 +122,15 @@ msgstr "Ambiente inválido"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/local_tracks.py:109
|
#: redbot/cogs/audio/core/utilities/local_tracks.py:109
|
||||||
msgid "No localtracks folder."
|
msgid "No localtracks folder."
|
||||||
msgstr ""
|
msgstr "Sem pasta localtracks."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/miscellaneous.py:50
|
#: redbot/cogs/audio/core/utilities/miscellaneous.py:50
|
||||||
msgid "Not enough {currency}"
|
msgid "Not enough {currency}"
|
||||||
msgstr ""
|
msgstr "Sem {currency} suficiente"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/miscellaneous.py:51
|
#: redbot/cogs/audio/core/utilities/miscellaneous.py:51
|
||||||
msgid "{required_credits} {currency} required, but you have {bal}."
|
msgid "{required_credits} {currency} required, but you have {bal}."
|
||||||
msgstr ""
|
msgstr "{required_credits} {currency} necessário, mas você possui {bal}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:78
|
#: redbot/cogs/audio/core/utilities/player.py:78
|
||||||
msgid "music in {} servers"
|
msgid "music in {} servers"
|
||||||
@@ -140,54 +140,56 @@ msgstr "música em {} servidores"
|
|||||||
#: redbot/cogs/audio/core/utilities/player.py:139
|
#: redbot/cogs/audio/core/utilities/player.py:139
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:144
|
#: redbot/cogs/audio/core/utilities/player.py:144
|
||||||
msgid "There's nothing in the queue."
|
msgid "There's nothing in the queue."
|
||||||
msgstr ""
|
msgstr "Não há nada na fila."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:141
|
#: redbot/cogs/audio/core/utilities/player.py:141
|
||||||
msgid "Currently livestreaming {track}"
|
msgid "Currently livestreaming {track}"
|
||||||
msgstr ""
|
msgstr "Transmitindo agora {track}"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:146
|
#: redbot/cogs/audio/core/utilities/player.py:146
|
||||||
msgid "{time} left on {track}"
|
msgid "{time} left on {track}"
|
||||||
msgstr ""
|
msgstr "{time} restante de {track}"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:154
|
#: redbot/cogs/audio/core/utilities/player.py:154
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:189
|
#: redbot/cogs/audio/core/utilities/player.py:189
|
||||||
msgid "Track Skipped"
|
msgid "Track Skipped"
|
||||||
msgstr ""
|
msgstr "Faixa Pulada"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:167
|
#: redbot/cogs/audio/core/utilities/player.py:167
|
||||||
msgid "Track number must be equal to or greater than 1."
|
msgid "Track number must be equal to or greater than 1."
|
||||||
msgstr ""
|
msgstr "O número da faixa deve ser igual ou maior que 1."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:173
|
#: redbot/cogs/audio/core/utilities/player.py:173
|
||||||
msgid "There are only {queuelen} songs currently queued."
|
msgid "There are only {queuelen} songs currently queued."
|
||||||
msgstr ""
|
msgstr "Há apenas músicas {queuelen} na fila."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:179
|
#: redbot/cogs/audio/core/utilities/player.py:179
|
||||||
msgid "{skip_to_track} Tracks Skipped"
|
msgid "{skip_to_track} Tracks Skipped"
|
||||||
msgstr ""
|
msgstr "{skip_to_track} Faixas Puladas"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:235
|
#: redbot/cogs/audio/core/utilities/player.py:235
|
||||||
msgid "The owner needs to set the Spotify client ID and Spotify client secret, before Spotify URLs or codes can be used. \n"
|
msgid "The owner needs to set the Spotify client ID and Spotify client secret, before Spotify URLs or codes can be used. \n"
|
||||||
"See `{prefix}audioset spotifyapi` for instructions."
|
"See `{prefix}audioset spotifyapi` for instructions."
|
||||||
msgstr ""
|
msgstr "O proprietário precisa definir o ID do cliente do Spotify e o Spotify Client Secret, antes que possam ser usadas URLs ou códigos do Spotify. \n"
|
||||||
|
"Veja `{prefix}audioset spotifyapi` para instruções."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:245
|
#: redbot/cogs/audio/core/utilities/player.py:245
|
||||||
msgid "The owner needs to set the YouTube API key before Spotify URLs or codes can be used.\n"
|
msgid "The owner needs to set the YouTube API key before Spotify URLs or codes can be used.\n"
|
||||||
"See `{prefix}audioset youtubeapi` for instructions."
|
"See `{prefix}audioset youtubeapi` for instructions."
|
||||||
msgstr ""
|
msgstr "O proprietário precisa definir a chave da API do YouTube antes que URLs ou códigos do Spotify possam ser usados.\n"
|
||||||
|
"Veja `{prefix}audioset youtubeapi` para instruções."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:254
|
#: redbot/cogs/audio/core/utilities/player.py:254
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:363
|
#: redbot/cogs/audio/core/utilities/player.py:363
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:594
|
#: redbot/cogs/audio/core/utilities/playlists.py:594
|
||||||
msgid "Unable To Get Tracks"
|
msgid "Unable To Get Tracks"
|
||||||
msgstr ""
|
msgstr "Não foi possível obter as faixas"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:255
|
#: redbot/cogs/audio/core/utilities/player.py:255
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:364
|
#: redbot/cogs/audio/core/utilities/player.py:364
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:595
|
#: redbot/cogs/audio/core/utilities/playlists.py:595
|
||||||
msgid "Wait until the playlist has finished loading."
|
msgid "Wait until the playlist has finished loading."
|
||||||
msgstr ""
|
msgstr "Aguarde até que a playlist termine de carregar."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:266
|
#: redbot/cogs/audio/core/utilities/player.py:266
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:308
|
#: redbot/cogs/audio/core/utilities/player.py:308
|
||||||
@@ -203,7 +205,7 @@ msgstr "Nada encontrado."
|
|||||||
#: redbot/cogs/audio/core/utilities/playlists.py:607
|
#: redbot/cogs/audio/core/utilities/playlists.py:607
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:640
|
#: redbot/cogs/audio/core/utilities/playlists.py:640
|
||||||
msgid "Track is not playable."
|
msgid "Track is not playable."
|
||||||
msgstr ""
|
msgstr "Faixa não é reproduzível."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:270
|
#: redbot/cogs/audio/core/utilities/player.py:270
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:311
|
#: redbot/cogs/audio/core/utilities/player.py:311
|
||||||
@@ -211,7 +213,7 @@ msgstr ""
|
|||||||
#: redbot/cogs/audio/core/utilities/playlists.py:608
|
#: redbot/cogs/audio/core/utilities/playlists.py:608
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:641
|
#: redbot/cogs/audio/core/utilities/playlists.py:641
|
||||||
msgid "**{suffix}** is not a fully supported format and some tracks may not play."
|
msgid "**{suffix}** is not a fully supported format and some tracks may not play."
|
||||||
msgstr ""
|
msgstr "**{suffix}** não é um formato totalmente suportado e algumas faixas podem não reproduzir."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:300
|
#: redbot/cogs/audio/core/utilities/player.py:300
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:393
|
#: redbot/cogs/audio/core/utilities/player.py:393
|
||||||
@@ -235,7 +237,7 @@ msgstr "A chave de API do Spotify ou segredo do cliente não foram definidos cor
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:351
|
#: redbot/cogs/audio/core/utilities/player.py:351
|
||||||
msgid "Unable To Find Tracks"
|
msgid "Unable To Find Tracks"
|
||||||
msgstr ""
|
msgstr "Não foi possível encontrar as faixas"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:352
|
#: redbot/cogs/audio/core/utilities/player.py:352
|
||||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||||
@@ -243,26 +245,27 @@ msgstr "Isto não parece ser uma URL ou código do Spotify válido."
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:378
|
#: redbot/cogs/audio/core/utilities/player.py:378
|
||||||
msgid "{query} is not an allowed query."
|
msgid "{query} is not an allowed query."
|
||||||
msgstr ""
|
msgstr "{query} não é uma solicitação permitida."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:394
|
#: redbot/cogs/audio/core/utilities/player.py:394
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:627
|
#: redbot/cogs/audio/core/utilities/playlists.py:627
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:656
|
#: redbot/cogs/audio/core/utilities/playlists.py:656
|
||||||
msgid "I'm unable to get a track from Lavalink node at the moment, try again in a few minutes."
|
msgid "I'm unable to get a track from Lavalink node at the moment, try again in a few minutes."
|
||||||
msgstr ""
|
msgstr "Não foi possível obter uma faixa do Lavalink Node no momento, tente novamente em alguns minutos."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:416
|
#: redbot/cogs/audio/core/utilities/player.py:416
|
||||||
msgid "Local tracks will not work if the `Lavalink.jar` cannot see the track.\n"
|
msgid "Local tracks will not work if the `Lavalink.jar` cannot see the track.\n"
|
||||||
"This may be due to permissions or because Lavalink.jar is being run in a different machine than the local tracks."
|
"This may be due to permissions or because Lavalink.jar is being run in a different machine than the local tracks."
|
||||||
msgstr ""
|
msgstr "As faixas locais não funcionarão se o `Lavalink.jar` não conseguir ver a faixa.\n"
|
||||||
|
"Isto pode ser devido a permissões ou porque o Lavalink.jar está sendo executado em uma máquina diferente das faixas locais."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:486
|
#: redbot/cogs/audio/core/utilities/player.py:486
|
||||||
msgid " {bad_tracks} tracks cannot be queued."
|
msgid " {bad_tracks} tracks cannot be queued."
|
||||||
msgstr ""
|
msgstr " {bad_tracks} faixas não puderam ser adicionadas."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:492
|
#: redbot/cogs/audio/core/utilities/player.py:492
|
||||||
msgid "No Title"
|
msgid "No Title"
|
||||||
msgstr ""
|
msgstr "Sem Título"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:494
|
#: redbot/cogs/audio/core/utilities/player.py:494
|
||||||
msgid "Playlist Enqueued"
|
msgid "Playlist Enqueued"
|
||||||
@@ -270,7 +273,7 @@ msgstr "Lista de reprodução enfileirada"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:494
|
#: redbot/cogs/audio/core/utilities/player.py:494
|
||||||
msgid "Album Enqueued"
|
msgid "Album Enqueued"
|
||||||
msgstr ""
|
msgstr "Álbum Adicionado"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:502
|
#: redbot/cogs/audio/core/utilities/player.py:502
|
||||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||||
@@ -286,25 +289,25 @@ msgstr "Nada foi encontrado"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:623
|
#: redbot/cogs/audio/core/utilities/player.py:623
|
||||||
msgid "Please wait, finding tracks..."
|
msgid "Please wait, finding tracks..."
|
||||||
msgstr ""
|
msgstr "Por favor, aguarde, encontrando faixas..."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:629
|
#: redbot/cogs/audio/core/utilities/player.py:629
|
||||||
msgid "Getting track {num}/{total}..."
|
msgid "Getting track {num}/{total}..."
|
||||||
msgstr ""
|
msgstr "Obtendo faixa {num}/{total}..."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:630
|
#: redbot/cogs/audio/core/utilities/player.py:630
|
||||||
msgid "Matching track {num}/{total}..."
|
msgid "Matching track {num}/{total}..."
|
||||||
msgstr ""
|
msgstr "Correspondendo faixa {num}/{total}..."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:631
|
#: redbot/cogs/audio/core/utilities/player.py:631
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:341
|
#: redbot/cogs/audio/core/utilities/playlists.py:341
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:414
|
#: redbot/cogs/audio/core/utilities/playlists.py:414
|
||||||
msgid "Loading track {num}/{total}..."
|
msgid "Loading track {num}/{total}..."
|
||||||
msgstr ""
|
msgstr "Carregando faixa {num}/{total}..."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:632
|
#: redbot/cogs/audio/core/utilities/player.py:632
|
||||||
msgid "Approximate time remaining: {seconds}"
|
msgid "Approximate time remaining: {seconds}"
|
||||||
msgstr ""
|
msgstr "Tempo restante aproximado: {seconds}"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/player.py:658
|
#: redbot/cogs/audio/core/utilities/player.py:658
|
||||||
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
||||||
@@ -316,27 +319,27 @@ msgstr "A conexão foi redefinida durante o carregamento da lista de reproduçã
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:83
|
#: redbot/cogs/audio/core/utilities/playlists.py:83
|
||||||
msgid "You do not have the permissions to manage {name} (`{id}`) [**{scope}**]."
|
msgid "You do not have the permissions to manage {name} (`{id}`) [**{scope}**]."
|
||||||
msgstr ""
|
msgstr "Você não tem as permissões para gerenciar {name} (`{id}`) [**{scope}**]."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:101
|
#: redbot/cogs/audio/core/utilities/playlists.py:101
|
||||||
msgid "You do not have the permissions to manage that playlist in {guild}."
|
msgid "You do not have the permissions to manage that playlist in {guild}."
|
||||||
msgstr ""
|
msgstr "Você não tem permissão para gerenciar essa playlist no {guild}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:108
|
#: redbot/cogs/audio/core/utilities/playlists.py:108
|
||||||
msgid "You do not have the permissions to manage playlist owned by {user}."
|
msgid "You do not have the permissions to manage playlist owned by {user}."
|
||||||
msgstr ""
|
msgstr "Você não tem permissão para gerenciar a playlist de {user}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:112
|
#: redbot/cogs/audio/core/utilities/playlists.py:112
|
||||||
msgid "You do not have the permissions to manage playlists in {scope} scope."
|
msgid "You do not have the permissions to manage playlists in {scope} scope."
|
||||||
msgstr ""
|
msgstr "Você não tem as permissões para gerenciar playlists no escopo {scope}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:116
|
#: redbot/cogs/audio/core/utilities/playlists.py:116
|
||||||
msgid "No access to playlist."
|
msgid "No access to playlist."
|
||||||
msgstr ""
|
msgstr "Sem acesso à playlist."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:224
|
#: redbot/cogs/audio/core/utilities/playlists.py:224
|
||||||
msgid "{match_count} playlists match {original_input}: Please try to be more specific, or use the playlist ID."
|
msgid "{match_count} playlists match {original_input}: Please try to be more specific, or use the playlist ID."
|
||||||
msgstr ""
|
msgstr "{match_count} playlists correspondem {original_input}: Por favor, tente ser mais específico, ou use o ID da playlist."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:241
|
#: redbot/cogs/audio/core/utilities/playlists.py:241
|
||||||
msgid "{number}. <{playlist.name}>\n"
|
msgid "{number}. <{playlist.name}>\n"
|
||||||
@@ -344,24 +347,28 @@ msgid "{number}. <{playlist.name}>\n"
|
|||||||
" - ID: < {playlist.id} >\n"
|
" - ID: < {playlist.id} >\n"
|
||||||
" - Tracks: < {tracks} >\n"
|
" - Tracks: < {tracks} >\n"
|
||||||
" - Author: < {author} >\n\n"
|
" - Author: < {author} >\n\n"
|
||||||
msgstr ""
|
msgstr "{number}. <{playlist.name}>\n"
|
||||||
|
" - Escopo: < {scope} >\n"
|
||||||
|
" - ID: < {playlist.id} >\n"
|
||||||
|
" - Faixas: < {tracks} >\n"
|
||||||
|
" - Autor: < {author} >\n\n"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:258
|
#: redbot/cogs/audio/core/utilities/playlists.py:258
|
||||||
msgid "{playlists} playlists found, which one would you like?"
|
msgid "{playlists} playlists found, which one would you like?"
|
||||||
msgstr ""
|
msgstr "{playlists} playlists encontradas, de qual você gostaria?"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:277
|
#: redbot/cogs/audio/core/utilities/playlists.py:277
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:283
|
#: redbot/cogs/audio/core/utilities/playlists.py:283
|
||||||
msgid "Too many matches found and you did not select which one you wanted."
|
msgid "Too many matches found and you did not select which one you wanted."
|
||||||
msgstr ""
|
msgstr "Muitas opções foram encontradas e você não selecionou qual você queria."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:308
|
#: redbot/cogs/audio/core/utilities/playlists.py:308
|
||||||
msgid "Playlists you can access in this server:"
|
msgid "Playlists you can access in this server:"
|
||||||
msgstr ""
|
msgstr "Playlists que você pode acessar neste servidor:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:314
|
#: redbot/cogs/audio/core/utilities/playlists.py:314
|
||||||
msgid "Playlists for {scope}:"
|
msgid "Playlists for {scope}:"
|
||||||
msgstr ""
|
msgstr "Playlists para {scope}:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:318
|
#: redbot/cogs/audio/core/utilities/playlists.py:318
|
||||||
msgid "Page {page_num}/{total_pages} | {num} playlists."
|
msgid "Page {page_num}/{total_pages} | {num} playlists."
|
||||||
@@ -370,46 +377,46 @@ msgstr ""
|
|||||||
#: redbot/cogs/audio/core/utilities/playlists.py:334
|
#: redbot/cogs/audio/core/utilities/playlists.py:334
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:412
|
#: redbot/cogs/audio/core/utilities/playlists.py:412
|
||||||
msgid "Please wait, adding tracks..."
|
msgid "Please wait, adding tracks..."
|
||||||
msgstr ""
|
msgstr "Por favor, aguarde, adicionando faixas..."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:361
|
#: redbot/cogs/audio/core/utilities/playlists.py:361
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:464
|
#: redbot/cogs/audio/core/utilities/playlists.py:464
|
||||||
msgid "Empty playlist {name} (`{id}`) [**{scope}**] created."
|
msgid "Empty playlist {name} (`{id}`) [**{scope}**] created."
|
||||||
msgstr ""
|
msgstr "Playlist vazia {name} (`{id}`) [**{scope}**] criada."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:366
|
#: redbot/cogs/audio/core/utilities/playlists.py:366
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:469
|
#: redbot/cogs/audio/core/utilities/playlists.py:469
|
||||||
msgid "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) could not be loaded."
|
msgid "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) could not be loaded."
|
||||||
msgstr ""
|
msgstr "Adicionadas {num} músicas da lista {playlist_name} . Não foi possível carregar a(s) faixa(s) {num_bad}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:371
|
#: redbot/cogs/audio/core/utilities/playlists.py:371
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:474
|
#: redbot/cogs/audio/core/utilities/playlists.py:474
|
||||||
msgid "Added {num} tracks from the {playlist_name} playlist."
|
msgid "Added {num} tracks from the {playlist_name} playlist."
|
||||||
msgstr ""
|
msgstr "Adicionadas {num} músicas da lista {playlist_name}."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:375
|
#: redbot/cogs/audio/core/utilities/playlists.py:375
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:478
|
#: redbot/cogs/audio/core/utilities/playlists.py:478
|
||||||
msgid "Playlist Saved"
|
msgid "Playlist Saved"
|
||||||
msgstr ""
|
msgstr "Playlist salva"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:540
|
#: redbot/cogs/audio/core/utilities/playlists.py:540
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:553
|
#: redbot/cogs/audio/core/utilities/playlists.py:553
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:560
|
#: redbot/cogs/audio/core/utilities/playlists.py:560
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:571
|
#: redbot/cogs/audio/core/utilities/playlists.py:571
|
||||||
msgid "Unable To Get Playlists"
|
msgid "Unable To Get Playlists"
|
||||||
msgstr ""
|
msgstr "Não foi possível obter as playlists"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:541
|
#: redbot/cogs/audio/core/utilities/playlists.py:541
|
||||||
msgid "I don't have permission to connect and speak in your channel."
|
msgid "I don't have permission to connect and speak in your channel."
|
||||||
msgstr ""
|
msgstr "Não tenho permissão para conectar e falar em seu canal."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:572
|
#: redbot/cogs/audio/core/utilities/playlists.py:572
|
||||||
msgid "You must be in the voice channel to use the playlist command."
|
msgid "You must be in the voice channel to use the playlist command."
|
||||||
msgstr ""
|
msgstr "Você deve estar no canal de voz para usar esse comando."
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:680
|
#: redbot/cogs/audio/core/utilities/playlists.py:680
|
||||||
msgid "the Global"
|
msgid "the Global"
|
||||||
msgstr ""
|
msgstr "o Global"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:680
|
#: redbot/cogs/audio/core/utilities/playlists.py:680
|
||||||
msgid "Global"
|
msgid "Global"
|
||||||
@@ -417,7 +424,7 @@ msgstr "Global"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:682
|
#: redbot/cogs/audio/core/utilities/playlists.py:682
|
||||||
msgid "the Server"
|
msgid "the Server"
|
||||||
msgstr ""
|
msgstr "o Servidor"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:682
|
#: redbot/cogs/audio/core/utilities/playlists.py:682
|
||||||
msgid "Server"
|
msgid "Server"
|
||||||
@@ -425,7 +432,7 @@ msgstr "Servidor"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:684
|
#: redbot/cogs/audio/core/utilities/playlists.py:684
|
||||||
msgid "the User"
|
msgid "the User"
|
||||||
msgstr ""
|
msgstr "o Usuário"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/playlists.py:684
|
#: redbot/cogs/audio/core/utilities/playlists.py:684
|
||||||
msgid "User"
|
msgid "User"
|
||||||
@@ -433,20 +440,20 @@ msgstr "Usuário"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:40
|
#: redbot/cogs/audio/core/utilities/queue.py:40
|
||||||
msgid "__Too many songs in the queue, only showing the first 500__.\n\n"
|
msgid "__Too many songs in the queue, only showing the first 500__.\n\n"
|
||||||
msgstr ""
|
msgstr "__Muitas músicas na fila, mostrando apenas os primeiros 500__.\n\n"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:57
|
#: redbot/cogs/audio/core/utilities/queue.py:57
|
||||||
msgid "**Currently livestreaming:**\n"
|
msgid "**Currently livestreaming:**\n"
|
||||||
msgstr ""
|
msgstr "**Transmitindo agora:**\n"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:59
|
#: redbot/cogs/audio/core/utilities/queue.py:59
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:64
|
#: redbot/cogs/audio/core/utilities/queue.py:64
|
||||||
msgid "Requested by: **{user}**"
|
msgid "Requested by: **{user}**"
|
||||||
msgstr ""
|
msgstr "Solicitado por: **{user}**"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:62
|
#: redbot/cogs/audio/core/utilities/queue.py:62
|
||||||
msgid "Playing: "
|
msgid "Playing: "
|
||||||
msgstr ""
|
msgstr "Reproduzindo: "
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:76
|
#: redbot/cogs/audio/core/utilities/queue.py:76
|
||||||
msgid "requested by **{user}**\n"
|
msgid "requested by **{user}**\n"
|
||||||
@@ -454,11 +461,11 @@ msgstr ""
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:80
|
#: redbot/cogs/audio/core/utilities/queue.py:80
|
||||||
msgid "Queue for __{guild_name}__"
|
msgid "Queue for __{guild_name}__"
|
||||||
msgstr ""
|
msgstr "Fila para __{guild_name}__"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:88
|
#: redbot/cogs/audio/core/utilities/queue.py:88
|
||||||
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks, {num_remaining} remaining\n"
|
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks, {num_remaining} remaining\n"
|
||||||
msgstr ""
|
msgstr "Página {page_num}/{total_pages} {num_tracks} faixas, {num_remaining} restantes\n"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:97
|
#: redbot/cogs/audio/core/utilities/queue.py:97
|
||||||
msgid "Auto-Play"
|
msgid "Auto-Play"
|
||||||
@@ -474,7 +481,7 @@ msgstr "Repetir"
|
|||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:161
|
#: redbot/cogs/audio/core/utilities/queue.py:161
|
||||||
msgid "Matching Tracks:"
|
msgid "Matching Tracks:"
|
||||||
msgstr ""
|
msgstr "Faixas correspondentes:"
|
||||||
|
|
||||||
#: redbot/cogs/audio/core/utilities/queue.py:164
|
#: redbot/cogs/audio/core/utilities/queue.py:164
|
||||||
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks"
|
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks"
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
JAR_VERSION: Final[LavalinkVersion] = LavalinkVersion(3, 7, 13, red=2)
|
JAR_VERSION: Final[LavalinkVersion] = LavalinkVersion(3, 7, 13, red=5)
|
||||||
YT_PLUGIN_VERSION: Final[str] = "1.18.0"
|
YT_PLUGIN_VERSION: Final[str] = "1.18.0"
|
||||||
# keep this sorted from oldest to latest
|
# keep this sorted from oldest to latest
|
||||||
SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = (11, 17)
|
SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = (17, 21)
|
||||||
LATEST_SUPPORTED_JAVA_VERSION: Final = SUPPORTED_JAVA_VERSIONS[-1]
|
LATEST_SUPPORTED_JAVA_VERSION: Final = SUPPORTED_JAVA_VERSIONS[-1]
|
||||||
OLDER_SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = SUPPORTED_JAVA_VERSIONS[:-1]
|
OLDER_SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = SUPPORTED_JAVA_VERSIONS[:-1]
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ from .downloader import Downloader
|
|||||||
async def setup(bot: Red) -> None:
|
async def setup(bot: Red) -> None:
|
||||||
cog = Downloader(bot)
|
cog = Downloader(bot)
|
||||||
await bot.add_cog(cog)
|
await bot.add_cog(cog)
|
||||||
cog.create_init_task()
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import _downloader, commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
from .installable import InstalledModule
|
from redbot.core._downloader.installable import InstalledModule
|
||||||
|
from redbot.core._downloader.repo_manager import Repo as _Repo
|
||||||
|
|
||||||
_ = Translator("Koala", __file__)
|
_ = Translator("Koala", __file__)
|
||||||
|
|
||||||
@@ -9,14 +10,21 @@ _ = Translator("Koala", __file__)
|
|||||||
class InstalledCog(InstalledModule):
|
class InstalledCog(InstalledModule):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
|
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
cog = discord.utils.get(await _downloader.installed_cogs(), name=arg)
|
||||||
if downloader is None:
|
|
||||||
raise commands.CommandError(_("No Downloader cog found."))
|
|
||||||
|
|
||||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
|
||||||
if cog is None:
|
if cog is None:
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
|
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
|
||||||
)
|
)
|
||||||
|
|
||||||
return cog
|
return cog
|
||||||
|
|
||||||
|
|
||||||
|
class Repo(_Repo):
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: commands.Context, argument: str) -> _Repo:
|
||||||
|
poss_repo = _downloader._repo_manager.get_repo(argument)
|
||||||
|
if poss_repo is None:
|
||||||
|
raise commands.BadArgument(
|
||||||
|
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
|
||||||
|
)
|
||||||
|
return poss_repo
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Generated
+8
-8
@@ -23,16 +23,16 @@ msgstr ""
|
|||||||
|
|
||||||
#: redbot/cogs/downloader/checks.py:38
|
#: redbot/cogs/downloader/checks.py:38
|
||||||
msgid "Your response has timed out, please try again."
|
msgid "Your response has timed out, please try again."
|
||||||
msgstr ""
|
msgstr "Sua resposta expirou. Por favor, tente novamente."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/converters.py:14
|
#: redbot/cogs/downloader/converters.py:14
|
||||||
#: redbot/cogs/downloader/repo_manager.py:176
|
#: redbot/cogs/downloader/repo_manager.py:176
|
||||||
msgid "No Downloader cog found."
|
msgid "No Downloader cog found."
|
||||||
msgstr ""
|
msgstr "Nenhum cog Downloader foi encontrado."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/converters.py:19
|
#: redbot/cogs/downloader/converters.py:19
|
||||||
msgid "Cog `{cog_name}` is not installed."
|
msgid "Cog `{cog_name}` is not installed."
|
||||||
msgstr ""
|
msgstr "O Cog `{cog_name}` não está instalado."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:31
|
#: redbot/cogs/downloader/downloader.py:31
|
||||||
msgid "\n"
|
msgid "\n"
|
||||||
@@ -67,24 +67,24 @@ msgstr ""
|
|||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:508
|
#: redbot/cogs/downloader/downloader.py:508
|
||||||
msgid "Libraries installed."
|
msgid "Libraries installed."
|
||||||
msgstr ""
|
msgstr "Bibliotecas instaladas."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:508
|
#: redbot/cogs/downloader/downloader.py:508
|
||||||
msgid "Library installed."
|
msgid "Library installed."
|
||||||
msgstr ""
|
msgstr "Biblioteca instalada."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:511
|
#: redbot/cogs/downloader/downloader.py:511
|
||||||
msgid "Some libraries failed to install. Please check your logs for a complete list."
|
msgid "Some libraries failed to install. Please check your logs for a complete list."
|
||||||
msgstr ""
|
msgstr "Não foi possível instalar algumas bibliotecas. Verifique os seus logs para ter uma lista completa."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:516
|
#: redbot/cogs/downloader/downloader.py:516
|
||||||
msgid "The library failed to install. Please check your logs for a complete list."
|
msgid "The library failed to install. Please check your logs for a complete list."
|
||||||
msgstr ""
|
msgstr "A biblioteca não foi instalada. Por favor, verifique os seus logs para ter uma lista completa."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:524
|
#: redbot/cogs/downloader/downloader.py:524
|
||||||
#, docstring
|
#, docstring
|
||||||
msgid "Base command for repository management."
|
msgid "Base command for repository management."
|
||||||
msgstr ""
|
msgstr "Comando base para gerenciamento do repositório."
|
||||||
|
|
||||||
#: redbot/cogs/downloader/downloader.py:531
|
#: redbot/cogs/downloader/downloader.py:531
|
||||||
#, docstring
|
#, docstring
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Generated
+41
-31
@@ -18,87 +18,87 @@ msgstr ""
|
|||||||
#: redbot/cogs/general/general.py:49
|
#: redbot/cogs/general/general.py:49
|
||||||
#, docstring
|
#, docstring
|
||||||
msgid "General commands."
|
msgid "General commands."
|
||||||
msgstr ""
|
msgstr "Comandos gerais."
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:54
|
#: redbot/cogs/general/general.py:54
|
||||||
msgid "As I see it, yes"
|
msgid "As I see it, yes"
|
||||||
msgstr ""
|
msgstr "Como eu vejo, sim"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:55
|
#: redbot/cogs/general/general.py:55
|
||||||
msgid "It is certain"
|
msgid "It is certain"
|
||||||
msgstr ""
|
msgstr "Com certeza"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:56
|
#: redbot/cogs/general/general.py:56
|
||||||
msgid "It is decidedly so"
|
msgid "It is decidedly so"
|
||||||
msgstr ""
|
msgstr "É decididamente assim"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:57
|
#: redbot/cogs/general/general.py:57
|
||||||
msgid "Most likely"
|
msgid "Most likely"
|
||||||
msgstr ""
|
msgstr "Muito provável"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:58
|
#: redbot/cogs/general/general.py:58
|
||||||
msgid "Outlook good"
|
msgid "Outlook good"
|
||||||
msgstr ""
|
msgstr "Perspectiva boa"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:59
|
#: redbot/cogs/general/general.py:59
|
||||||
msgid "Signs point to yes"
|
msgid "Signs point to yes"
|
||||||
msgstr ""
|
msgstr "Os sinais indicam que sim"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:60
|
#: redbot/cogs/general/general.py:60
|
||||||
msgid "Without a doubt"
|
msgid "Without a doubt"
|
||||||
msgstr ""
|
msgstr "Sem dúvida"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:61
|
#: redbot/cogs/general/general.py:61
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr "Sim"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:62
|
#: redbot/cogs/general/general.py:62
|
||||||
msgid "Yes – definitely"
|
msgid "Yes – definitely"
|
||||||
msgstr ""
|
msgstr "Sim – definitivamente"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:63
|
#: redbot/cogs/general/general.py:63
|
||||||
msgid "You may rely on it"
|
msgid "You may rely on it"
|
||||||
msgstr ""
|
msgstr "Você pode contar com isso"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:64
|
#: redbot/cogs/general/general.py:64
|
||||||
msgid "Reply hazy, try again"
|
msgid "Reply hazy, try again"
|
||||||
msgstr ""
|
msgstr "Resposta confusa, tente novamente"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:65
|
#: redbot/cogs/general/general.py:65
|
||||||
msgid "Ask again later"
|
msgid "Ask again later"
|
||||||
msgstr ""
|
msgstr "Pergunte novamente mais tarde"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:66
|
#: redbot/cogs/general/general.py:66
|
||||||
msgid "Better not tell you now"
|
msgid "Better not tell you now"
|
||||||
msgstr ""
|
msgstr "Melhor não te contar agora"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:67
|
#: redbot/cogs/general/general.py:67
|
||||||
msgid "Cannot predict now"
|
msgid "Cannot predict now"
|
||||||
msgstr ""
|
msgstr "Não consigo prever agora"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:68
|
#: redbot/cogs/general/general.py:68
|
||||||
msgid "Concentrate and ask again"
|
msgid "Concentrate and ask again"
|
||||||
msgstr ""
|
msgstr "Concentre-se e pergunte de novo"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:69
|
#: redbot/cogs/general/general.py:69
|
||||||
msgid "Don't count on it"
|
msgid "Don't count on it"
|
||||||
msgstr ""
|
msgstr "Não conte com isso"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:70
|
#: redbot/cogs/general/general.py:70
|
||||||
msgid "My reply is no"
|
msgid "My reply is no"
|
||||||
msgstr ""
|
msgstr "Minha resposta é não"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:71
|
#: redbot/cogs/general/general.py:71
|
||||||
msgid "My sources say no"
|
msgid "My sources say no"
|
||||||
msgstr ""
|
msgstr "Minhas fontes dizem que não"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:72
|
#: redbot/cogs/general/general.py:72
|
||||||
msgid "Outlook not so good"
|
msgid "Outlook not so good"
|
||||||
msgstr ""
|
msgstr "A previsão não é muito boa"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:73
|
#: redbot/cogs/general/general.py:73
|
||||||
msgid "Very doubtful"
|
msgid "Very doubtful"
|
||||||
msgstr ""
|
msgstr "Muito duvidoso"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:88
|
#: redbot/cogs/general/general.py:88
|
||||||
#, docstring
|
#, docstring
|
||||||
@@ -107,11 +107,15 @@ msgid "Choose between multiple options.\n\n"
|
|||||||
" Options are separated by spaces.\n\n"
|
" Options are separated by spaces.\n\n"
|
||||||
" To denote options which include whitespace, you should enclose the options in double quotes.\n"
|
" To denote options which include whitespace, you should enclose the options in double quotes.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr "Escolha entre múltiplas opções.\n\n"
|
||||||
|
" Deve haver pelo menos 2 opções para escolher.\n"
|
||||||
|
" As opções são separadas por espaços.\n\n"
|
||||||
|
" Para denotar opções que incluem espaços em branco, você deve colocar as opções entre aspas duplas.\n"
|
||||||
|
" "
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:97
|
#: redbot/cogs/general/general.py:97
|
||||||
msgid "Not enough options to pick from."
|
msgid "Not enough options to pick from."
|
||||||
msgstr ""
|
msgstr "Opções insuficientes para escolher."
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:103
|
#: redbot/cogs/general/general.py:103
|
||||||
#, docstring
|
#, docstring
|
||||||
@@ -119,39 +123,45 @@ msgid "Roll a random number.\n\n"
|
|||||||
" The result will be between 1 and `<number>`.\n\n"
|
" The result will be between 1 and `<number>`.\n\n"
|
||||||
" `<number>` defaults to 100.\n"
|
" `<number>` defaults to 100.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr "Role um número aleatório.\n\n"
|
||||||
|
" O resultado será entre 1 e `<number>`.\n\n"
|
||||||
|
" `<number>` o padrão é 100.\n"
|
||||||
|
" "
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:118
|
#: redbot/cogs/general/general.py:118
|
||||||
msgid "{author.mention} Maybe higher than 1? ;P"
|
msgid "{author.mention} Maybe higher than 1? ;P"
|
||||||
msgstr ""
|
msgstr "{author.mention} Talvez maior que 1? ;P"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:121
|
#: redbot/cogs/general/general.py:121
|
||||||
msgid "{author.mention} Max allowed number is {maxamount}."
|
msgid "{author.mention} Max allowed number is {maxamount}."
|
||||||
msgstr ""
|
msgstr "{author.mention} O número máximo permitido é {maxamount}."
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:128
|
#: redbot/cogs/general/general.py:128
|
||||||
#, docstring
|
#, docstring
|
||||||
msgid "Flip a coin... or a user.\n\n"
|
msgid "Flip a coin... or a user.\n\n"
|
||||||
" Defaults to a coin.\n"
|
" Defaults to a coin.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr "Jogue uma moeda... ou um usuário.\n\n"
|
||||||
|
" O padrão é uma moeda.\n"
|
||||||
|
" "
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:136
|
#: redbot/cogs/general/general.py:136
|
||||||
msgid "Nice try. You think this is funny?\n"
|
msgid "Nice try. You think this is funny?\n"
|
||||||
" How about *this* instead:\n\n"
|
" How about *this* instead:\n\n"
|
||||||
msgstr ""
|
msgstr "Boa tentativa. Você pensa que isso é engraçado?\n"
|
||||||
|
" Que tal *isso* em vez disso:\n\n"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:147
|
#: redbot/cogs/general/general.py:147
|
||||||
msgid "*flips a coin and... "
|
msgid "*flips a coin and... "
|
||||||
msgstr ""
|
msgstr "*vira uma moeda e... "
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:147
|
#: redbot/cogs/general/general.py:147
|
||||||
msgid "HEADS!*"
|
msgid "HEADS!*"
|
||||||
msgstr ""
|
msgstr "CARA!*"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:147
|
#: redbot/cogs/general/general.py:147
|
||||||
msgid "TAILS!*"
|
msgid "TAILS!*"
|
||||||
msgstr ""
|
msgstr "COROA!*"
|
||||||
|
|
||||||
#: redbot/cogs/general/general.py:151
|
#: redbot/cogs/general/general.py:151
|
||||||
#, docstring
|
#, docstring
|
||||||
|
|||||||
Generated
+1
-1
@@ -572,7 +572,7 @@ msgstr ""
|
|||||||
#: redbot/cogs/mod/settings.py:85 redbot/cogs/mod/settings.py:93
|
#: redbot/cogs/mod/settings.py:85 redbot/cogs/mod/settings.py:93
|
||||||
#: redbot/cogs/mod/settings.py:96 redbot/cogs/mod/settings.py:108
|
#: redbot/cogs/mod/settings.py:96 redbot/cogs/mod/settings.py:108
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr "Sim"
|
||||||
|
|
||||||
#: redbot/cogs/mod/settings.py:31 redbot/cogs/mod/settings.py:57
|
#: redbot/cogs/mod/settings.py:31 redbot/cogs/mod/settings.py:57
|
||||||
#: redbot/cogs/mod/settings.py:62 redbot/cogs/mod/settings.py:67
|
#: redbot/cogs/mod/settings.py:62 redbot/cogs/mod/settings.py:67
|
||||||
|
|||||||
Generated
+1
-1
@@ -532,7 +532,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: redbot/cogs/mutes/mutes.py:1794
|
#: redbot/cogs/mutes/mutes.py:1794
|
||||||
msgid "this server"
|
msgid "this server"
|
||||||
msgstr ""
|
msgstr "este servidor"
|
||||||
|
|
||||||
#: redbot/cogs/mutes/voicemutes.py:42
|
#: redbot/cogs/mutes/voicemutes.py:42
|
||||||
msgid "That user is not in a voice channel."
|
msgid "That user is not in a voice channel."
|
||||||
|
|||||||
Generated
+1
-1
@@ -171,7 +171,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: redbot/cogs/trivia/trivia.py:44
|
#: redbot/cogs/trivia/trivia.py:44
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr "Sim"
|
||||||
|
|
||||||
#: redbot/cogs/trivia/trivia.py:46
|
#: redbot/cogs/trivia/trivia.py:46
|
||||||
msgid "No"
|
msgid "No"
|
||||||
|
|||||||
+41
-1
@@ -3,13 +3,15 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Optional
|
from typing import Any, Coroutine, Optional, TypeVar
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord import __version__ as discord_version
|
from discord import __version__ as discord_version
|
||||||
|
|
||||||
from redbot.core.utils._internal_utils import cli_level_to_log_level
|
from redbot.core.utils._internal_utils import cli_level_to_log_level
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
# This needs to be an int enum to be used
|
# This needs to be an int enum to be used
|
||||||
# with sys.exit
|
# with sys.exit
|
||||||
@@ -244,6 +246,14 @@ def parse_cli_flags(args):
|
|||||||
dest="logging_level",
|
dest="logging_level",
|
||||||
help="Increase the verbosity of the logs, each usage of this flag increases the verbosity level by 1.",
|
help="Increase the verbosity of the logs, each usage of this flag increases the verbosity level by 1.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-verbose",
|
||||||
|
"--no-debug",
|
||||||
|
action="store_const",
|
||||||
|
const=0,
|
||||||
|
dest="logging_level",
|
||||||
|
help="Set the verbosity level to 0.",
|
||||||
|
)
|
||||||
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
|
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mentionable",
|
"--mentionable",
|
||||||
@@ -360,3 +370,33 @@ def parse_cli_flags(args):
|
|||||||
args.logging_level = cli_level_to_log_level(args.logging_level)
|
args.logging_level = cli_level_to_log_level(args.logging_level)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def asyncio_run(coro: Coroutine[Any, Any, _T]) -> _T:
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
with asyncio.Runner(loop_factory=new_event_loop) as runner:
|
||||||
|
return runner.run(coro)
|
||||||
|
|
||||||
|
if sys.implementation.name == "cpython":
|
||||||
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def new_event_loop() -> asyncio.AbstractEventLoop:
|
||||||
|
if sys.implementation.name == "cpython":
|
||||||
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return uvloop.new_event_loop()
|
||||||
|
|
||||||
|
return asyncio.new_event_loop()
|
||||||
|
|||||||
@@ -0,0 +1,866 @@
|
|||||||
|
# TODO list:
|
||||||
|
# - design ergonomic APIs instead of whatever you want to call what we have now
|
||||||
|
# - try to be consistent about requiring Installable vs cog name
|
||||||
|
# between cog install and other functionality
|
||||||
|
# - use immutable objects more
|
||||||
|
# - change Installable's equality to include its commit
|
||||||
|
# (note: we currently heavily rely on this *not* being the case)
|
||||||
|
# - add asyncio.Lock appropriately for things that Downloader does
|
||||||
|
# - avoid doing some of the work on RepoManager initialization to speedup bot startup
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
FrozenSet,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, version_info as red_version_info
|
||||||
|
from redbot.core._cog_manager import CogManager
|
||||||
|
from redbot.core.data_manager import cog_data_path
|
||||||
|
|
||||||
|
from . import errors
|
||||||
|
from .log import log
|
||||||
|
from .installable import InstallableType, Installable, InstalledModule
|
||||||
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
|
_SCHEMA_VERSION = 1
|
||||||
|
_config: Config
|
||||||
|
_bot_ref: Optional[Red]
|
||||||
|
_cog_mgr: CogManager
|
||||||
|
_repo_manager: RepoManager
|
||||||
|
|
||||||
|
LIB_PATH: Path
|
||||||
|
SHAREDLIB_PATH: Path
|
||||||
|
_SHAREDLIB_INIT: Path
|
||||||
|
|
||||||
|
|
||||||
|
async def _init(bot: Red) -> None:
|
||||||
|
global _bot_ref
|
||||||
|
_bot_ref = bot
|
||||||
|
|
||||||
|
await _init_without_bot(_bot_ref._cog_mgr)
|
||||||
|
|
||||||
|
|
||||||
|
async def _init_without_bot(cog_manager: CogManager) -> None:
|
||||||
|
global _cog_mgr
|
||||||
|
_cog_mgr = cog_manager
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
global _config
|
||||||
|
_config = Config.get_conf(None, 998240343, cog_name="Downloader", force_registration=True)
|
||||||
|
_config.register_global(schema_version=0, installed_cogs={}, installed_libraries={})
|
||||||
|
await _migrate_config()
|
||||||
|
|
||||||
|
global LIB_PATH, SHAREDLIB_PATH, _SHAREDLIB_INIT
|
||||||
|
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
|
||||||
|
SHAREDLIB_PATH = LIB_PATH / "cog_shared"
|
||||||
|
_SHAREDLIB_INIT = SHAREDLIB_PATH / "__init__.py"
|
||||||
|
_create_lib_folder()
|
||||||
|
|
||||||
|
global _repo_manager
|
||||||
|
_repo_manager = RepoManager()
|
||||||
|
await _repo_manager.initialize()
|
||||||
|
|
||||||
|
stop = time.perf_counter()
|
||||||
|
|
||||||
|
log.debug("Finished initialization in %.2fs", stop - start)
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_config() -> None:
|
||||||
|
schema_version = await _config.schema_version()
|
||||||
|
|
||||||
|
if schema_version == _SCHEMA_VERSION:
|
||||||
|
return
|
||||||
|
|
||||||
|
if schema_version == 0:
|
||||||
|
await _schema_0_to_1()
|
||||||
|
schema_version += 1
|
||||||
|
await _config.schema_version.set(schema_version)
|
||||||
|
|
||||||
|
|
||||||
|
async def _schema_0_to_1():
|
||||||
|
"""
|
||||||
|
This contains migration to allow saving state
|
||||||
|
of both installed cogs and shared libraries.
|
||||||
|
"""
|
||||||
|
old_conf = await _config.get_raw("installed", default=[])
|
||||||
|
if not old_conf:
|
||||||
|
return
|
||||||
|
async with _config.installed_cogs() as new_cog_conf:
|
||||||
|
for cog_json in old_conf:
|
||||||
|
repo_name = cog_json["repo_name"]
|
||||||
|
module_name = cog_json["cog_name"]
|
||||||
|
if repo_name not in new_cog_conf:
|
||||||
|
new_cog_conf[repo_name] = {}
|
||||||
|
new_cog_conf[repo_name][module_name] = {
|
||||||
|
"repo_name": repo_name,
|
||||||
|
"module_name": module_name,
|
||||||
|
"commit": "",
|
||||||
|
"pinned": False,
|
||||||
|
}
|
||||||
|
await _config.clear_raw("installed")
|
||||||
|
# no reliable way to get installed libraries (i.a. missing repo name)
|
||||||
|
# but it only helps `[p]cog update` run faster so it's not an issue
|
||||||
|
|
||||||
|
|
||||||
|
def _create_lib_folder(*, remove_first: bool = False) -> None:
|
||||||
|
if remove_first:
|
||||||
|
shutil.rmtree(str(LIB_PATH))
|
||||||
|
SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not _SHAREDLIB_INIT.exists():
|
||||||
|
with _SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_cogs() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed cogs.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed cogs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
installed = await _config.installed_cogs()
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return tuple(
|
||||||
|
InstalledModule.from_json(cog_json, _repo_manager)
|
||||||
|
for repo_json in installed.values()
|
||||||
|
for cog_json in repo_json.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_libraries() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed shared libraries.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed shared libraries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
installed = await _config.installed_libraries()
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return tuple(
|
||||||
|
InstalledModule.from_json(lib_json, _repo_manager)
|
||||||
|
for repo_json in installed.values()
|
||||||
|
for lib_json in repo_json.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_modules() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed cogs and shared libraries.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed cogs and shared libraries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return await installed_cogs() + await installed_libraries()
|
||||||
|
|
||||||
|
|
||||||
|
async def _save_to_installed(modules: Iterable[InstalledModule]) -> None:
|
||||||
|
"""Mark modules as installed or updates their json in Config.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
modules : `list` of `InstalledModule`
|
||||||
|
The modules to check off.
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with _config.all() as global_data:
|
||||||
|
installed_cogs = global_data["installed_cogs"]
|
||||||
|
installed_libraries = global_data["installed_libraries"]
|
||||||
|
for module in modules:
|
||||||
|
if module.type is InstallableType.COG:
|
||||||
|
installed = installed_cogs
|
||||||
|
elif module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
installed = installed_libraries
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
module_json = module.to_json()
|
||||||
|
repo_json = installed.setdefault(module.repo_name, {})
|
||||||
|
repo_json[module.name] = module_json
|
||||||
|
|
||||||
|
|
||||||
|
async def _remove_from_installed(modules: Iterable[InstalledModule]) -> None:
|
||||||
|
"""Remove modules from the saved list
|
||||||
|
of installed modules (corresponding to type of module).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
modules : `list` of `InstalledModule`
|
||||||
|
The modules to remove.
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with _config.all() as global_data:
|
||||||
|
installed_cogs = global_data["installed_cogs"]
|
||||||
|
installed_libraries = global_data["installed_libraries"]
|
||||||
|
for module in modules:
|
||||||
|
if module.type is InstallableType.COG:
|
||||||
|
installed = installed_cogs
|
||||||
|
elif module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
installed = installed_libraries
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
installed[module._json_repo_name].pop(module.name)
|
||||||
|
|
||||||
|
|
||||||
|
async def _shared_lib_load_check(cog_name: str) -> Optional[Repo]:
|
||||||
|
_is_installed, cog = await is_installed(cog_name)
|
||||||
|
if _is_installed and cog.repo is not None and cog.repo.available_libraries:
|
||||||
|
return cog.repo
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def is_installed(
|
||||||
|
cog_name: str,
|
||||||
|
) -> Union[Tuple[Literal[True], InstalledModule], Tuple[Literal[False], None]]:
|
||||||
|
"""Check to see if a cog has been installed through Downloader.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cog_name : str
|
||||||
|
The name of the cog to check for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of (`bool`, `InstalledModule`)
|
||||||
|
:code:`(True, InstalledModule)` if the cog is installed, else
|
||||||
|
:code:`(False, None)`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for installed_cog in await installed_cogs():
|
||||||
|
if installed_cog.name == cog_name:
|
||||||
|
return True, installed_cog
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
async def _available_updates(
|
||||||
|
cogs: Iterable[InstalledModule],
|
||||||
|
) -> Tuple[Tuple[Installable, ...], Tuple[Installable, ...]]:
|
||||||
|
"""
|
||||||
|
Get cogs and libraries which can be updated.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `InstalledModule`
|
||||||
|
List of cogs, which should be checked against the updates.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of cogs and libraries which can be updated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
||||||
|
_installed_libraries = await installed_libraries()
|
||||||
|
|
||||||
|
modules: Set[InstalledModule] = set()
|
||||||
|
cogs_to_update: Set[Installable] = set()
|
||||||
|
libraries_to_update: Set[Installable] = set()
|
||||||
|
# split libraries and cogs into 2 categories:
|
||||||
|
# 1. `cogs_to_update`, `libraries_to_update` - module needs update, skip diffs
|
||||||
|
# 2. `modules` - module MAY need update, check diffs
|
||||||
|
for repo in repos:
|
||||||
|
for lib in repo.available_libraries:
|
||||||
|
try:
|
||||||
|
index = _installed_libraries.index(lib)
|
||||||
|
except ValueError:
|
||||||
|
libraries_to_update.add(lib)
|
||||||
|
else:
|
||||||
|
modules.add(_installed_libraries[index])
|
||||||
|
for cog in cogs:
|
||||||
|
if cog.repo is None:
|
||||||
|
# cog had its repo removed, can't check for updates
|
||||||
|
continue
|
||||||
|
if cog.commit:
|
||||||
|
modules.add(cog)
|
||||||
|
continue
|
||||||
|
# marking cog for update if there's no commit data saved (back-compat, see GH-2571)
|
||||||
|
last_cog_occurrence = await cog.repo.get_last_module_occurrence(cog.name)
|
||||||
|
if last_cog_occurrence is not None and not last_cog_occurrence.disabled:
|
||||||
|
cogs_to_update.add(last_cog_occurrence)
|
||||||
|
|
||||||
|
# Reduces diff requests to a single dict with no repeats
|
||||||
|
hashes: Dict[Tuple[Repo, str], Set[InstalledModule]] = defaultdict(set)
|
||||||
|
for module in modules:
|
||||||
|
module.repo = cast(Repo, module.repo)
|
||||||
|
if module.repo.commit != module.commit:
|
||||||
|
try:
|
||||||
|
should_add = await module.repo.is_ancestor(module.commit, module.repo.commit)
|
||||||
|
except errors.UnknownRevision:
|
||||||
|
# marking module for update if the saved commit data is invalid
|
||||||
|
last_module_occurrence = await module.repo.get_last_module_occurrence(module.name)
|
||||||
|
if last_module_occurrence is not None and not last_module_occurrence.disabled:
|
||||||
|
if last_module_occurrence.type is InstallableType.COG:
|
||||||
|
cogs_to_update.add(last_module_occurrence)
|
||||||
|
elif last_module_occurrence.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
libraries_to_update.add(last_module_occurrence)
|
||||||
|
else:
|
||||||
|
if should_add:
|
||||||
|
hashes[(module.repo, module.commit)].add(module)
|
||||||
|
|
||||||
|
update_commits = []
|
||||||
|
for (repo, old_hash), modules_to_check in hashes.items():
|
||||||
|
modified = await repo.get_modified_modules(old_hash, repo.commit)
|
||||||
|
for module in modules_to_check:
|
||||||
|
try:
|
||||||
|
index = modified.index(module)
|
||||||
|
except ValueError:
|
||||||
|
# module wasn't modified - we just need to update its commit
|
||||||
|
module.commit = repo.commit
|
||||||
|
update_commits.append(module)
|
||||||
|
else:
|
||||||
|
modified_module = modified[index]
|
||||||
|
if modified_module.type is InstallableType.COG:
|
||||||
|
if not modified_module.disabled:
|
||||||
|
cogs_to_update.add(modified_module)
|
||||||
|
elif modified_module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
libraries_to_update.add(modified_module)
|
||||||
|
|
||||||
|
await _save_to_installed(update_commits)
|
||||||
|
|
||||||
|
return (tuple(cogs_to_update), tuple(libraries_to_update))
|
||||||
|
|
||||||
|
|
||||||
|
async def _install_cogs(
|
||||||
|
cogs: Iterable[Installable],
|
||||||
|
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
|
||||||
|
"""Installs a list of cogs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `Installable`
|
||||||
|
Cogs to install. ``repo`` property of those objects can't be `None`
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of installed and failed cogs.
|
||||||
|
"""
|
||||||
|
repos: Dict[str, Tuple[Repo, Dict[str, List[Installable]]]] = {}
|
||||||
|
for cog in cogs:
|
||||||
|
try:
|
||||||
|
repo_by_commit = repos[cog.repo_name]
|
||||||
|
except KeyError:
|
||||||
|
cog.repo = cast(Repo, cog.repo) # docstring specifies this already
|
||||||
|
repo_by_commit = repos[cog.repo_name] = (cog.repo, defaultdict(list))
|
||||||
|
cogs_by_commit = repo_by_commit[1]
|
||||||
|
cogs_by_commit[cog.commit].append(cog)
|
||||||
|
installed = []
|
||||||
|
failed = []
|
||||||
|
for repo, cogs_by_commit in repos.values():
|
||||||
|
exit_to_commit = repo.commit
|
||||||
|
for commit, cogs_to_install in cogs_by_commit.items():
|
||||||
|
await repo.checkout(commit)
|
||||||
|
for cog in cogs_to_install:
|
||||||
|
if await cog.copy_to(await _cog_mgr.install_path()):
|
||||||
|
installed.append(InstalledModule.from_installable(cog))
|
||||||
|
else:
|
||||||
|
failed.append(cog)
|
||||||
|
await repo.checkout(exit_to_commit)
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return (tuple(installed), tuple(failed))
|
||||||
|
|
||||||
|
|
||||||
|
async def _reinstall_libraries(
|
||||||
|
libraries: Iterable[Installable],
|
||||||
|
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
|
||||||
|
"""Installs a list of shared libraries, used when updating.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
libraries : `list` of `Installable`
|
||||||
|
Libraries to reinstall. ``repo`` property of those objects can't be `None`
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of installed and failed libraries.
|
||||||
|
"""
|
||||||
|
repos: Dict[str, Tuple[Repo, Dict[str, Set[Installable]]]] = {}
|
||||||
|
for lib in libraries:
|
||||||
|
try:
|
||||||
|
repo_by_commit = repos[lib.repo_name]
|
||||||
|
except KeyError:
|
||||||
|
lib.repo = cast(Repo, lib.repo) # docstring specifies this already
|
||||||
|
repo_by_commit = repos[lib.repo_name] = (lib.repo, defaultdict(set))
|
||||||
|
libs_by_commit = repo_by_commit[1]
|
||||||
|
libs_by_commit[lib.commit].add(lib)
|
||||||
|
|
||||||
|
all_installed: List[InstalledModule] = []
|
||||||
|
all_failed: List[Installable] = []
|
||||||
|
for repo, libs_by_commit in repos.values():
|
||||||
|
exit_to_commit = repo.commit
|
||||||
|
for commit, libs in libs_by_commit.items():
|
||||||
|
await repo.checkout(commit)
|
||||||
|
installed, failed = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH, libraries=libs
|
||||||
|
)
|
||||||
|
all_installed += installed
|
||||||
|
all_failed += failed
|
||||||
|
await repo.checkout(exit_to_commit)
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return (tuple(all_installed), tuple(all_failed))
|
||||||
|
|
||||||
|
|
||||||
|
async def _install_requirements(cogs: Iterable[Installable]) -> Tuple[str, ...]:
|
||||||
|
"""
|
||||||
|
Installs requirements for given cogs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `Installable`
|
||||||
|
Cogs whose requirements should be installed.
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
Tuple of failed requirements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Reduces requirements to a single list with no repeats
|
||||||
|
requirements = {requirement for cog in cogs for requirement in cog.requirements}
|
||||||
|
repos: List[Tuple[Repo, List[str]]] = [(repo, []) for repo in _repo_manager.repos]
|
||||||
|
|
||||||
|
# This for loop distributes the requirements across all repos
|
||||||
|
# which will allow us to concurrently install requirements
|
||||||
|
for i, req in enumerate(requirements):
|
||||||
|
repo_index = i % len(repos)
|
||||||
|
repos[repo_index][1].append(req)
|
||||||
|
|
||||||
|
has_reqs = list(filter(lambda item: len(item[1]) > 0, repos))
|
||||||
|
|
||||||
|
failed_reqs = []
|
||||||
|
for repo, reqs in has_reqs:
|
||||||
|
for req in reqs:
|
||||||
|
if not await repo.install_raw_requirements([req], LIB_PATH):
|
||||||
|
failed_reqs.append(req)
|
||||||
|
return tuple(failed_reqs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_cog(target: Path) -> None:
|
||||||
|
"""
|
||||||
|
Removes an (installed) cog.
|
||||||
|
:param target: Path pointing to an existing file or directory
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not target.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
if target.is_dir():
|
||||||
|
shutil.rmtree(str(target))
|
||||||
|
elif target.is_file():
|
||||||
|
os.remove(str(target))
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_cogs_to_check(
|
||||||
|
*,
|
||||||
|
repos: Optional[Iterable[Repo]] = None,
|
||||||
|
cogs: Optional[Iterable[InstalledModule]] = None,
|
||||||
|
update_repos: bool = True,
|
||||||
|
) -> Tuple[Set[InstalledModule], List[str]]:
|
||||||
|
failed: List[str] = []
|
||||||
|
if not (cogs or repos):
|
||||||
|
if update_repos:
|
||||||
|
__, failed = await _repo_manager.update_repos()
|
||||||
|
|
||||||
|
cogs_to_check = {
|
||||||
|
cog
|
||||||
|
for cog in await installed_cogs()
|
||||||
|
if cog.repo is not None and cog.repo.name not in failed
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# this is enough to be sure that `cogs` is not None (based on if above)
|
||||||
|
if not repos:
|
||||||
|
cogs = cast(Iterable[InstalledModule], cogs)
|
||||||
|
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
||||||
|
|
||||||
|
if update_repos:
|
||||||
|
__, failed = await _repo_manager.update_repos(repos)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
# remove failed repos
|
||||||
|
repos = {repo for repo in repos if repo.name not in failed}
|
||||||
|
|
||||||
|
if cogs:
|
||||||
|
cogs_to_check = {cog for cog in cogs if cog.repo is not None and cog.repo in repos}
|
||||||
|
else:
|
||||||
|
cogs_to_check = {
|
||||||
|
cog for cog in await installed_cogs() if cog.repo is not None and cog.repo in repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cogs_to_check, failed)
|
||||||
|
|
||||||
|
|
||||||
|
# functionality extracted from command implementations
|
||||||
|
# TODO: make them into nice APIs instead of what they are now...
|
||||||
|
|
||||||
|
|
||||||
|
async def pip_install(*deps: str) -> bool:
|
||||||
|
repo = Repo("", "", "", "", Path.cwd())
|
||||||
|
return await repo.install_raw_requirements(deps, LIB_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
async def reinstall_requirements() -> Tuple[Tuple[str, ...], Tuple[Installable, ...]]:
|
||||||
|
_create_lib_folder(remove_first=True)
|
||||||
|
_installed_cogs = await installed_cogs()
|
||||||
|
cogs = []
|
||||||
|
repos = set()
|
||||||
|
for cog in _installed_cogs:
|
||||||
|
if cog.repo is None:
|
||||||
|
continue
|
||||||
|
repos.add(cog.repo)
|
||||||
|
cogs.append(cog)
|
||||||
|
failed_reqs = await _install_requirements(cogs)
|
||||||
|
all_installed_libs: List[InstalledModule] = []
|
||||||
|
all_failed_libs: List[Installable] = []
|
||||||
|
for repo in repos:
|
||||||
|
installed_libs, failed_libs = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
|
||||||
|
)
|
||||||
|
all_installed_libs += installed_libs
|
||||||
|
all_failed_libs += failed_libs
|
||||||
|
|
||||||
|
return failed_reqs, tuple(all_failed_libs)
|
||||||
|
|
||||||
|
|
||||||
|
async def install_cogs(
|
||||||
|
repo: Repo, rev: Optional[str], cog_names: Iterable[str]
|
||||||
|
) -> CogInstallResult:
|
||||||
|
commit = None
|
||||||
|
|
||||||
|
if rev is not None:
|
||||||
|
# raises errors.AmbiguousRevision and errors.UnknownRevision
|
||||||
|
commit = await repo.get_full_sha1(rev)
|
||||||
|
|
||||||
|
cog_names = set(cog_names)
|
||||||
|
_installed_cogs = await installed_cogs()
|
||||||
|
|
||||||
|
cogs: List[Installable] = []
|
||||||
|
unavailable_cogs: List[str] = []
|
||||||
|
already_installed: List[Installable] = []
|
||||||
|
name_already_used: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
|
||||||
|
result_installed_cogs: Tuple[InstalledModule, ...] = ()
|
||||||
|
result_failed_cogs: Tuple[Installable, ...] = ()
|
||||||
|
result_failed_reqs: Tuple[str, ...] = ()
|
||||||
|
result_installed_libs: Tuple[InstalledModule, ...] = ()
|
||||||
|
result_failed_libs: Tuple[Installable, ...] = ()
|
||||||
|
|
||||||
|
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||||
|
for cog_name in cog_names:
|
||||||
|
cog: Optional[Installable] = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
|
if cog is None:
|
||||||
|
unavailable_cogs.append(cog_name)
|
||||||
|
elif cog in _installed_cogs:
|
||||||
|
already_installed.append(cog)
|
||||||
|
elif discord.utils.get(_installed_cogs, name=cog.name):
|
||||||
|
name_already_used.append(cog)
|
||||||
|
elif cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
cogs.append(cog)
|
||||||
|
|
||||||
|
if cogs:
|
||||||
|
result_failed_reqs = await _install_requirements(cogs)
|
||||||
|
if not result_failed_reqs:
|
||||||
|
result_installed_cogs, result_failed_cogs = await _install_cogs(cogs)
|
||||||
|
|
||||||
|
if cogs and not result_failed_reqs:
|
||||||
|
result_installed_libs, result_failed_libs = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
|
||||||
|
)
|
||||||
|
if rev is not None:
|
||||||
|
for cog in result_installed_cogs:
|
||||||
|
cog.pinned = True
|
||||||
|
await _save_to_installed(result_installed_cogs + result_installed_libs)
|
||||||
|
|
||||||
|
return CogInstallResult(
|
||||||
|
installed_cogs=result_installed_cogs,
|
||||||
|
installed_libs=result_installed_libs,
|
||||||
|
failed_cogs=result_failed_cogs,
|
||||||
|
failed_libs=result_failed_libs,
|
||||||
|
failed_reqs=result_failed_reqs,
|
||||||
|
unavailable_cogs=tuple(unavailable_cogs),
|
||||||
|
already_installed=tuple(already_installed),
|
||||||
|
name_already_used=tuple(name_already_used),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def uninstall_cogs(*cogs: InstalledModule) -> tuple[list[str], list[str]]:
|
||||||
|
uninstalled_cogs = []
|
||||||
|
failed_cogs = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
real_name = cog.name
|
||||||
|
|
||||||
|
poss_installed_path = (await _cog_mgr.install_path()) / real_name
|
||||||
|
if poss_installed_path.exists():
|
||||||
|
if _bot_ref is not None:
|
||||||
|
with contextlib.suppress(commands.ExtensionNotLoaded):
|
||||||
|
await _bot_ref.unload_extension(real_name)
|
||||||
|
await _bot_ref.remove_loaded_package(real_name)
|
||||||
|
await _delete_cog(poss_installed_path)
|
||||||
|
uninstalled_cogs.append(real_name)
|
||||||
|
else:
|
||||||
|
failed_cogs.append(real_name)
|
||||||
|
await _remove_from_installed(cogs)
|
||||||
|
|
||||||
|
return uninstalled_cogs, failed_cogs
|
||||||
|
|
||||||
|
|
||||||
|
async def check_cog_updates(
|
||||||
|
*,
|
||||||
|
repos: Optional[Iterable[Repo]] = None,
|
||||||
|
cogs: Optional[Iterable[InstalledModule]] = None,
|
||||||
|
update_repos: bool = True,
|
||||||
|
) -> CogUpdateCheckResult:
|
||||||
|
cogs_to_check, failed_repos = await _get_cogs_to_check(
|
||||||
|
repos=repos, cogs=cogs, update_repos=update_repos
|
||||||
|
)
|
||||||
|
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
|
||||||
|
|
||||||
|
updatable_cogs: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
for cog in outdated_cogs:
|
||||||
|
if cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
updatable_cogs.append(cog)
|
||||||
|
|
||||||
|
return CogUpdateCheckResult(
|
||||||
|
outdated_cogs=outdated_cogs,
|
||||||
|
outdated_libs=outdated_libs,
|
||||||
|
updatable_cogs=tuple(updatable_cogs),
|
||||||
|
failed_repos=tuple(failed_repos),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# update given cogs or all cogs
|
||||||
|
async def update_cogs(
|
||||||
|
*, cogs: Optional[List[InstalledModule]] = None, repos: Optional[List[Repo]] = None
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
if cogs is not None and repos is not None:
|
||||||
|
raise ValueError("You can specify cogs or repos argument, not both")
|
||||||
|
|
||||||
|
cogs_to_check, failed_repos = await _get_cogs_to_check(repos=repos, cogs=cogs)
|
||||||
|
return await _update_cogs(cogs_to_check, failed_repos=failed_repos)
|
||||||
|
|
||||||
|
|
||||||
|
# update given cogs or all cogs from the specified repo
|
||||||
|
# using the specified revision (or latest if not specified)
|
||||||
|
async def update_repo_cogs(
|
||||||
|
repo: Repo, cogs: Optional[List[InstalledModule]] = None, *, rev: Optional[str] = None
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
try:
|
||||||
|
await repo.update()
|
||||||
|
except errors.UpdateError:
|
||||||
|
return await _update_cogs(set(), failed_repos=(repo.name,))
|
||||||
|
|
||||||
|
# TODO: should this be set to `repo.branch` when `rev` is None?
|
||||||
|
commit = None
|
||||||
|
if rev is not None:
|
||||||
|
# raises errors.AmbiguousRevision and errors.UnknownRevision
|
||||||
|
commit = await repo.get_full_sha1(rev)
|
||||||
|
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||||
|
cogs_to_check, __ = await _get_cogs_to_check(repos=[repo], cogs=cogs, update_repos=False)
|
||||||
|
return await _update_cogs(cogs_to_check, failed_repos=())
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_cogs(
|
||||||
|
cogs_to_check: Set[InstalledModule], *, failed_repos: Sequence[str]
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
||||||
|
cogs_to_check -= pinned_cogs
|
||||||
|
|
||||||
|
outdated_cogs: Tuple[Installable, ...] = ()
|
||||||
|
outdated_libs: Tuple[Installable, ...] = ()
|
||||||
|
updatable_cogs: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
|
||||||
|
updated_cogs: Tuple[InstalledModule, ...] = ()
|
||||||
|
failed_cogs: Tuple[Installable, ...] = ()
|
||||||
|
failed_reqs: Tuple[str, ...] = ()
|
||||||
|
updated_libs: Tuple[InstalledModule, ...] = ()
|
||||||
|
failed_libs: Tuple[Installable, ...] = ()
|
||||||
|
|
||||||
|
if cogs_to_check:
|
||||||
|
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
|
||||||
|
|
||||||
|
for cog in outdated_cogs:
|
||||||
|
if cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
updatable_cogs.append(cog)
|
||||||
|
|
||||||
|
if updatable_cogs or outdated_libs:
|
||||||
|
failed_reqs = await _install_requirements(updatable_cogs)
|
||||||
|
if not failed_reqs:
|
||||||
|
updated_cogs, failed_cogs = await _install_cogs(updatable_cogs)
|
||||||
|
updated_libs, failed_libs = await _reinstall_libraries(outdated_libs)
|
||||||
|
await _save_to_installed(updated_cogs + updated_libs)
|
||||||
|
|
||||||
|
return CogUpdateResult(
|
||||||
|
checked_cogs=frozenset(cogs_to_check),
|
||||||
|
pinned_cogs=frozenset(pinned_cogs),
|
||||||
|
updated_cogs=updated_cogs,
|
||||||
|
updated_libs=updated_libs,
|
||||||
|
failed_cogs=failed_cogs,
|
||||||
|
failed_libs=failed_libs,
|
||||||
|
failed_reqs=failed_reqs,
|
||||||
|
outdated_cogs=outdated_cogs,
|
||||||
|
outdated_libs=outdated_libs,
|
||||||
|
updatable_cogs=tuple(updatable_cogs),
|
||||||
|
failed_repos=tuple(failed_repos),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def pin_cogs(
|
||||||
|
*cogs: InstalledModule,
|
||||||
|
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
|
||||||
|
already_pinned = []
|
||||||
|
pinned = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
if cog.pinned:
|
||||||
|
already_pinned.append(cog)
|
||||||
|
continue
|
||||||
|
cog.pinned = True
|
||||||
|
pinned.append(cog)
|
||||||
|
if pinned:
|
||||||
|
await _save_to_installed(pinned)
|
||||||
|
|
||||||
|
return tuple(pinned), tuple(already_pinned)
|
||||||
|
|
||||||
|
|
||||||
|
async def unpin_cogs(
|
||||||
|
*cogs: InstalledModule,
|
||||||
|
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
|
||||||
|
not_pinned = []
|
||||||
|
unpinned = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
if not cog.pinned:
|
||||||
|
not_pinned.append(cog)
|
||||||
|
continue
|
||||||
|
cog.pinned = False
|
||||||
|
unpinned.append(cog)
|
||||||
|
if unpinned:
|
||||||
|
await _save_to_installed(unpinned)
|
||||||
|
|
||||||
|
return tuple(unpinned), tuple(not_pinned)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogInstallResult:
|
||||||
|
installed_cogs: Tuple[InstalledModule, ...]
|
||||||
|
installed_libs: Tuple[InstalledModule, ...]
|
||||||
|
failed_cogs: Tuple[Installable, ...]
|
||||||
|
failed_libs: Tuple[Installable, ...]
|
||||||
|
failed_reqs: Tuple[str, ...]
|
||||||
|
unavailable_cogs: Tuple[str, ...]
|
||||||
|
already_installed: Tuple[Installable, ...]
|
||||||
|
name_already_used: Tuple[Installable, ...]
|
||||||
|
incompatible_python_version: Tuple[Installable, ...]
|
||||||
|
incompatible_bot_version: Tuple[Installable, ...]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogUpdateCheckResult:
|
||||||
|
outdated_cogs: Tuple[Installable, ...]
|
||||||
|
outdated_libs: Tuple[Installable, ...]
|
||||||
|
updatable_cogs: Tuple[Installable, ...]
|
||||||
|
failed_repos: Tuple[str, ...]
|
||||||
|
incompatible_python_version: Tuple[Installable, ...]
|
||||||
|
incompatible_bot_version: Tuple[Installable, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updates_available(self) -> bool:
|
||||||
|
return bool(self.outdated_cogs or self.outdated_libs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updates_installable(self) -> bool:
|
||||||
|
return bool(self.updatable_cogs or self.outdated_libs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def incompatible_cogs(self) -> Tuple[Installable, ...]:
|
||||||
|
return self.incompatible_python_version + self.incompatible_bot_version
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogUpdateResult(CogUpdateCheckResult):
|
||||||
|
# checked_cogs contains old modules, before update
|
||||||
|
checked_cogs: FrozenSet[InstalledModule]
|
||||||
|
pinned_cogs: FrozenSet[InstalledModule]
|
||||||
|
updated_cogs: Tuple[InstalledModule, ...]
|
||||||
|
updated_libs: Tuple[InstalledModule, ...]
|
||||||
|
failed_cogs: Tuple[Installable, ...]
|
||||||
|
failed_libs: Tuple[Installable, ...]
|
||||||
|
failed_reqs: Tuple[str, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updated_modules(self) -> Tuple[InstalledModule, ...]:
|
||||||
|
return self.updated_cogs + self.updated_libs
|
||||||
|
|
||||||
|
|
||||||
|
class CogUnavailableError(Exception):
|
||||||
|
def __init__(self, repo_name: str, cog_name: str) -> None:
|
||||||
|
self.repo_name = repo_name
|
||||||
|
self.cog_name = cog_name
|
||||||
|
super().__init__(f"Couldn't find cog {cog_name!r} in {repo_name!r}")
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import shutil
|
import shutil
|
||||||
from enum import IntEnum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union, cast
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union, cast
|
||||||
|
|
||||||
@@ -16,8 +16,7 @@ if TYPE_CHECKING:
|
|||||||
from .repo_manager import RepoManager, Repo
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
|
|
||||||
class InstallableType(IntEnum):
|
class InstallableType(Enum):
|
||||||
# using IntEnum, because hot-reload breaks its identity
|
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
COG = 1
|
COG = 1
|
||||||
SHARED_LIBRARY = 2
|
SHARED_LIBRARY = 2
|
||||||
@@ -139,7 +138,7 @@ class Installable(RepoJSONMixin):
|
|||||||
super()._read_info_file()
|
super()._read_info_file()
|
||||||
|
|
||||||
update_mixin(self, INSTALLABLE_SCHEMA)
|
update_mixin(self, INSTALLABLE_SCHEMA)
|
||||||
if self.type == InstallableType.SHARED_LIBRARY:
|
if self.type is InstallableType.SHARED_LIBRARY:
|
||||||
self.hidden = True
|
self.hidden = True
|
||||||
|
|
||||||
|
|
||||||
@@ -163,7 +162,7 @@ class InstalledModule(Installable):
|
|||||||
json_repo_name: str = "",
|
json_repo_name: str = "",
|
||||||
):
|
):
|
||||||
super().__init__(location=location, repo=repo, commit=commit)
|
super().__init__(location=location, repo=repo, commit=commit)
|
||||||
self.pinned: bool = pinned if self.type == InstallableType.COG else False
|
self.pinned: bool = pinned if self.type is InstallableType.COG else False
|
||||||
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
|
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
|
||||||
self._json_repo_name = json_repo_name
|
self._json_repo_name = json_repo_name
|
||||||
|
|
||||||
@@ -173,7 +172,7 @@ class InstalledModule(Installable):
|
|||||||
"module_name": self.name,
|
"module_name": self.name,
|
||||||
"commit": self.commit,
|
"commit": self.commit,
|
||||||
}
|
}
|
||||||
if self.type == InstallableType.COG:
|
if self.type is InstallableType.COG:
|
||||||
module_json["pinned"] = self.pinned
|
module_json["pinned"] = self.pinned
|
||||||
return module_json
|
return module_json
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger("red.core.downloader")
|
||||||
File diff suppressed because it is too large
Load Diff
+23
-11
@@ -176,14 +176,15 @@ def init_events(bot, cli_flags):
|
|||||||
if bot.intents.members: # Lets avoid 0 Unique Users
|
if bot.intents.members: # Lets avoid 0 Unique Users
|
||||||
table_counts.add_row("Unique Users", str(users))
|
table_counts.add_row("Unique Users", str(users))
|
||||||
|
|
||||||
outdated_red_message = ""
|
fetch_version_task = asyncio.create_task(fetch_latest_red_version_info())
|
||||||
rich_outdated_message = ""
|
log.info("Fetching information about latest Red version...")
|
||||||
pypi_version, py_version_req = await fetch_latest_red_version_info()
|
try:
|
||||||
outdated = pypi_version and pypi_version > red_version_info
|
await asyncio.wait_for(asyncio.shield(fetch_version_task), timeout=5)
|
||||||
if outdated:
|
except asyncio.TimeoutError:
|
||||||
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
|
log.info("Version information will continue to be fetched in the background...")
|
||||||
pypi_version, py_version_req
|
except Exception:
|
||||||
)
|
# these will be logged later
|
||||||
|
pass
|
||||||
|
|
||||||
rich_console = rich.get_console()
|
rich_console = rich.get_console()
|
||||||
rich_console.print(INTRO, style="red", markup=False, highlight=False)
|
rich_console.print(INTRO, style="red", markup=False, highlight=False)
|
||||||
@@ -209,11 +210,22 @@ def init_events(bot, cli_flags):
|
|||||||
rich_console.print(
|
rich_console.print(
|
||||||
f"Looking for a quick guide on setting up Red? Checkout {Text('https://start.discord.red', style='link https://start.discord.red}')}"
|
f"Looking for a quick guide on setting up Red? Checkout {Text('https://start.discord.red', style='link https://start.discord.red}')}"
|
||||||
)
|
)
|
||||||
if rich_outdated_message:
|
|
||||||
rich_console.print(rich_outdated_message)
|
|
||||||
|
|
||||||
bot._red_ready.set()
|
bot._red_ready.set()
|
||||||
if outdated_red_message:
|
|
||||||
|
try:
|
||||||
|
pypi_version, py_version_req = await fetch_version_task
|
||||||
|
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
||||||
|
log.error("Failed to fetch latest version information from PyPI.", exc_info=exc)
|
||||||
|
except (KeyError, ValueError) as exc:
|
||||||
|
log.error("Failed to parse version metadata received from PyPI.", exc_info=exc)
|
||||||
|
else:
|
||||||
|
outdated = pypi_version and pypi_version > red_version_info
|
||||||
|
if outdated:
|
||||||
|
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
|
||||||
|
pypi_version, py_version_req
|
||||||
|
)
|
||||||
|
rich_console.print(rich_outdated_message)
|
||||||
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
|
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
|
|||||||
@@ -97,9 +97,6 @@ class RPC:
|
|||||||
self._runner,
|
self._runner,
|
||||||
host="127.0.0.1",
|
host="127.0.0.1",
|
||||||
port=port,
|
port=port,
|
||||||
shutdown_timeout=120
|
|
||||||
# Give the RPC server 2 minutes to finish up, else slap it!
|
|
||||||
# Seems like a reasonable time. See Red#6391
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
+53
-8
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
@@ -37,7 +38,18 @@ import discord
|
|||||||
from discord.ext import commands as dpy_commands
|
from discord.ext import commands as dpy_commands
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
|
|
||||||
from . import Config, _i18n, i18n, app_commands, commands, errors, _drivers, modlog, bank
|
from . import (
|
||||||
|
Config,
|
||||||
|
_i18n,
|
||||||
|
i18n,
|
||||||
|
app_commands,
|
||||||
|
commands,
|
||||||
|
errors,
|
||||||
|
_drivers,
|
||||||
|
modlog,
|
||||||
|
bank,
|
||||||
|
_downloader,
|
||||||
|
)
|
||||||
from ._cli import ExitCodes
|
from ._cli import ExitCodes
|
||||||
from ._cog_manager import CogManager, CogManagerUI
|
from ._cog_manager import CogManager, CogManagerUI
|
||||||
from .core_commands import Core
|
from .core_commands import Core
|
||||||
@@ -69,6 +81,8 @@ CUSTOM_GROUPS = "CUSTOM_GROUPS"
|
|||||||
COMMAND_SCOPE = "COMMAND"
|
COMMAND_SCOPE = "COMMAND"
|
||||||
SHARED_API_TOKENS = "SHARED_API_TOKENS"
|
SHARED_API_TOKENS = "SHARED_API_TOKENS"
|
||||||
|
|
||||||
|
_DEFAULT_DESCRIPTION = "Red V3"
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
|
|
||||||
__all__ = ("Red",)
|
__all__ = ("Red",)
|
||||||
@@ -101,7 +115,9 @@ class Red(
|
|||||||
): # pylint: disable=no-member # barely spurious warning caused by shadowing
|
): # pylint: disable=no-member # barely spurious warning caused by shadowing
|
||||||
"""Our subclass of discord.ext.commands.AutoShardedBot"""
|
"""Our subclass of discord.ext.commands.AutoShardedBot"""
|
||||||
|
|
||||||
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
|
def __init__(
|
||||||
|
self, *args: Any, cli_flags: argparse.Namespace, bot_dir: Path = Path.cwd(), **kwargs: Any
|
||||||
|
) -> None:
|
||||||
self._shutdown_mode = ExitCodes.CRITICAL
|
self._shutdown_mode = ExitCodes.CRITICAL
|
||||||
self._cli_flags = cli_flags
|
self._cli_flags = cli_flags
|
||||||
self._config = Config.get_core_conf(force_registration=False)
|
self._config = Config.get_core_conf(force_registration=False)
|
||||||
@@ -132,7 +148,7 @@ class Red(
|
|||||||
help__tagline="",
|
help__tagline="",
|
||||||
help__use_tick=False,
|
help__use_tick=False,
|
||||||
help__react_timeout=30,
|
help__react_timeout=30,
|
||||||
description="Red V3",
|
description=_DEFAULT_DESCRIPTION,
|
||||||
invite_public=False,
|
invite_public=False,
|
||||||
invite_perm=0,
|
invite_perm=0,
|
||||||
invite_commands_scope=False,
|
invite_commands_scope=False,
|
||||||
@@ -141,6 +157,7 @@ class Red(
|
|||||||
invoke_error_msg=None,
|
invoke_error_msg=None,
|
||||||
extra_owner_destinations=[],
|
extra_owner_destinations=[],
|
||||||
owner_opt_out_list=[],
|
owner_opt_out_list=[],
|
||||||
|
last_system_info__python_prefix=None,
|
||||||
last_system_info__python_version=[3, 7],
|
last_system_info__python_version=[3, 7],
|
||||||
last_system_info__machine=None,
|
last_system_info__machine=None,
|
||||||
last_system_info__system=None,
|
last_system_info__system=None,
|
||||||
@@ -238,7 +255,13 @@ class Red(
|
|||||||
self._main_dir = bot_dir
|
self._main_dir = bot_dir
|
||||||
self._cog_mgr = CogManager()
|
self._cog_mgr = CogManager()
|
||||||
self._use_team_features = cli_flags.use_team_features
|
self._use_team_features = cli_flags.use_team_features
|
||||||
super().__init__(*args, help_command=None, tree_cls=RedTree, **kwargs)
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
description=kwargs.pop("description", _DEFAULT_DESCRIPTION),
|
||||||
|
help_command=None,
|
||||||
|
tree_cls=RedTree,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
# Do not manually use the help formatter attribute here, see `send_help_for`,
|
||||||
# for a documented API. The internals of this object are still subject to change.
|
# for a documented API. The internals of this object are still subject to change.
|
||||||
self._help_formatter = commands.help.RedHelpFormatter()
|
self._help_formatter = commands.help.RedHelpFormatter()
|
||||||
@@ -1198,14 +1221,35 @@ class Red(
|
|||||||
|
|
||||||
last_system_info = await self._config.last_system_info()
|
last_system_info = await self._config.last_system_info()
|
||||||
|
|
||||||
|
last_python_prefix = last_system_info["python_prefix"]
|
||||||
|
if last_python_prefix is None:
|
||||||
|
await self._config.last_system_info.python_prefix.set(sys.prefix)
|
||||||
|
elif last_python_prefix != sys.prefix:
|
||||||
|
await self._config.last_system_info.python_prefix.set(sys.prefix)
|
||||||
|
try:
|
||||||
|
same_install = os.path.samefile(last_python_prefix, sys.prefix)
|
||||||
|
except OSError:
|
||||||
|
same_install = False
|
||||||
|
if not same_install:
|
||||||
|
if sys.prefix != sys.base_prefix:
|
||||||
|
install_info = "in the currently used virtual environment"
|
||||||
|
else:
|
||||||
|
install_info = "with the currently used Python installation"
|
||||||
|
log.warning(
|
||||||
|
"Red seems to have been started with a different Python installation"
|
||||||
|
" and/or virtual environment. This is not, in itself, an issue but is often"
|
||||||
|
" done unintentionally and may explain some, otherwise unexpected, behavior."
|
||||||
|
" This message will not be shown again, if you start Red %s again.",
|
||||||
|
install_info,
|
||||||
|
)
|
||||||
|
|
||||||
ver_info = list(sys.version_info[:2])
|
ver_info = list(sys.version_info[:2])
|
||||||
python_version_changed = False
|
python_version_changed = False
|
||||||
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
|
|
||||||
if ver_info != last_system_info["python_version"]:
|
if ver_info != last_system_info["python_version"]:
|
||||||
await self._config.last_system_info.python_version.set(ver_info)
|
await self._config.last_system_info.python_version.set(ver_info)
|
||||||
if any(LIB_PATH.iterdir()):
|
if any(_downloader.LIB_PATH.iterdir()):
|
||||||
shutil.rmtree(str(LIB_PATH))
|
shutil.rmtree(str(_downloader.LIB_PATH))
|
||||||
LIB_PATH.mkdir()
|
_downloader.LIB_PATH.mkdir()
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
send_to_owners_with_prefix_replaced(
|
send_to_owners_with_prefix_replaced(
|
||||||
self,
|
self,
|
||||||
@@ -2502,6 +2546,7 @@ class Red(
|
|||||||
n_remaining = len(messages) - idx
|
n_remaining = len(messages) - idx
|
||||||
files_perm = (
|
files_perm = (
|
||||||
isinstance(channel, discord.abc.User)
|
isinstance(channel, discord.abc.User)
|
||||||
|
or channel.guild is None
|
||||||
or channel.permissions_for(channel.guild.me).attach_files
|
or channel.permissions_for(channel.guild.me).attach_files
|
||||||
)
|
)
|
||||||
options = ("more", "file") if files_perm else ("more",)
|
options = ("more", "file") if files_perm else ("more",)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from . import (
|
|||||||
i18n,
|
i18n,
|
||||||
bank,
|
bank,
|
||||||
modlog,
|
modlog,
|
||||||
|
_downloader,
|
||||||
)
|
)
|
||||||
from ._diagnoser import IssueDiagnoser
|
from ._diagnoser import IssueDiagnoser
|
||||||
from .utils import AsyncIter, can_user_send_messages_in
|
from .utils import AsyncIter, can_user_send_messages_in
|
||||||
@@ -215,12 +216,8 @@ class CoreLogic:
|
|||||||
else:
|
else:
|
||||||
await bot.add_loaded_package(name)
|
await bot.add_loaded_package(name)
|
||||||
loaded_packages.append(name)
|
loaded_packages.append(name)
|
||||||
# remove in Red 3.4
|
|
||||||
downloader = bot.get_cog("Downloader")
|
|
||||||
if downloader is None:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
maybe_repo = await downloader._shared_lib_load_check(name)
|
maybe_repo = await _downloader._shared_lib_load_check(name)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
"Shared library check failed,"
|
"Shared library check failed,"
|
||||||
@@ -424,7 +421,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
owner = app_info.owner
|
owner = app_info.owner
|
||||||
custom_info = await self.bot._config.custom_info()
|
custom_info = await self.bot._config.custom_info()
|
||||||
|
|
||||||
pypi_version, py_version_req = await fetch_latest_red_version_info()
|
try:
|
||||||
|
pypi_version, __ = await fetch_latest_red_version_info()
|
||||||
|
except (aiohttp.ClientError, TimeoutError) as exc:
|
||||||
|
log.error("Failed to fetch latest version information from PyPI.", exc_info=exc)
|
||||||
|
pypi_version = None
|
||||||
outdated = pypi_version and pypi_version > red_version_info
|
outdated = pypi_version and pypi_version > red_version_info
|
||||||
|
|
||||||
if embed_links:
|
if embed_links:
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class DevOutput:
|
|||||||
output.append(self.formatted_exc)
|
output.append(self.formatted_exc)
|
||||||
elif self.always_include_result or self.result is not None:
|
elif self.always_include_result or self.result is not None:
|
||||||
try:
|
try:
|
||||||
result = str(self.result)
|
result = str(self.result) if isinstance(self.result, str) else repr(self.result)
|
||||||
# ensure that the result can be encoded (GH-6485)
|
# ensure that the result can be encoded (GH-6485)
|
||||||
result.encode("utf-8")
|
result.encode("utf-8")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+1444
-897
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+607
-607
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
Generated
+604
-604
File diff suppressed because it is too large
Load Diff
@@ -236,7 +236,7 @@ async def create_backup(dest: Path = Path.home()) -> Optional[Path]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Avoiding circular imports
|
# Avoiding circular imports
|
||||||
from ...cogs.downloader.repo_manager import RepoManager
|
from redbot.core._downloader.repo_manager import RepoManager
|
||||||
|
|
||||||
repo_mgr = RepoManager()
|
repo_mgr = RepoManager()
|
||||||
await repo_mgr.initialize()
|
await repo_mgr.initialize()
|
||||||
@@ -326,14 +326,25 @@ def expected_version(current: str, expected: str) -> bool:
|
|||||||
return Requirement(f"x{expected}").specifier.contains(current, prereleases=True)
|
return Requirement(f"x{expected}").specifier.contains(current, prereleases=True)
|
||||||
|
|
||||||
|
|
||||||
async def fetch_latest_red_version_info() -> Tuple[Optional[VersionInfo], Optional[str]]:
|
async def fetch_latest_red_version_info() -> Tuple[VersionInfo, Optional[str]]:
|
||||||
try:
|
"""
|
||||||
|
Fetch information about latest Red release on PyPI.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
aiohttp.ClientError
|
||||||
|
An error occurred during request to PyPI.
|
||||||
|
TimeoutError
|
||||||
|
The request to PyPI timed out.
|
||||||
|
ValueError
|
||||||
|
An invalid version string was returned in PyPI metadata.
|
||||||
|
KeyError
|
||||||
|
The PyPI metadata is missing some of the required information.
|
||||||
|
"""
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get("https://pypi.org/pypi/Red-DiscordBot/json") as r:
|
async with session.get("https://pypi.org/pypi/Red-DiscordBot/json") as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
||||||
return None, None
|
|
||||||
else:
|
|
||||||
release = VersionInfo.from_str(data["info"]["version"])
|
release = VersionInfo.from_str(data["info"]["version"])
|
||||||
required_python = data["info"]["requires_python"]
|
required_python = data["info"]["requires_python"]
|
||||||
|
|
||||||
|
|||||||
+33
-17
@@ -282,7 +282,20 @@ class RedRichHandler(RichHandler):
|
|||||||
self.console.print(traceback)
|
self.console.print(traceback)
|
||||||
|
|
||||||
|
|
||||||
def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespace) -> None:
|
_FILE_FORMATTER = logging.Formatter(
|
||||||
|
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging(
|
||||||
|
level: int,
|
||||||
|
*,
|
||||||
|
location: Optional[pathlib.Path] = None,
|
||||||
|
rich_logging: Optional[bool] = None,
|
||||||
|
rich_tracebacks: bool = False,
|
||||||
|
rich_traceback_extra_lines: int = 0,
|
||||||
|
rich_traceback_show_locals: bool = False,
|
||||||
|
) -> None:
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
root_logger.setLevel(level)
|
root_logger.setLevel(level)
|
||||||
# DEBUG logging for discord.py is a bit too ridiculous :)
|
# DEBUG logging for discord.py is a bit too ridiculous :)
|
||||||
@@ -312,24 +325,21 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
|
|||||||
|
|
||||||
enable_rich_logging = False
|
enable_rich_logging = False
|
||||||
|
|
||||||
if isatty(0) and cli_flags.rich_logging is None:
|
if isatty(0) and rich_logging is None:
|
||||||
# Check if the bot thinks it has a active terminal.
|
# Check if the bot thinks it has a active terminal.
|
||||||
enable_rich_logging = True
|
enable_rich_logging = True
|
||||||
elif cli_flags.rich_logging is True:
|
elif rich_logging is True:
|
||||||
enable_rich_logging = True
|
enable_rich_logging = True
|
||||||
|
|
||||||
file_formatter = logging.Formatter(
|
|
||||||
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
|
|
||||||
)
|
|
||||||
if enable_rich_logging is True:
|
if enable_rich_logging is True:
|
||||||
rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{")
|
rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{")
|
||||||
|
|
||||||
stdout_handler = RedRichHandler(
|
stdout_handler = RedRichHandler(
|
||||||
rich_tracebacks=cli_flags.rich_tracebacks,
|
rich_tracebacks=rich_tracebacks,
|
||||||
show_path=False,
|
show_path=False,
|
||||||
highlighter=NullHighlighter(),
|
highlighter=NullHighlighter(),
|
||||||
tracebacks_extra_lines=cli_flags.rich_traceback_extra_lines,
|
tracebacks_extra_lines=rich_traceback_extra_lines,
|
||||||
tracebacks_show_locals=cli_flags.rich_traceback_show_locals,
|
tracebacks_show_locals=rich_traceback_show_locals,
|
||||||
tracebacks_theme=(
|
tracebacks_theme=(
|
||||||
PygmentsSyntaxTheme(FixedMonokaiStyle)
|
PygmentsSyntaxTheme(FixedMonokaiStyle)
|
||||||
if rich_console.color_system == "truecolor"
|
if rich_console.color_system == "truecolor"
|
||||||
@@ -339,11 +349,22 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
|
|||||||
stdout_handler.setFormatter(rich_formatter)
|
stdout_handler.setFormatter(rich_formatter)
|
||||||
else:
|
else:
|
||||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||||
stdout_handler.setFormatter(file_formatter)
|
stdout_handler.setFormatter(_FILE_FORMATTER)
|
||||||
|
|
||||||
root_logger.addHandler(stdout_handler)
|
root_logger.addHandler(stdout_handler)
|
||||||
logging.captureWarnings(True)
|
logging.captureWarnings(True)
|
||||||
|
|
||||||
|
if location is not None:
|
||||||
|
init_file_logging(location)
|
||||||
|
|
||||||
|
if not enable_rich_logging and rich_tracebacks:
|
||||||
|
log.warning(
|
||||||
|
"Rich tracebacks were requested but they will not be enabled"
|
||||||
|
" as Rich logging is not active."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_file_logging(location: pathlib.Path) -> None:
|
||||||
if not location.exists():
|
if not location.exists():
|
||||||
location.mkdir(parents=True, exist_ok=True)
|
location.mkdir(parents=True, exist_ok=True)
|
||||||
# Rotate latest logs to previous logs
|
# Rotate latest logs to previous logs
|
||||||
@@ -379,12 +400,7 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
root_logger = logging.getLogger()
|
||||||
for fhandler in (latest_fhandler, all_fhandler):
|
for fhandler in (latest_fhandler, all_fhandler):
|
||||||
fhandler.setFormatter(file_formatter)
|
fhandler.setFormatter(_FILE_FORMATTER)
|
||||||
root_logger.addHandler(fhandler)
|
root_logger.addHandler(fhandler)
|
||||||
|
|
||||||
if not enable_rich_logging and cli_flags.rich_tracebacks:
|
|
||||||
log.warning(
|
|
||||||
"Rich tracebacks were requested but they will not be enabled"
|
|
||||||
" as Rich logging is not active."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -172,11 +172,9 @@ def red(config_fr):
|
|||||||
|
|
||||||
cli_flags = parse_cli_flags(["ignore_me"])
|
cli_flags = parse_cli_flags(["ignore_me"])
|
||||||
|
|
||||||
description = "Red v3 - Alpha"
|
|
||||||
|
|
||||||
Config.get_core_conf = lambda *args, **kwargs: config_fr
|
Config.get_core_conf = lambda *args, **kwargs: config_fr
|
||||||
|
|
||||||
red = Red(cli_flags=cli_flags, description=description, dm_help=None, owner_ids=set())
|
red = Red(cli_flags=cli_flags)
|
||||||
|
|
||||||
yield red
|
yield red
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import shutil
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import RepoManager, Repo, ProcessFormatter
|
from redbot.core._downloader.repo_manager import RepoManager, Repo, ProcessFormatter
|
||||||
from redbot.cogs.downloader.installable import Installable, InstalledModule
|
from redbot.core._downloader.installable import Installable, InstalledModule
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GIT_VERSION",
|
"GIT_VERSION",
|
||||||
|
|||||||
+43
-45
@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, Union
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
import redbot.logging
|
||||||
from redbot.core._cli import confirm
|
from redbot.core._cli import confirm
|
||||||
from redbot.core.utils._internal_utils import (
|
from redbot.core.utils._internal_utils import (
|
||||||
safe_delete,
|
safe_delete,
|
||||||
@@ -22,7 +23,7 @@ from redbot.core.utils._internal_utils import (
|
|||||||
)
|
)
|
||||||
from redbot.core import config, data_manager
|
from redbot.core import config, data_manager
|
||||||
from redbot.core._config import migrate
|
from redbot.core._config import migrate
|
||||||
from redbot.core._cli import ExitCodes
|
from redbot.core._cli import ExitCodes, asyncio_run
|
||||||
from redbot.core.data_manager import appdir, config_dir, config_file
|
from redbot.core.data_manager import appdir, config_dir, config_file
|
||||||
from redbot.core._drivers import (
|
from redbot.core._drivers import (
|
||||||
BackendType,
|
BackendType,
|
||||||
@@ -60,9 +61,9 @@ def save_config(name, data, remove=False):
|
|||||||
def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive: bool) -> str:
|
def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive: bool) -> str:
|
||||||
if data_path is not None:
|
if data_path is not None:
|
||||||
return str(data_path.resolve())
|
return str(data_path.resolve())
|
||||||
data_path = Path(appdir.user_data_dir) / "data" / instance_name
|
default_data_path = Path(appdir.user_data_dir) / "data" / instance_name
|
||||||
if not interactive:
|
if not interactive:
|
||||||
return str(data_path.resolve())
|
return str(default_data_path.resolve())
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"We've attempted to figure out a sane default data location which is printed below."
|
"We've attempted to figure out a sane default data location which is printed below."
|
||||||
@@ -70,12 +71,15 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
|
|||||||
" otherwise input your desired data location."
|
" otherwise input your desired data location."
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
print("Default: {}".format(data_path))
|
print(f"Default: {default_data_path}")
|
||||||
|
|
||||||
|
while True:
|
||||||
data_path_input = input("> ")
|
data_path_input = input("> ")
|
||||||
|
|
||||||
if data_path_input != "":
|
if data_path_input != "":
|
||||||
data_path = Path(data_path_input)
|
data_path = Path(data_path_input)
|
||||||
|
else:
|
||||||
|
data_path = default_data_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exists = data_path.exists()
|
exists = data_path.exists()
|
||||||
@@ -84,7 +88,7 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
|
|||||||
"We were unable to check your chosen directory."
|
"We were unable to check your chosen directory."
|
||||||
" Provided path may contain an invalid character."
|
" Provided path may contain an invalid character."
|
||||||
)
|
)
|
||||||
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
continue
|
||||||
|
|
||||||
if not exists:
|
if not exists:
|
||||||
try:
|
try:
|
||||||
@@ -97,10 +101,10 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
|
|||||||
)
|
)
|
||||||
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
||||||
|
|
||||||
print("You have chosen {} to be your data directory.".format(data_path))
|
print(f"You have chosen {str(data_path)!r} to be your data directory.")
|
||||||
if not click.confirm("Please confirm", default=True):
|
if click.confirm("Please confirm", default=True):
|
||||||
print("Please start the process over.")
|
break
|
||||||
sys.exit(ExitCodes.CRITICAL)
|
|
||||||
return str(data_path.resolve())
|
return str(data_path.resolve())
|
||||||
|
|
||||||
|
|
||||||
@@ -131,8 +135,7 @@ def get_storage_type(backend: Optional[str], *, interactive: bool):
|
|||||||
return storage_dict[storage]
|
return storage_dict[storage]
|
||||||
|
|
||||||
|
|
||||||
def get_name(name: str) -> str:
|
INSTANCE_NAME_RE = re.compile(
|
||||||
INSTANCE_NAME_RE = re.compile(
|
|
||||||
r"""
|
r"""
|
||||||
[a-z0-9] # starts with letter or digit
|
[a-z0-9] # starts with letter or digit
|
||||||
(?:
|
(?:
|
||||||
@@ -142,7 +145,10 @@ def get_name(name: str) -> str:
|
|||||||
)? # optional to allow strings of length 1
|
)? # optional to allow strings of length 1
|
||||||
""",
|
""",
|
||||||
re.VERBOSE | re.IGNORECASE,
|
re.VERBOSE | re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(name: str = "", *, confirm_overwrite: bool = False) -> str:
|
||||||
if name:
|
if name:
|
||||||
if INSTANCE_NAME_RE.fullmatch(name) is None:
|
if INSTANCE_NAME_RE.fullmatch(name) is None:
|
||||||
print(
|
print(
|
||||||
@@ -151,9 +157,17 @@ def get_name(name: str) -> str:
|
|||||||
" and non-consecutive underscores (_) and periods (.)."
|
" and non-consecutive underscores (_) and periods (.)."
|
||||||
)
|
)
|
||||||
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
||||||
|
if name in instance_data and not confirm_overwrite:
|
||||||
|
print(
|
||||||
|
"An instance with this name already exists.\n"
|
||||||
|
"If you want to remove the existing instance and replace it with this one,"
|
||||||
|
" run this command with --overwrite-existing-instance flag."
|
||||||
|
)
|
||||||
|
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
while len(name) == 0:
|
name = ""
|
||||||
|
while not name:
|
||||||
print(
|
print(
|
||||||
"Please enter a name for your instance,"
|
"Please enter a name for your instance,"
|
||||||
" it will be used to run your bot from here on out.\n"
|
" it will be used to run your bot from here on out.\n"
|
||||||
@@ -176,6 +190,16 @@ def get_name(name: str) -> str:
|
|||||||
default=False,
|
default=False,
|
||||||
):
|
):
|
||||||
name = ""
|
name = ""
|
||||||
|
elif name in instance_data and not confirm_overwrite:
|
||||||
|
print(
|
||||||
|
"WARNING: An instance already exists with this name."
|
||||||
|
" Continuing will overwrite the existing instance config."
|
||||||
|
)
|
||||||
|
if not click.confirm(
|
||||||
|
"Are you absolutely certain you want to continue with this instance name?",
|
||||||
|
default=False,
|
||||||
|
):
|
||||||
|
name = ""
|
||||||
|
|
||||||
print() # new line for aesthetics
|
print() # new line for aesthetics
|
||||||
return name
|
return name
|
||||||
@@ -205,7 +229,7 @@ def basic_setup(
|
|||||||
"Hello! Before we begin, we need to gather some initial information"
|
"Hello! Before we begin, we need to gather some initial information"
|
||||||
" for the new instance."
|
" for the new instance."
|
||||||
)
|
)
|
||||||
name = get_name(name)
|
name = get_name(name, confirm_overwrite=overwrite_existing_instance)
|
||||||
|
|
||||||
default_data_dir = get_data_dir(
|
default_data_dir = get_data_dir(
|
||||||
instance_name=name, data_path=data_path, interactive=interactive
|
instance_name=name, data_path=data_path, interactive=interactive
|
||||||
@@ -220,26 +244,6 @@ def basic_setup(
|
|||||||
driver_cls = get_driver_class(storage_type)
|
driver_cls = get_driver_class(storage_type)
|
||||||
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
|
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
|
||||||
|
|
||||||
if name in instance_data:
|
|
||||||
if overwrite_existing_instance:
|
|
||||||
pass
|
|
||||||
elif interactive:
|
|
||||||
print(
|
|
||||||
"WARNING: An instance already exists with this name. "
|
|
||||||
"Continuing will overwrite the existing instance config."
|
|
||||||
)
|
|
||||||
if not click.confirm(
|
|
||||||
"Are you absolutely certain you want to continue?", default=False
|
|
||||||
):
|
|
||||||
print("Not continuing")
|
|
||||||
sys.exit(ExitCodes.SHUTDOWN)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"An instance with this name already exists.\n"
|
|
||||||
"If you want to remove the existing instance and replace it with this one,"
|
|
||||||
" run this command with --overwrite-existing-instance flag."
|
|
||||||
)
|
|
||||||
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
|
||||||
save_config(name, default_dirs)
|
save_config(name, default_dirs)
|
||||||
|
|
||||||
if interactive:
|
if interactive:
|
||||||
@@ -436,15 +440,9 @@ def cli(
|
|||||||
overwrite_existing_instance: bool,
|
overwrite_existing_instance: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new instance."""
|
"""Create a new instance."""
|
||||||
|
|
||||||
level = cli_level_to_log_level(debug)
|
level = cli_level_to_log_level(debug)
|
||||||
base_logger = logging.getLogger("red")
|
redbot.logging.init_logging(level)
|
||||||
base_logger.setLevel(level)
|
|
||||||
formatter = logging.Formatter(
|
|
||||||
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
|
|
||||||
)
|
|
||||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
stdout_handler.setFormatter(formatter)
|
|
||||||
base_logger.addHandler(stdout_handler)
|
|
||||||
|
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
basic_setup(
|
basic_setup(
|
||||||
@@ -514,7 +512,7 @@ def delete(
|
|||||||
remove_datapath: Optional[bool],
|
remove_datapath: Optional[bool],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Removes an instance."""
|
"""Removes an instance."""
|
||||||
asyncio.run(
|
asyncio_run(
|
||||||
remove_instance(
|
remove_instance(
|
||||||
instance, interactive, delete_data, _create_backup, drop_db, remove_datapath
|
instance, interactive, delete_data, _create_backup, drop_db, remove_datapath
|
||||||
)
|
)
|
||||||
@@ -536,7 +534,7 @@ def convert(instance: str, backend: str) -> None:
|
|||||||
if current_backend == BackendType.MONGOV1:
|
if current_backend == BackendType.MONGOV1:
|
||||||
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
|
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
|
||||||
else:
|
else:
|
||||||
new_storage_details = asyncio.run(do_migration(current_backend, target))
|
new_storage_details = asyncio_run(do_migration(current_backend, target))
|
||||||
|
|
||||||
if new_storage_details is not None:
|
if new_storage_details is not None:
|
||||||
default_dirs["STORAGE_TYPE"] = target.value
|
default_dirs["STORAGE_TYPE"] = target.value
|
||||||
@@ -560,7 +558,7 @@ def convert(instance: str, backend: str) -> None:
|
|||||||
)
|
)
|
||||||
def backup(instance: str, destination_folder: Path) -> None:
|
def backup(instance: str, destination_folder: Path) -> None:
|
||||||
"""Backup instance's data."""
|
"""Backup instance's data."""
|
||||||
asyncio.run(create_backup(instance, destination_folder))
|
asyncio_run(create_backup(instance, destination_folder))
|
||||||
|
|
||||||
|
|
||||||
def run_cli():
|
def run_cli():
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ typing_extensions
|
|||||||
yarl
|
yarl
|
||||||
distro; sys_platform == "linux"
|
distro; sys_platform == "linux"
|
||||||
# https://github.com/MagicStack/uvloop/issues/702
|
# https://github.com/MagicStack/uvloop/issues/702
|
||||||
uvloop>=0.21.0,!=0.22.0,!=0.22.1; sys_platform != "win32" and platform_python_implementation == "CPython"
|
uvloop; sys_platform != "win32" and platform_python_implementation == "CPython"
|
||||||
|
|
||||||
# Used by discord.py[speedup]. See Pull request #6587 for more info.
|
# Used by discord.py[speedup]. See Pull request #6587 for more info.
|
||||||
Brotli
|
Brotli
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ importlib-metadata==8.5.0; python_version != "3.10" and python_version != "3.11"
|
|||||||
# via markdown
|
# via markdown
|
||||||
pytz==2026.1.post1; python_version == "3.8"
|
pytz==2026.1.post1; python_version == "3.8"
|
||||||
# via babel
|
# via babel
|
||||||
uvloop==0.21.0; (sys_platform != "win32" and platform_python_implementation == "CPython") and sys_platform != "win32"
|
uvloop==0.22.1; (sys_platform != "win32" and platform_python_implementation == "CPython") and sys_platform != "win32"
|
||||||
# via -r base.in
|
# via -r base.in
|
||||||
zipp==3.20.2; python_version != "3.10" and python_version != "3.11"
|
zipp==3.20.2; python_version != "3.10" and python_version != "3.11"
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
-c base.txt
|
-c base.txt
|
||||||
|
|
||||||
Sphinx
|
Sphinx
|
||||||
|
sphinx-markdown-builder>=0.6.10
|
||||||
sphinx-prompt
|
sphinx-prompt
|
||||||
sphinx_rtd_theme>1
|
sphinx_rtd_theme>1
|
||||||
sphinxcontrib-trio
|
sphinxcontrib-trio
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ sphinx==7.1.2
|
|||||||
# sphinx-rtd-theme
|
# sphinx-rtd-theme
|
||||||
# sphinxcontrib-jquery
|
# sphinxcontrib-jquery
|
||||||
# sphinxcontrib-trio
|
# sphinxcontrib-trio
|
||||||
|
sphinx-markdown-builder==0.6.10
|
||||||
|
# via -r extra-doc.in
|
||||||
sphinx-prompt==1.7.0
|
sphinx-prompt==1.7.0
|
||||||
# via -r extra-doc.in
|
# via -r extra-doc.in
|
||||||
sphinx-rtd-theme==3.1.0
|
sphinx-rtd-theme==3.1.0
|
||||||
@@ -54,6 +56,8 @@ sphinxcontrib-serializinghtml==1.1.5
|
|||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-trio==1.2.0
|
sphinxcontrib-trio==1.2.0
|
||||||
# via -r extra-doc.in
|
# via -r extra-doc.in
|
||||||
|
tabulate==0.9.0
|
||||||
|
# via sphinx-markdown-builder
|
||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
# via requests
|
# via requests
|
||||||
zipp==3.20.2
|
zipp==3.20.2
|
||||||
|
|||||||
+2
-4
@@ -3,16 +3,14 @@ import os
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot import _update_event_loop_policy
|
|
||||||
from redbot.core import _drivers, data_manager
|
from redbot.core import _drivers, data_manager
|
||||||
|
from redbot.core._cli import new_event_loop
|
||||||
_update_event_loop_policy()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def event_loop(request):
|
def event_loop(request):
|
||||||
"""Create an instance of the default event loop for entire session."""
|
"""Create an instance of the default event loop for entire session."""
|
||||||
loop = asyncio.new_event_loop()
|
loop = new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
yield loop
|
yield loop
|
||||||
asyncio.set_event_loop(None)
|
asyncio.set_event_loop(None)
|
||||||
|
|||||||
+9
-9
@@ -9,9 +9,9 @@ from pytest_mock import MockFixture
|
|||||||
|
|
||||||
from redbot.pytest.downloader import *
|
from redbot.pytest.downloader import *
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import Installable
|
from redbot.core._downloader.repo_manager import Installable
|
||||||
from redbot.cogs.downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
|
from redbot.core._downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
|
||||||
from redbot.cogs.downloader.errors import (
|
from redbot.core._downloader.errors import (
|
||||||
AmbiguousRevision,
|
AmbiguousRevision,
|
||||||
ExistingGitRepo,
|
ExistingGitRepo,
|
||||||
GitException,
|
GitException,
|
||||||
@@ -322,9 +322,9 @@ async def test_update(mocker, repo):
|
|||||||
|
|
||||||
|
|
||||||
async def test_add_repo(monkeypatch, repo_manager):
|
async def test_add_repo(monkeypatch, repo_manager):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
|
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
|
||||||
)
|
)
|
||||||
|
|
||||||
squid = await repo_manager.add_repo(
|
squid = await repo_manager.add_repo(
|
||||||
@@ -335,9 +335,9 @@ async def test_add_repo(monkeypatch, repo_manager):
|
|||||||
|
|
||||||
|
|
||||||
async def test_lib_install_requirements(monkeypatch, library_installable, repo, tmpdir):
|
async def test_lib_install_requirements(monkeypatch, library_installable, repo, tmpdir):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.available_libraries", (library_installable,)
|
"redbot.core._downloader.repo_manager.Repo.available_libraries", (library_installable,)
|
||||||
)
|
)
|
||||||
|
|
||||||
lib_path = Path(str(tmpdir)) / "cog_data_path" / "lib"
|
lib_path = Path(str(tmpdir)) / "cog_data_path" / "lib"
|
||||||
@@ -353,9 +353,9 @@ async def test_lib_install_requirements(monkeypatch, library_installable, repo,
|
|||||||
|
|
||||||
|
|
||||||
async def test_remove_repo(monkeypatch, repo_manager):
|
async def test_remove_repo(monkeypatch, repo_manager):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
|
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
|
||||||
)
|
)
|
||||||
|
|
||||||
await repo_manager.add_repo(
|
await repo_manager.add_repo(
|
||||||
@@ -3,7 +3,7 @@ import subprocess as sp
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import ProcessFormatter, Repo
|
from redbot.core._downloader.repo_manager import ProcessFormatter, Repo
|
||||||
from redbot.pytest.downloader import (
|
from redbot.pytest.downloader import (
|
||||||
GIT_VERSION,
|
GIT_VERSION,
|
||||||
cloned_git_repo,
|
cloned_git_repo,
|
||||||
+3
-3
@@ -4,14 +4,14 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.pytest.downloader import *
|
from redbot.pytest.downloader import *
|
||||||
from redbot.cogs.downloader.installable import Installable, InstallableType
|
from redbot.core._downloader.installable import Installable, InstallableType
|
||||||
from redbot.core import VersionInfo
|
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 is InstallableType.COG
|
||||||
elif k in ("min_bot_version", "max_bot_version"):
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
assert getattr(installable, k) == VersionInfo.from_str(v)
|
assert getattr(installable, k) == VersionInfo.from_str(v)
|
||||||
else:
|
else:
|
||||||
@@ -21,7 +21,7 @@ def test_process_info_file(installable):
|
|||||||
def test_process_lib_info_file(library_installable):
|
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 is InstallableType.SHARED_LIBRARY
|
||||||
elif k in ("min_bot_version", "max_bot_version"):
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
assert getattr(library_installable, k) == VersionInfo.from_str(v)
|
assert getattr(library_installable, k) == VersionInfo.from_str(v)
|
||||||
elif k == "hidden":
|
elif k == "hidden":
|
||||||
@@ -965,12 +965,7 @@ def cli_contributors(version: str, *, show_not_merged: bool = False) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def get_contributors(version: str, *, show_not_merged: bool = False) -> None:
|
def get_contributors(version: str, *, show_not_merged: bool = False) -> None:
|
||||||
print(
|
print(*_get_contributors(version, show_not_merged=show_not_merged))
|
||||||
", ".join(
|
|
||||||
f":ghuser:`{username}`"
|
|
||||||
for username in _get_contributors(version, show_not_merged=show_not_merged)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_contributors(version: str, *, show_not_merged: bool = False) -> List[str]:
|
def _get_contributors(version: str, *, show_not_merged: bool = False) -> List[str]:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ extras = doc
|
|||||||
commands =
|
commands =
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/html" -W --keep-going -bhtml
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/html" -W --keep-going -bhtml
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/doctest" -W --keep-going -bdoctest
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/doctest" -W --keep-going -bdoctest
|
||||||
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/markdown" docs/changelog.rst -W --keep-going -bmarkdown
|
||||||
|
|
||||||
[testenv:style]
|
[testenv:style]
|
||||||
description = Stylecheck the code with black to see if anything needs changes.
|
description = Stylecheck the code with black to see if anything needs changes.
|
||||||
|
|||||||
Reference in New Issue
Block a user