mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-22 06:54:51 -04:00
Compare commits
50 Commits
3.5.23
...
V3/develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b8ecba975 | |||
| f6259ed0a2 | |||
| 661e0f7f18 | |||
| 4473f9bb69 | |||
| 0df902ae12 | |||
| 1ad5723c0c | |||
| 58085ab828 | |||
| 69faa20837 | |||
| 6da17a5a57 | |||
| eb77211eb8 | |||
| d5b816d360 | |||
| 71fe7137a0 | |||
| fce2e30451 | |||
| 5659bad5d8 | |||
| de11f52c41 | |||
| 32ed5e0b6b | |||
| 74a989a0b9 | |||
| 7e2a74b276 | |||
| 899f24ceca | |||
| 13f45f69ac | |||
| a234fc1e02 | |||
| 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 |
+8
-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/*
|
||||||
@@ -208,10 +204,18 @@
|
|||||||
- docs/cog_guides/core.rst
|
- docs/cog_guides/core.rst
|
||||||
"Category: Core - Command-line Interfaces":
|
"Category: Core - Command-line Interfaces":
|
||||||
- redbot/__main__.py
|
- redbot/__main__.py
|
||||||
|
- redbot/_update/**/*
|
||||||
- redbot/logging.py
|
- redbot/logging.py
|
||||||
- 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 +267,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":
|
||||||
|
|||||||
@@ -25,3 +25,9 @@ jobs:
|
|||||||
uses: Jackenmen/label-doconly-changes@v1
|
uses: Jackenmen/label-doconly-changes@v1
|
||||||
env:
|
env:
|
||||||
LDC_LABELS: Docs-only
|
LDC_LABELS: Docs-only
|
||||||
|
LDC_HOOK_UNCONDITIONAL__ALLOWED_FILES: |-
|
||||||
|
# default list of unconditionally allowed files
|
||||||
|
*.rst
|
||||||
|
*.md
|
||||||
|
# Red-specific includes
|
||||||
|
docs/.resources/*
|
||||||
|
|||||||
@@ -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
@@ -43,6 +43,8 @@ gettext:
|
|||||||
upload_translations:
|
upload_translations:
|
||||||
crowdin upload sources
|
crowdin upload sources
|
||||||
download_translations:
|
download_translations:
|
||||||
|
# get rid of any files that are no longer on Crowdin
|
||||||
|
find -name '*.po' -delete
|
||||||
crowdin download
|
crowdin download
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|||||||
@@ -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.
|
||||||
+127
-19
@@ -4,32 +4,140 @@
|
|||||||
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 system as long as it is a supported per our `end-user-guarantees`.
|
||||||
|
The system it's restored to can be different from the system that was backed up.
|
||||||
|
|
||||||
Backup steps are to be done in order and carefully to avoid any issues.
|
.. note::
|
||||||
|
|
||||||
|
Some 3rd-party cogs may not support all systems that Core Red supports and such cogs may therefore not work,
|
||||||
|
if restored to an unsupported system. This does not affect cogs that do not impose additional restrictions.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
:depth: 2
|
||||||
|
|
||||||
|
Creating backups
|
||||||
|
****************
|
||||||
|
|
||||||
|
Windows
|
||||||
|
-------
|
||||||
|
|
||||||
|
To make a backup, perform the following steps:
|
||||||
|
|
||||||
#. Take note of the installed cogs with ``[p]cogs``; and cog repositories with ``[p]load downloader``, then ``[p]repo list`` (``[p]`` is your bot's prefix).
|
|
||||||
#. Stop the bot, ideally with ``[p]shutdown``.
|
#. Stop the bot, ideally with ``[p]shutdown``.
|
||||||
#. Activate your venv, and run ``redbot-setup backup <instancename>``, replacing ``<instancename>`` with the name of your instance.
|
#. Activate your venv.
|
||||||
#. Copy your backup file to the new machine/location.
|
|
||||||
#. Extract the file to a location of your choice (remember the full path and make sure that the user you are going to install/run Red under can access this path).
|
|
||||||
#. :ref:`Install Red <install-guides>` as normal on the new machine/location.
|
|
||||||
#. Run ``redbot-setup`` in your venv to create a new instance, using the path you remembered above as your data path.
|
|
||||||
#. Start your new instance.
|
|
||||||
#. Re-add the cog repositories using the same names as before.
|
|
||||||
#. Do ``[p]cog update``.
|
|
||||||
#. Re-add any cogs that were not re-installed (you may have to uninstall them first as Downloader may think they are still installed).
|
|
||||||
|
|
||||||
.. note::
|
.. prompt:: batch
|
||||||
|
|
||||||
The config (data) from cogs has been saved, but not the code itself.
|
"%userprofile%\redenv\Scripts\activate.bat"
|
||||||
|
#. Backup your Red instance with the following command:
|
||||||
|
|
||||||
.. tip::
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
You can fix permissions (if needed) on your directory using:
|
redbot-setup backup <your instance name>
|
||||||
|
|
||||||
.. code-block:: bash
|
.. attention::
|
||||||
|
|
||||||
sudo chown -R <user>:<user> ~/.local
|
Replace ``<your instance name>`` with the name of the instance you want to backup.
|
||||||
|
#. The command will create a backup file for you and show you the path to it.
|
||||||
|
|
||||||
Replace ``<user>`` with your actual username.
|
.. tip::
|
||||||
|
|
||||||
|
If you want to backup your instance to a custom folder,
|
||||||
|
you can run the ``redbot-setup backup`` command as shown below,
|
||||||
|
replacing ``C:\path\to\backup\folder`` with the path to the folder that
|
||||||
|
you want to backup your instance to:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
|
redbot-setup backup <your instance name> C:\path\to\backup\folder
|
||||||
|
|
||||||
|
Linux & Mac
|
||||||
|
-----------
|
||||||
|
|
||||||
|
To make a backup, perform the following steps:
|
||||||
|
|
||||||
|
#. Stop the bot, ideally with ``[p]shutdown``.
|
||||||
|
#. Activate your venv.
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
source ~/redenv/bin/activate
|
||||||
|
#. Backup your Red instance with the following command:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
:prompts: (redenv) $
|
||||||
|
|
||||||
|
redbot-setup backup <your instance name>
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
Replace ``<your instance name>`` with the name of the instance you want to backup.
|
||||||
|
#. The command will create a backup file for you and show you the path to it.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
If you want to backup your instance to a custom folder,
|
||||||
|
you can run the ``redbot-setup backup`` command as shown below,
|
||||||
|
replacing ``/path/to/backup/folder`` with the path to the folder that
|
||||||
|
you want to backup your instance to:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
:prompts: (redenv) $
|
||||||
|
|
||||||
|
redbot-setup backup <your instance name> /path/to/backup/folder
|
||||||
|
|
||||||
|
Restoring backups
|
||||||
|
*****************
|
||||||
|
|
||||||
|
Windows
|
||||||
|
-------
|
||||||
|
|
||||||
|
To restore a backup, perform the following steps:
|
||||||
|
|
||||||
|
#. `Install Red <windows-install-guide>` on the new machine/location, skipping the ``redbot-setup`` step.
|
||||||
|
#. Activate your venv.
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
|
||||||
|
"%userprofile%\redenv\Scripts\activate.bat"
|
||||||
|
#. Restore your Red instance with the following command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
|
redbot-setup restore C:\path\to\backup\file.tar.gz
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
Replace ``C:\path\to\backup\file.tar.gz`` with the path to the backup file
|
||||||
|
that you want to restore from.
|
||||||
|
|
||||||
|
#. The command will guide you through the restore process.
|
||||||
|
|
||||||
|
Linux & Mac
|
||||||
|
-----------
|
||||||
|
|
||||||
|
To restore a backup, perform the following steps:
|
||||||
|
|
||||||
|
#. `Install Red <install-guides>` on the new machine/location, skipping the ``redbot-setup`` step.
|
||||||
|
#. Activate your venv.
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
source ~/redenv/bin/activate
|
||||||
|
#. Restore your Red instance with the following command:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
:prompts: (redenv) $
|
||||||
|
|
||||||
|
redbot-setup restore /path/to/backup/file.tar.gz
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
Replace ``/path/to/backup/file.tar.gz`` with the path to the backup file
|
||||||
|
that you want to restore from.
|
||||||
|
|
||||||
|
#. The command will guide you through the restore process.
|
||||||
|
|||||||
@@ -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 25, 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 25, 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=25>`__.
|
||||||
|
|
||||||
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 25, 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_compatible_java_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 25, 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 25, 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 25, 21, and 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
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ Install them with dnf:
|
|||||||
|
|
||||||
sudo dnf -y update
|
sudo dnf -y update
|
||||||
sudo dnf -y group install development
|
sudo dnf -y group install development
|
||||||
sudo dnf -y install python3.11 python3.11-devel java-17-openjdk-headless nano git
|
sudo dnf -y install python3.11 python3.11-devel java-25-openjdk-headless nano git
|
||||||
|
|
||||||
Set ``java`` executable to point to Java 17:
|
Set ``java`` executable to point to Java 21:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo alternatives --set java "java-17-openjdk.$(uname -i)"
|
sudo alternatives --set java "java-21-openjdk.$(uname -i)"
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Red Hat Enterprise Linux (RHEL) 9.4-9.x and its derivatives have all required packages available in official repositories.
|
Red Hat Enterprise Linux (RHEL) 9.6-9.x and its derivatives have all required packages available in official repositories.
|
||||||
Install them with dnf:
|
Install them with dnf:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo dnf -y install python3.11 python3.11-devel git java-17-openjdk-headless @development nano
|
sudo dnf -y install python3.11 python3.11-devel git java-25-openjdk-headless @development nano
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ them with dnf:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo dnf -y install python3.11 python3.11-devel git java-17-amazon-corretto-headless @development nano
|
sudo dnf -y install python3.11 python3.11-devel git java-25-amazon-corretto-headless @development nano
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Install the pre-requirements with pacman:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo pacman -Syu git jre17-openjdk-headless base-devel nano
|
sudo pacman -Syu git jre25-openjdk-headless base-devel nano
|
||||||
|
|
||||||
On Arch Linux, Python 3.11 can be installed from the Arch User Repository (AUR) from the ``python311`` package.
|
On Arch Linux, Python 3.11 can be installed from the Arch User Repository (AUR) from the ``python311`` package.
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ Installing Red on Fedora Linux
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Fedora Linux 42 and above has all required packages available in official repositories. Install
|
Fedora Linux 43 and above has all required packages available in official repositories. Install
|
||||||
them with dnf:
|
them with dnf:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo dnf -y install python3.11 python3.11-devel git adoptium-temurin-java-repository @development-tools nano
|
sudo dnf -y install python3.11 python3.11-devel git adoptium-temurin-java-repository @development-tools nano
|
||||||
sudo dnf config-manager setopt adoptium-temurin-java-repository.enabled=1
|
sudo dnf config-manager setopt adoptium-temurin-java-repository.enabled=1
|
||||||
sudo dnf -y install temurin-17-jre
|
sudo dnf -y install temurin-25-jre
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ one-by-one:
|
|||||||
|
|
||||||
brew install python@3.11
|
brew install python@3.11
|
||||||
brew install git
|
brew install git
|
||||||
brew install temurin@17
|
brew install temurin@25
|
||||||
|
|
||||||
By default, Python installed through Homebrew is not added to the load path.
|
By default, Python installed through Homebrew is not added to the load path.
|
||||||
To fix this, you should run these commands:
|
To fix this, you should run these commands:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ with zypper:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo zypper -n install python311 python311-devel git-core java-17-openjdk-headless nano
|
sudo zypper -n install python311 python311-devel git-core java-21-openjdk-headless nano
|
||||||
sudo zypper -n install -t pattern devel_basis
|
sudo zypper -n install -t pattern devel_basis
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ with zypper:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo zypper -n install python311 python311-devel git-core java-17-openjdk-headless nano
|
sudo zypper -n install python311 python311-devel git-core java-25-openjdk-headless nano
|
||||||
sudo zypper -n install -t pattern devel_basis
|
sudo zypper -n install -t pattern devel_basis
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ with apt:
|
|||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt -y install python3.10 python3.10-dev python3.10-venv git openjdk-17-jre-headless build-essential nano
|
sudo apt -y install python3.10 python3.10-dev python3.10-venv git openjdk-25-jre-headless build-essential nano
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Now install the pre-requirements with apt:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo apt -y install python3.11 python3.11-dev python3.11-venv git openjdk-17-jre-headless build-essential nano
|
sudo apt -y install python3.11 python3.11-dev python3.11-venv git openjdk-25-jre-headless build-essential nano
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ For Audio support, you should also run the following command before exiting:
|
|||||||
|
|
||||||
.. prompt:: powershell
|
.. prompt:: powershell
|
||||||
|
|
||||||
choco upgrade temurin17 -y
|
choco upgrade temurin25 -y
|
||||||
|
|
||||||
|
|
||||||
From here, exit the prompt then continue onto `creating-venv-windows`.
|
From here, exit the prompt then continue onto `creating-venv-windows`.
|
||||||
@@ -66,7 +66,7 @@ Manually installing dependencies
|
|||||||
|
|
||||||
.. attention:: Please choose the option to "Git from the command line and also from 3rd-party software" in Git's setup.
|
.. attention:: Please choose the option to "Git from the command line and also from 3rd-party software" in Git's setup.
|
||||||
|
|
||||||
* `Java 17 <https://adoptium.net/temurin/releases/?version=17>`_ - needed for Audio
|
* `Java 25 <https://adoptium.net/temurin/releases/?version=25>`_ - needed for Audio
|
||||||
|
|
||||||
From here, continue onto `creating-venv-windows`.
|
From here, continue onto `creating-venv-windows`.
|
||||||
|
|
||||||
|
|||||||
+57
-13
@@ -25,13 +25,13 @@ Updating differs depending on the version you currently have. Next sections will
|
|||||||
:depth: 1
|
:depth: 1
|
||||||
|
|
||||||
|
|
||||||
Red 3.5.0 or newer
|
Red 3.5.25 or newer
|
||||||
******************
|
*******************
|
||||||
|
|
||||||
Windows
|
Windows
|
||||||
-------
|
-------
|
||||||
|
|
||||||
If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
If you have Red 3.5.25 or newer, you can upgrade by following these steps:
|
||||||
|
|
||||||
#. Shut your bot down.
|
#. Shut your bot down.
|
||||||
#. Activate your venv with the following command:
|
#. Activate your venv with the following command:
|
||||||
@@ -44,18 +44,13 @@ If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
|||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
redbot-update
|
||||||
|
|
||||||
.. attention::
|
|
||||||
|
|
||||||
If you're using PostgreSQL data backend, replace ``Red-DiscordBot`` in the second command with ``Red-DiscordBot[postgres]``
|
|
||||||
#. Start your bot.
|
#. Start your bot.
|
||||||
#. If you have any 3rd-party cogs installed, we highly recommend you update them with this command in Discord: ``[p]cog update`` (``[p]`` is considered as your prefix)
|
|
||||||
|
|
||||||
Linux & Mac
|
Linux & Mac
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
If you have Red 3.5.25 or newer, you can upgrade by following these steps:
|
||||||
|
|
||||||
#. Shut your bot down.
|
#. Shut your bot down.
|
||||||
#. Activate your virtual environment.
|
#. Activate your virtual environment.
|
||||||
@@ -71,7 +66,56 @@ If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
|||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
redbot-update
|
||||||
|
#. Start your bot.
|
||||||
|
|
||||||
|
Red 3.5.0-3.5.24
|
||||||
|
****************
|
||||||
|
|
||||||
|
Windows
|
||||||
|
-------
|
||||||
|
|
||||||
|
If you have a Red version between 3.5.0 and 3.5.24, you can upgrade by following these steps:
|
||||||
|
|
||||||
|
#. Shut your bot down.
|
||||||
|
#. Activate your venv with the following command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
|
||||||
|
"%userprofile%\redenv\Scripts\activate.bat"
|
||||||
|
#. Update Red with this command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
|
python -m pip install -U "Red-DiscordBot==3.5.25"
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
If you're using PostgreSQL data backend, replace ``Red-DiscordBot`` in the second command with ``Red-DiscordBot[postgres]``
|
||||||
|
#. Start your bot.
|
||||||
|
#. If you have any 3rd-party cogs installed, we highly recommend you update them with this command in Discord: ``[p]cog update`` (``[p]`` is considered as your prefix)
|
||||||
|
|
||||||
|
Linux & Mac
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If you have a Red version between 3.5.0 and 3.5.24, you can upgrade by following these steps:
|
||||||
|
|
||||||
|
#. Shut your bot down.
|
||||||
|
#. Activate your virtual environment.
|
||||||
|
|
||||||
|
If you used ``venv`` for your virtual environment, use:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
source ~/redenv/bin/activate
|
||||||
|
|
||||||
|
#. Update Red with this command:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
:prompts: (redenv) $
|
||||||
|
|
||||||
|
python -m pip install -U 'Red-DiscordBot==3.5.25'
|
||||||
|
|
||||||
.. attention::
|
.. attention::
|
||||||
|
|
||||||
@@ -98,7 +142,7 @@ If you have a Red version between 3.2.0 and 3.4.19, you can upgrade by following
|
|||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U "Red-DiscordBot==3.5.25"
|
||||||
|
|
||||||
.. attention::
|
.. attention::
|
||||||
|
|
||||||
@@ -137,7 +181,7 @@ If you have a Red version between 3.2.0 and 3.4.19, you can upgrade by following
|
|||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U 'Red-DiscordBot==3.5.25'
|
||||||
|
|
||||||
.. attention::
|
.. attention::
|
||||||
|
|
||||||
|
|||||||
@@ -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 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>`__)
|
Fedora Linux 44 x86-64, aarch64 2027-06-02 (`End of Life <https://fedorapeople.org/groups/schedule/f-44/f-44-key-tasks.html>`__)
|
||||||
|
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>`__)
|
||||||
@@ -71,10 +71,9 @@ Raspberry Pi OS (Legacy) 12 aarch64, armv7l ~2027-10 (approxima
|
|||||||
RHEL 8 (latest) x86-64, aarch64 2029-05-31 (`End of Maintenance Support <https://access.redhat.com/support/policy/updates/errata#Life_Cycle_Dates>`__)
|
RHEL 8 (latest) x86-64, aarch64 2029-05-31 (`End of Maintenance Support <https://access.redhat.com/support/policy/updates/errata#Life_Cycle_Dates>`__)
|
||||||
RHEL 8.10 x86-64, aarch64 2029-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
RHEL 8.10 x86-64, aarch64 2029-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
||||||
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.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>`__)
|
||||||
================================ ======================= ============================================================
|
================================ ======================= ============================================================
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ Command to execute. See Cmdlet's description for more information.
|
|||||||
|
|
||||||
#>
|
#>
|
||||||
|
|
||||||
# I'm too dumb for PowerShell, so $script:availableCommands needs to be defined in 2 places // Jack
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param (
|
param (
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
@@ -68,7 +66,7 @@ function activateenv() {
|
|||||||
& $PSScriptRoot\.venv\Scripts\Activate.ps1
|
& $PSScriptRoot\.venv\Scripts\Activate.ps1
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:availableCommands = @("reformat", "stylecheck", "stylediff", "newenv", "syncenv", "activateenv")
|
$script:availableCommands = $MyInvocation.MyCommand.ParameterSets[0].Parameters[0].Attributes[0].ScriptBlock.Invoke()
|
||||||
|
|
||||||
if (Test-Path -LiteralPath "$PSScriptRoot\.venv" -PathType Container) {
|
if (Test-Path -LiteralPath "$PSScriptRoot\.venv" -PathType Container) {
|
||||||
$script:venvPython = "$PSScriptRoot\.venv\Scripts\python.exe"
|
$script:venvPython = "$PSScriptRoot\.venv\Scripts\python.exe"
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final, Optional, Tuple
|
||||||
|
|
||||||
|
import click
|
||||||
|
from packaging.version import Version
|
||||||
|
from python_discovery import PythonInfo
|
||||||
|
|
||||||
|
from redbot.core._cli import asyncio_run
|
||||||
|
|
||||||
|
from . import cmd, common, updater
|
||||||
|
|
||||||
|
|
||||||
|
_CHECK_OTHER_PYTHON_INSTALLS_CMD_ARG_NAME: Final = "--check-other-python-installs"
|
||||||
|
|
||||||
|
|
||||||
|
def _help_major_update_example() -> str:
|
||||||
|
version = common.get_current_red_version().__replace__(dev=None, local=None)
|
||||||
|
release = (version.major, version.minor + 1) + (0,) * (len(version.release) - 2)
|
||||||
|
next_major_version = version.__replace__(release=release)
|
||||||
|
return f"updating from Red {version} to Red {next_major_version}"
|
||||||
|
|
||||||
|
|
||||||
|
def _help_minor_update_example() -> str:
|
||||||
|
version = common.get_current_red_version().__replace__(dev=None, local=None)
|
||||||
|
release = (version.major, version.minor, version.micro + 1) + (0,) * (len(version.release) - 3)
|
||||||
|
next_minor_version = version.__replace__(release=release)
|
||||||
|
return f"updating from Red {version} to Red {next_minor_version}"
|
||||||
|
|
||||||
|
|
||||||
|
class _PythonInfoParamType(click.ParamType):
|
||||||
|
name = "Python interpreter"
|
||||||
|
|
||||||
|
def convert(
|
||||||
|
self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
|
||||||
|
) -> PythonInfo:
|
||||||
|
if isinstance(value, PythonInfo):
|
||||||
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
return PythonInfo.from_exe(value)
|
||||||
|
except RuntimeError:
|
||||||
|
self.fail(f"{value!r} is not a valid Python executable.", param, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
# command-specific options
|
||||||
|
@click.option(
|
||||||
|
"--include-instance",
|
||||||
|
"included_instances",
|
||||||
|
multiple=True,
|
||||||
|
type=click.Choice(common.INSTANCE_LIST),
|
||||||
|
help="The list of instances to backup and check cog compatibility for. If not specified,"
|
||||||
|
" all instances that use the current virtual environment will be backed up and checked.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--exclude-instance",
|
||||||
|
"excluded_instances",
|
||||||
|
multiple=True,
|
||||||
|
type=click.Choice(common.INSTANCE_LIST),
|
||||||
|
help="Exclude an instance from the list of instances to backup"
|
||||||
|
" and check cog compatibility for.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--backup-dir",
|
||||||
|
default=None,
|
||||||
|
type=click.Path(
|
||||||
|
dir_okay=True, file_okay=False, resolve_path=True, writable=True, path_type=Path
|
||||||
|
),
|
||||||
|
help="The directory to place the backups of the virtual environment and instances.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-backup",
|
||||||
|
help="Do not make backups of the virtual environment and instances before update.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--red-version",
|
||||||
|
"--version",
|
||||||
|
type=common.VersionParamType(),
|
||||||
|
default=None,
|
||||||
|
help="Version of Red to update to instead of the latest.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-major-updates",
|
||||||
|
help=f"Skip major updates. For example: {_help_major_update_example()} is a major update"
|
||||||
|
f" but {_help_minor_update_example()} isn't.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-full-changelog",
|
||||||
|
help='Skip showing full changelog in a terminal user interface. The "Read before updating"'
|
||||||
|
" sections will still be printed.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-cog-compatibility-check",
|
||||||
|
help="Skip performing cog compatibility check before the update.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--new-python-interpreter",
|
||||||
|
type=_PythonInfoParamType(),
|
||||||
|
help="The new Python interpreter that should be used when creating a virtual environment"
|
||||||
|
" for Red. This can either be a path to a Python executable or a name of a Python executable"
|
||||||
|
" on the PATH.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--update-cogs/--no-update-cogs",
|
||||||
|
default=None,
|
||||||
|
help="When this option is used, it determines whether the cogs should be updated after Red"
|
||||||
|
" is updated. By default, you'll be asked, if you want to update.\n"
|
||||||
|
"In non-interactive mode, cogs will be updated unless this option is used to override"
|
||||||
|
" the default behavior.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
# `pip install` having an option with the same name is coincidental,
|
||||||
|
# this does not call `pip install` with the `--force-reinstall` option.
|
||||||
|
# Not that there would be any point in doing so - we create a fresh virtual environment.
|
||||||
|
"--force-reinstall",
|
||||||
|
type=bool,
|
||||||
|
is_flag=True,
|
||||||
|
help="Force the update process to proceed even, if there is no new version detected."
|
||||||
|
" This will essentially reinstall latest Red version into a fresh virtual environment. You can"
|
||||||
|
" combine it with the --new-python-interpreter option to change Red's Python interpreter.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-prompt",
|
||||||
|
"interactive",
|
||||||
|
type=bool,
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help="Don't ask for user input during the process (non-interactive mode).\n"
|
||||||
|
"NOTE: If you want to use this to automate Red updates, consider specifying --no-major-update"
|
||||||
|
" to avoid performing major updates without making an explicit decision to.\n"
|
||||||
|
"When performing a major update where the current Python interpreter is no longer compatible,"
|
||||||
|
" the --new-python-interpreter option has to be specified or the command will fail.",
|
||||||
|
)
|
||||||
|
# global options
|
||||||
|
@click.option(
|
||||||
|
cmd.arg_names.DEBUG,
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
"logging_level",
|
||||||
|
count=True,
|
||||||
|
help=(
|
||||||
|
"Increase the verbosity of the logs, each usage of this flag increases the verbosity"
|
||||||
|
" level by 1."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--check-other-venvs",
|
||||||
|
_CHECK_OTHER_PYTHON_INSTALLS_CMD_ARG_NAME,
|
||||||
|
"ignore_prefix",
|
||||||
|
help="Check the compatibility of cogs for instances that are normally ran with"
|
||||||
|
" a different Python installation and/or virtual environment than the current one.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(
|
||||||
|
ctx: click.Context,
|
||||||
|
included_instances: Tuple[str, ...],
|
||||||
|
excluded_instances: Tuple[str, ...],
|
||||||
|
backup_dir: Optional[Path],
|
||||||
|
no_backup: bool,
|
||||||
|
red_version: Optional[Version],
|
||||||
|
no_major_updates: bool,
|
||||||
|
no_full_changelog: bool,
|
||||||
|
no_cog_compatibility_check: bool,
|
||||||
|
new_python_interpreter: Optional[PythonInfo],
|
||||||
|
update_cogs: Optional[bool],
|
||||||
|
force_reinstall: bool,
|
||||||
|
interactive: bool,
|
||||||
|
logging_level: int,
|
||||||
|
ignore_prefix: bool,
|
||||||
|
) -> None:
|
||||||
|
common.ensure_supported_env()
|
||||||
|
common.configure_logging(logging_level)
|
||||||
|
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj["IGNORE_PREFIX"] = ignore_prefix
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
if included_instances:
|
||||||
|
# de-duplicate with order intact
|
||||||
|
instances = list(dict.fromkeys(included_instances))
|
||||||
|
else:
|
||||||
|
instances = list(common.INSTANCE_LIST)
|
||||||
|
options = updater.UpdaterOptions(
|
||||||
|
instances=instances,
|
||||||
|
excluded_instances=set(excluded_instances),
|
||||||
|
ignore_prefix=ignore_prefix,
|
||||||
|
backup_dir=backup_dir,
|
||||||
|
no_backup=no_backup,
|
||||||
|
red_version=red_version,
|
||||||
|
no_major_updates=no_major_updates,
|
||||||
|
no_full_changelog=no_full_changelog,
|
||||||
|
no_cog_compatibility_check=no_cog_compatibility_check,
|
||||||
|
new_python_interpreter=new_python_interpreter,
|
||||||
|
update_cogs=update_cogs,
|
||||||
|
force_reinstall=force_reinstall,
|
||||||
|
interactive=interactive,
|
||||||
|
)
|
||||||
|
app = updater.Updater(options)
|
||||||
|
asyncio_run(app.run())
|
||||||
|
# these should not be available to subcommands
|
||||||
|
elif included_instances:
|
||||||
|
raise click.NoSuchOption("--include-instance", ctx=ctx)
|
||||||
|
elif excluded_instances:
|
||||||
|
raise click.NoSuchOption("--exclude-instance", ctx=ctx)
|
||||||
|
elif backup_dir is not None:
|
||||||
|
raise click.NoSuchOption("--backup-dir", ctx=ctx)
|
||||||
|
elif no_backup:
|
||||||
|
raise click.NoSuchOption("--no-backup", ctx=ctx)
|
||||||
|
elif red_version:
|
||||||
|
raise click.NoSuchOption("--red-version", ctx=ctx)
|
||||||
|
elif no_major_updates:
|
||||||
|
raise click.NoSuchOption("--no-major-updates", ctx=ctx)
|
||||||
|
elif no_cog_compatibility_check:
|
||||||
|
raise click.NoSuchOption("--no-cog-compatibility-check", ctx=ctx)
|
||||||
|
elif new_python_interpreter:
|
||||||
|
raise click.NoSuchOption("--new-python-interpreter", ctx=ctx)
|
||||||
|
elif update_cogs is True:
|
||||||
|
raise click.NoSuchOption("--update-cogs", ctx=ctx)
|
||||||
|
elif update_cogs is False:
|
||||||
|
raise click.NoSuchOption("--no-update-cogs", ctx=ctx)
|
||||||
|
elif not interactive:
|
||||||
|
raise click.NoSuchOption("--no-prompt", ctx=ctx)
|
||||||
|
elif force_reinstall:
|
||||||
|
raise click.NoSuchOption("--force-reinstall", ctx=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
cli.add_command(cmd.cog_compatibility.check_cog_compatibility)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import dataclasses
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import yarl
|
||||||
|
from packaging.version import Version
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
|
_CHANGELOG_PATTERN = re.compile(
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-BEGIN: (?P<version>.+) +--+>\n"
|
||||||
|
r"(?P<content>[\s\S]+?)"
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-END +--+>"
|
||||||
|
)
|
||||||
|
_RTD_CANONICAL_URL = os.getenv("_RED_RTD_CANONICAL_URL") or "https://docs.discord.red/en/stable/"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class VersionChangelog:
|
||||||
|
version: Version
|
||||||
|
content: str
|
||||||
|
_RELEASE_DATE_PATTERN = re.compile(
|
||||||
|
r"^<!--+ +RED-CHANGELOG-RELEASE-DATE: (\d{4})-(\d{2})-(\d{2}) +--+>$",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
_CONTRIBUTORS_PATTERN = re.compile(
|
||||||
|
r"^<!--+ +RED-CHANGELOG-CONTRIBUTORS: (?P<contributors>.+) +--+>$",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
_READ_BEFORE_UPDATING_SECTION_PATTERN = re.compile(
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-READ-BEFORE-UPDATE-BEGIN +--+>\n"
|
||||||
|
r"(?P<content>[\s\S]+?)"
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-READ-BEFORE-UPDATE-END +--+>"
|
||||||
|
)
|
||||||
|
_USER_CHANGELOG_SECTION_PATTERN = re.compile(
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-USER-CHANGELOG-BEGIN +--+>\n"
|
||||||
|
r"(?P<content>[\s\S]+?)"
|
||||||
|
r"\n<!--+ +RED-CHANGELOG-USER-CHANGELOG-END +--+>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
return cls(version=Version(data["version"]), content=data["content"])
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"version": str(self.version), "content": self.content}
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def release_date(self) -> datetime.date:
|
||||||
|
return datetime.date(*map(int, self._RELEASE_DATE_PATTERN.search(self.content).groups()))
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def contributors(self) -> List[str]:
|
||||||
|
match = self._CONTRIBUTORS_PATTERN.search(self.content)
|
||||||
|
if match is None:
|
||||||
|
return []
|
||||||
|
return match["contributors"].split()
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def read_before_updating_section(self) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
match["content"].strip()
|
||||||
|
for match in self._READ_BEFORE_UPDATING_SECTION_PATTERN.finditer(self.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def user_changelog_section(self) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
match["content"].strip()
|
||||||
|
for match in self._USER_CHANGELOG_SECTION_PATTERN.finditer(self.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Changelogs = Dict[Version, VersionChangelog]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelogs(content: str) -> Changelogs:
|
||||||
|
changelogs = {}
|
||||||
|
for match in _CHANGELOG_PATTERN.finditer(content):
|
||||||
|
changelog = VersionChangelog(Version(match["version"]), match["content"])
|
||||||
|
changelogs[changelog.version] = changelog
|
||||||
|
|
||||||
|
return changelogs
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(changelogs: Changelogs, *, minimal: bool = False) -> str:
|
||||||
|
if not changelogs:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parts = ["# Read before updating"]
|
||||||
|
for changelog in reversed(changelogs.values()):
|
||||||
|
parts.append(f"## {changelog.version}")
|
||||||
|
parts.append(changelog.read_before_updating_section)
|
||||||
|
|
||||||
|
contributors = sorted(
|
||||||
|
{
|
||||||
|
contributor
|
||||||
|
for changelog in changelogs.values()
|
||||||
|
for contributor in changelog.contributors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if contributors:
|
||||||
|
contributor_thanks = (
|
||||||
|
" \n**The releases below were made with help from the following people:** \n"
|
||||||
|
)
|
||||||
|
contributor_thanks += ", ".join(
|
||||||
|
f"[@{contributor}](https://github.com/sponsors/{contributor})"
|
||||||
|
for contributor in contributors
|
||||||
|
)
|
||||||
|
contributor_thanks += " \n**Thank you** \N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}"
|
||||||
|
parts.append(contributor_thanks)
|
||||||
|
|
||||||
|
# show the header both at the top and the bottom
|
||||||
|
parts.append(parts[0])
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_changelogs_between(
|
||||||
|
changelogs: Changelogs, newer_than: Version, not_newer_than: Version
|
||||||
|
) -> Changelogs:
|
||||||
|
return {
|
||||||
|
changelog_version: changelog
|
||||||
|
for changelog_version, changelog in changelogs.items()
|
||||||
|
if newer_than < changelog_version <= not_newer_than
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_changelogs() -> Changelogs:
|
||||||
|
"""
|
||||||
|
Fetch the Markdown-formatted changelog from Red's docs site.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Dict[Version, VersionChangelog]
|
||||||
|
A dict mapping versions to their changelogs. Sorted by version, newest first.
|
||||||
|
"""
|
||||||
|
async with aiohttp.ClientSession(raise_for_status=True) as session:
|
||||||
|
async with session.get(yarl.URL(_RTD_CANONICAL_URL) / "_markdown/changelog.md") as resp:
|
||||||
|
return parse_changelogs(await resp.text())
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from . import arg_names, cog_compatibility
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"arg_names",
|
||||||
|
"cog_compatibility",
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DEBUG: Final = "--debug"
|
||||||
|
RED_VERSION: Final = "--red-version"
|
||||||
|
PYTHON_VERSION: Final = "--python-version"
|
||||||
|
CHECK_OTHER_PYTHON_INSTALLS: Final = "--check-other-python-installs"
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import Final, Optional, Tuple
|
||||||
|
|
||||||
|
import click
|
||||||
|
from packaging.version import Version
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from redbot._update import cog_compatibility_checker, common
|
||||||
|
from redbot._update.cog_compatibility_checker import CompatibilitySummary
|
||||||
|
from redbot.core import _drivers
|
||||||
|
from redbot.core._cli import asyncio_run
|
||||||
|
from redbot.core.utils._internal_utils import fetch_latest_red_version
|
||||||
|
|
||||||
|
from . import arg_names
|
||||||
|
|
||||||
|
|
||||||
|
EXIT_INSTANCE_SITE_PREFIX_MISMATCH: Final = 4
|
||||||
|
EXIT_INSTANCE_BACKEND_UNSUPPORTED: Final = 5
|
||||||
|
CMD_NAME: Final = "check-cog-compatibility"
|
||||||
|
_COMPATIBILITY_RESULTS_ENV_VAR = "_RED_UPDATE_COMPATIBILITY_RESULTS_FILE"
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(CMD_NAME)
|
||||||
|
@click.argument(
|
||||||
|
"instances",
|
||||||
|
nargs=-1,
|
||||||
|
type=click.Choice(common.INSTANCE_LIST),
|
||||||
|
default=None,
|
||||||
|
metavar="[INSTANCE_NAME]",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
arg_names.RED_VERSION,
|
||||||
|
type=common.VersionParamType(),
|
||||||
|
default=None,
|
||||||
|
help="The Red version to check cog compatibility for."
|
||||||
|
" If not provided, the information about latest available version will be fetched"
|
||||||
|
" and the command will check whether installed cogs support that version.\n"
|
||||||
|
"If this option is provided, --python-version also has to be provided.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
arg_names.PYTHON_VERSION,
|
||||||
|
type=common.VersionParamType(),
|
||||||
|
default=None,
|
||||||
|
help="The Python version to check cog compatibility for."
|
||||||
|
" If not provided, the command will either use the current interpreter's version or,"
|
||||||
|
" if that version is not compatible with the latest Red version, it will try to"
|
||||||
|
" find the latest available CPython interpreter on the system and will check whether"
|
||||||
|
" installed cogs support it.\n"
|
||||||
|
"If this option is provided, --red-version also has to be provided.",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def check_cog_compatibility(
|
||||||
|
ctx: click.Context,
|
||||||
|
instances: Tuple[str, ...],
|
||||||
|
red_version: Optional[Version],
|
||||||
|
python_version: Optional[Version],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Check if the installed cogs are compatible with the given version.
|
||||||
|
"""
|
||||||
|
if (red_version, python_version).count(None) == 1:
|
||||||
|
raise click.BadParameter(
|
||||||
|
"Either both --red-version and --python-version options"
|
||||||
|
" have to be specified or neither.",
|
||||||
|
param_hint=[arg_names.RED_VERSION, arg_names.PYTHON_VERSION],
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio_run(
|
||||||
|
_check_cog_compatibility_command_impl(
|
||||||
|
red_version=red_version,
|
||||||
|
python_version=python_version,
|
||||||
|
instances=instances,
|
||||||
|
ignore_prefix=ctx.obj["IGNORE_PREFIX"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_cog_compatibility_command_impl(
|
||||||
|
*,
|
||||||
|
red_version: Optional[Version],
|
||||||
|
python_version: Optional[Version],
|
||||||
|
instances: Tuple[str, ...] = (),
|
||||||
|
ignore_prefix: bool = False,
|
||||||
|
) -> None:
|
||||||
|
console = common.get_console()
|
||||||
|
if red_version is None or python_version is None:
|
||||||
|
with console.status("Checking latest version..."):
|
||||||
|
latest = await fetch_latest_red_version(
|
||||||
|
include_prereleases=common.get_current_red_version().is_prerelease
|
||||||
|
)
|
||||||
|
red_version = latest.version
|
||||||
|
|
||||||
|
python_version = Version(".".join(map(str, sys.version_info[:3])))
|
||||||
|
if python_version not in latest.requires_python:
|
||||||
|
interpreters = common.search_for_interpreters(latest.requires_python)
|
||||||
|
_, python_version, _ = interpreters[0]
|
||||||
|
|
||||||
|
if len(instances) == 1:
|
||||||
|
results_file = os.getenv(_COMPATIBILITY_RESULTS_ENV_VAR, "")
|
||||||
|
try:
|
||||||
|
results = await cog_compatibility_checker.check_instance(
|
||||||
|
instances[0],
|
||||||
|
latest_version=red_version,
|
||||||
|
interpreter_version=python_version,
|
||||||
|
ignore_prefix=ignore_prefix,
|
||||||
|
)
|
||||||
|
except _drivers.MissingExtraRequirements:
|
||||||
|
if not results_file:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
Text(instances[0], style="bold"),
|
||||||
|
" instance could not be checked as it uses a storage backend"
|
||||||
|
" that is not supported by the current Red installation"
|
||||||
|
" (some requirements are missing).",
|
||||||
|
)
|
||||||
|
raise SystemExit(EXIT_INSTANCE_BACKEND_UNSUPPORTED)
|
||||||
|
except cog_compatibility_checker.InstanceSitePrefixMismatchError as exc:
|
||||||
|
if not results_file:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
Text(exc.instance_name, style="bold"),
|
||||||
|
" instance could not be checked as it is a part of"
|
||||||
|
" a different Python installation and/or virtual environment.",
|
||||||
|
)
|
||||||
|
raise SystemExit(EXIT_INSTANCE_SITE_PREFIX_MISMATCH)
|
||||||
|
if results_file:
|
||||||
|
with open(results_file, "w", encoding="utf-8") as fp:
|
||||||
|
json.dump(results.to_json_dict(), fp)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not instances:
|
||||||
|
instances = tuple(common.INSTANCE_LIST)
|
||||||
|
checked_instances = []
|
||||||
|
for instance_name in instances:
|
||||||
|
exit_code, _, _ = await call(
|
||||||
|
instance_name,
|
||||||
|
red_version=red_version,
|
||||||
|
python_version=python_version,
|
||||||
|
ignore_prefix=ignore_prefix,
|
||||||
|
)
|
||||||
|
if exit_code != EXIT_INSTANCE_SITE_PREFIX_MISMATCH:
|
||||||
|
if exit_code:
|
||||||
|
raise SystemExit(exit_code)
|
||||||
|
checked_instances.append(instance_name)
|
||||||
|
|
||||||
|
if not checked_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR, "There were no instances to check cog compatibility for."
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
async def call(
|
||||||
|
instance_name: str,
|
||||||
|
*,
|
||||||
|
red_version: Version,
|
||||||
|
python_version: Version,
|
||||||
|
ignore_prefix: bool = False,
|
||||||
|
return_results: bool = False,
|
||||||
|
stdout: Optional[int] = None,
|
||||||
|
) -> Tuple[int, Optional[str], Optional[CompatibilitySummary]]:
|
||||||
|
debug_args = (arg_names.DEBUG,) * common.get_log_cli_level()
|
||||||
|
args = [
|
||||||
|
"-m",
|
||||||
|
"redbot._update",
|
||||||
|
*debug_args,
|
||||||
|
CMD_NAME,
|
||||||
|
instance_name,
|
||||||
|
arg_names.RED_VERSION,
|
||||||
|
str(red_version),
|
||||||
|
arg_names.PYTHON_VERSION,
|
||||||
|
str(python_version),
|
||||||
|
]
|
||||||
|
if ignore_prefix:
|
||||||
|
args.append(arg_names.CHECK_OTHER_PYTHON_INSTALLS)
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# terminal woes
|
||||||
|
console = common.get_console()
|
||||||
|
if console.is_terminal:
|
||||||
|
env["TTY_COMPATIBLE"] = "1"
|
||||||
|
# Rich only checks stdout for Windows console features:
|
||||||
|
# https://github.com/Textualize/rich/blob/fc41075a3206d2a5fd846c6f41c4d2becab814fa/rich/_windows.py#L46
|
||||||
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "1" if console.legacy_windows else "0"
|
||||||
|
else:
|
||||||
|
# Rich does not set legacy_windows correctly when is_terminal is False
|
||||||
|
# https://github.com/Textualize/rich/issues/3647
|
||||||
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "0"
|
||||||
|
env["PYTHONIOENCODING"] = sys.stdout.encoding
|
||||||
|
|
||||||
|
results = None
|
||||||
|
results_file = None
|
||||||
|
if return_results:
|
||||||
|
results_file = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
try:
|
||||||
|
if results_file is not None:
|
||||||
|
results_file.close()
|
||||||
|
env[_COMPATIBILITY_RESULTS_ENV_VAR] = str(results_file.name)
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_exec(sys.executable, *args, env=env, stdout=stdout)
|
||||||
|
stdout_data, _ = await proc.communicate()
|
||||||
|
decoded_stdout = None
|
||||||
|
if stdout_data is not None:
|
||||||
|
decoded_stdout = stdout_data.decode()
|
||||||
|
exit_code = await proc.wait()
|
||||||
|
if not exit_code and results_file is not None:
|
||||||
|
with open(results_file.name, encoding="utf-8") as fp:
|
||||||
|
results = CompatibilitySummary.from_json_dict(json.load(fp))
|
||||||
|
finally:
|
||||||
|
if results_file is not None:
|
||||||
|
os.remove(results_file.name)
|
||||||
|
|
||||||
|
return exit_code, decoded_stdout, results
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Set, Tuple
|
||||||
|
|
||||||
|
import rich
|
||||||
|
from packaging.version import Version
|
||||||
|
from rich.text import Text
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from redbot.core import _downloader, _drivers, data_manager
|
||||||
|
from redbot.core._cli import parse_cli_flags
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.utils._internal_utils import detailed_progress
|
||||||
|
|
||||||
|
from . import common
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceSitePrefixMismatchError(Exception):
|
||||||
|
"""The instance's last known sys.prefix is different from the current one."""
|
||||||
|
|
||||||
|
def __init__(self, instance_name: str, last_known_prefix: Optional[str]) -> None:
|
||||||
|
self.instance_name = instance_name
|
||||||
|
self.last_known_prefix = last_known_prefix
|
||||||
|
super().__init__(
|
||||||
|
f"The last known sys.prefix of {instance_name!r} is different from"
|
||||||
|
" current process's sys.prefix.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCompatibilityStatus(common.OrderedEnum):
|
||||||
|
UNSUPPORTED = enum.auto()
|
||||||
|
POTENTIALLY_SUPPORTED = enum.auto()
|
||||||
|
EXPLICITLY_SUPPORTED = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibilityStatus(enum.Enum):
|
||||||
|
# unsupported is <100, 200)
|
||||||
|
UNSUPPORTED_PYTHON_VERSION = 100
|
||||||
|
UNSUPPORTED_BOT_VERSION = 101
|
||||||
|
# potentially supported is <200, 300)
|
||||||
|
POTENTIALLY_SUPPORTED = 200
|
||||||
|
# explicitly supported is <300, 400)
|
||||||
|
EXPLICITLY_SUPPORTED_NON_BREAKING = 300
|
||||||
|
EXPLICITLY_SUPPORTED_MIN_BOT_VERSION = 301
|
||||||
|
EXPLICITLY_SUPPORTED_MAX_BOT_VERSION = 302
|
||||||
|
EXPLICITLY_SUPPORTED_READY_TAG = 303
|
||||||
|
|
||||||
|
@property
|
||||||
|
def simple_status(self) -> SimpleCompatibilityStatus:
|
||||||
|
if self.unsupported:
|
||||||
|
return SimpleCompatibilityStatus.UNSUPPORTED
|
||||||
|
if self.potentially_supported:
|
||||||
|
return SimpleCompatibilityStatus.POTENTIALLY_SUPPORTED
|
||||||
|
if self.explicitly_supported:
|
||||||
|
return SimpleCompatibilityStatus.EXPLICITLY_SUPPORTED
|
||||||
|
raise RuntimeError("unreachable")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unsupported(self) -> bool:
|
||||||
|
return 100 <= self.value < 200
|
||||||
|
|
||||||
|
@property
|
||||||
|
def potentially_supported(self) -> bool:
|
||||||
|
return 200 <= self.value < 300
|
||||||
|
|
||||||
|
@property
|
||||||
|
def explicitly_supported(self) -> bool:
|
||||||
|
return 300 <= self.value < 400
|
||||||
|
|
||||||
|
def __ge__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.simple_status >= other.simple_status
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __gt__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.simple_status > other.simple_status
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __le__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.simple_status <= other.simple_status
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.simple_status < other.simple_status
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogCompatibilityInfo:
|
||||||
|
name: str
|
||||||
|
repo_name: str
|
||||||
|
min_bot_version: Version
|
||||||
|
max_bot_version: Version
|
||||||
|
min_python_version: Version
|
||||||
|
tags: Tuple[str, ...]
|
||||||
|
compatibility_status: CompatibilityStatus = CompatibilityStatus.POTENTIALLY_SUPPORTED
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_installable(cls, installable: _downloader.Installable) -> Self:
|
||||||
|
return cls(
|
||||||
|
name=installable.name,
|
||||||
|
repo_name=installable.repo_name,
|
||||||
|
min_bot_version=installable.min_bot_version,
|
||||||
|
max_bot_version=installable.max_bot_version,
|
||||||
|
min_python_version=installable.min_python_version,
|
||||||
|
tags=installable.tags,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
name=data["name"],
|
||||||
|
repo_name=data["repo_name"],
|
||||||
|
min_bot_version=Version(data["min_bot_version"]),
|
||||||
|
max_bot_version=Version(data["max_bot_version"]),
|
||||||
|
min_python_version=Version(data["min_python_version"]),
|
||||||
|
tags=tuple(data["tags"]),
|
||||||
|
compatibility_status=CompatibilityStatus(data["compatibility_status"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"repo_name": self.repo_name,
|
||||||
|
"min_bot_version": str(self.min_bot_version),
|
||||||
|
"max_bot_version": str(self.max_bot_version),
|
||||||
|
"min_python_version": str(self.min_python_version),
|
||||||
|
"tags": self.tags,
|
||||||
|
"compatibility_status": self.compatibility_status.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CogSupportDict = Dict[str, CogCompatibilityInfo]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class CompatibilityResults(Mapping[str, CogCompatibilityInfo]):
|
||||||
|
latest_version: Version
|
||||||
|
interpreter_version: Version
|
||||||
|
|
||||||
|
explicitly_supported: CogSupportDict = dataclasses.field(default_factory=dict)
|
||||||
|
potentially_supported: CogSupportDict = dataclasses.field(default_factory=dict)
|
||||||
|
incompatible_python_version: CogSupportDict = dataclasses.field(default_factory=dict)
|
||||||
|
incompatible_bot_version: CogSupportDict = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
latest_version=Version(data["latest_version"]),
|
||||||
|
interpreter_version=Version(data["interpreter_version"]),
|
||||||
|
explicitly_supported={
|
||||||
|
cog_name: CogCompatibilityInfo.from_json_dict(info_data)
|
||||||
|
for cog_name, info_data in data["explicitly_supported"].items()
|
||||||
|
},
|
||||||
|
potentially_supported={
|
||||||
|
cog_name: CogCompatibilityInfo.from_json_dict(info_data)
|
||||||
|
for cog_name, info_data in data["potentially_supported"].items()
|
||||||
|
},
|
||||||
|
incompatible_python_version={
|
||||||
|
cog_name: CogCompatibilityInfo.from_json_dict(info_data)
|
||||||
|
for cog_name, info_data in data["incompatible_python_version"].items()
|
||||||
|
},
|
||||||
|
incompatible_bot_version={
|
||||||
|
cog_name: CogCompatibilityInfo.from_json_dict(info_data)
|
||||||
|
for cog_name, info_data in data["incompatible_bot_version"].items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"latest_version": str(self.latest_version),
|
||||||
|
"interpreter_version": str(self.interpreter_version),
|
||||||
|
"explicitly_supported": {
|
||||||
|
cog_name: info.to_json_dict()
|
||||||
|
for cog_name, info in self.explicitly_supported.items()
|
||||||
|
},
|
||||||
|
"potentially_supported": {
|
||||||
|
cog_name: info.to_json_dict()
|
||||||
|
for cog_name, info in self.potentially_supported.items()
|
||||||
|
},
|
||||||
|
"incompatible_python_version": {
|
||||||
|
cog_name: info.to_json_dict()
|
||||||
|
for cog_name, info in self.incompatible_python_version.items()
|
||||||
|
},
|
||||||
|
"incompatible_bot_version": {
|
||||||
|
cog_name: info.to_json_dict()
|
||||||
|
for cog_name, info in self.incompatible_bot_version.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> CogCompatibilityInfo:
|
||||||
|
for data in (
|
||||||
|
self.explicitly_supported,
|
||||||
|
self.potentially_supported,
|
||||||
|
self.incompatible_python_version,
|
||||||
|
self.incompatible_bot_version,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return data[key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return itertools.chain(
|
||||||
|
self.explicitly_supported.keys(),
|
||||||
|
self.potentially_supported.keys(),
|
||||||
|
self.incompatible_python_version.keys(),
|
||||||
|
self.incompatible_bot_version.keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
count = 0
|
||||||
|
for data in (
|
||||||
|
self.explicitly_supported,
|
||||||
|
self.potentially_supported,
|
||||||
|
self.incompatible_python_version,
|
||||||
|
self.incompatible_bot_version,
|
||||||
|
):
|
||||||
|
count += len(data)
|
||||||
|
return count
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return any(
|
||||||
|
(
|
||||||
|
self.explicitly_supported,
|
||||||
|
self.potentially_supported,
|
||||||
|
self.incompatible_python_version,
|
||||||
|
self.incompatible_bot_version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def print(self) -> None:
|
||||||
|
major_version = Text(f"{self.latest_version.major}.{self.latest_version.minor}")
|
||||||
|
if self.explicitly_supported:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_SUCCESS,
|
||||||
|
"The following cogs are explicitly marked as supporting Red ",
|
||||||
|
major_version,
|
||||||
|
":\n",
|
||||||
|
Text(", ").join(Text(cog, style="bold") for cog in self.explicitly_supported),
|
||||||
|
)
|
||||||
|
if self.potentially_supported:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_WARN,
|
||||||
|
"The following cogs may support Red ",
|
||||||
|
major_version,
|
||||||
|
" but they haven't been explicitly marked as such:\n",
|
||||||
|
Text(", ").join(Text(cog, style="bold") for cog in self.potentially_supported),
|
||||||
|
)
|
||||||
|
if self.incompatible_bot_version:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"The following cogs do not support Red ",
|
||||||
|
Text(str(self.latest_version)),
|
||||||
|
":\n",
|
||||||
|
Text(", ").join(Text(cog, style="bold") for cog in self.incompatible_bot_version),
|
||||||
|
)
|
||||||
|
if self.incompatible_python_version:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"The following cogs do not support Python ",
|
||||||
|
Text(str(self.interpreter_version)),
|
||||||
|
":\n",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(cog, style="bold") for cog in self.incompatible_python_version
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not self.explicitly_supported and (
|
||||||
|
self.potentially_supported
|
||||||
|
or self.incompatible_bot_version
|
||||||
|
or self.incompatible_python_version
|
||||||
|
):
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"None of the checked cogs were explicitly marked as supporting Red ",
|
||||||
|
major_version,
|
||||||
|
".",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class CompatibilitySummary:
|
||||||
|
instance_name: str
|
||||||
|
before_update: CompatibilityResults
|
||||||
|
after_update: CompatibilityResults
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
instance_name=data["instance_name"],
|
||||||
|
before_update=CompatibilityResults.from_json_dict(data["before_update"]),
|
||||||
|
after_update=CompatibilityResults.from_json_dict(data["after_update"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"instance_name": self.instance_name,
|
||||||
|
"before_update": self.before_update.to_json_dict(),
|
||||||
|
"after_update": self.after_update.to_json_dict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CogCompatibilityChecker:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bot: Red,
|
||||||
|
*,
|
||||||
|
latest_version: Version,
|
||||||
|
interpreter_version: Version,
|
||||||
|
ignore_prefix: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
self.latest_version = latest_version
|
||||||
|
self.interpreter_version = interpreter_version
|
||||||
|
self.ignore_prefix = ignore_prefix
|
||||||
|
self._console = common.get_console(stderr=True)
|
||||||
|
self._stdout_console = common.get_console()
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def current_version(self) -> Version:
|
||||||
|
return common.get_current_red_version()
|
||||||
|
|
||||||
|
async def check(self) -> CompatibilitySummary:
|
||||||
|
instance_name = data_manager.instance_name()
|
||||||
|
if not self.ignore_prefix:
|
||||||
|
last_known_prefix = await self.bot._config.last_system_info.python_prefix()
|
||||||
|
same_install = False
|
||||||
|
if last_known_prefix is not None:
|
||||||
|
try:
|
||||||
|
same_install = os.path.samefile(last_known_prefix, sys.prefix)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if not same_install:
|
||||||
|
raise InstanceSitePrefixMismatchError(instance_name, last_known_prefix)
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Started checking cog compatibility for the ",
|
||||||
|
Text(instance_name, style="bold"),
|
||||||
|
" instance.",
|
||||||
|
console=self._console,
|
||||||
|
)
|
||||||
|
status = Text.assemble(
|
||||||
|
"Checking compatibility of cogs installed on the ",
|
||||||
|
(instance_name, "bold"),
|
||||||
|
" instance...",
|
||||||
|
)
|
||||||
|
with self._console.status(status):
|
||||||
|
await _downloader._init_without_bot(self.bot._cog_mgr)
|
||||||
|
|
||||||
|
await self._update_repos()
|
||||||
|
|
||||||
|
installed_cogs = await _downloader.installed_cogs()
|
||||||
|
repo_unknown = []
|
||||||
|
to_check = set()
|
||||||
|
|
||||||
|
for cog in installed_cogs:
|
||||||
|
if cog.repo is None:
|
||||||
|
repo_unknown.append(cog)
|
||||||
|
else:
|
||||||
|
to_check.add(cog)
|
||||||
|
|
||||||
|
with self._console.status("Checking available cog updates..."):
|
||||||
|
update_check_result = await _downloader.check_cog_updates(
|
||||||
|
cogs=to_check,
|
||||||
|
update_repos=False,
|
||||||
|
env=_downloader.Environment(
|
||||||
|
red_version=self.latest_version, python_version=self.interpreter_version
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._console.print("Available cog updates checked.")
|
||||||
|
|
||||||
|
summary = CompatibilitySummary(
|
||||||
|
instance_name=instance_name,
|
||||||
|
before_update=self._evaluate_before_update_compatibility(to_check),
|
||||||
|
after_update=self._evaluate_after_update_compatibility(
|
||||||
|
to_check, update_check_result
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Finished checking cog compatibility for the ",
|
||||||
|
Text(instance_name, style="bold"),
|
||||||
|
" instance.",
|
||||||
|
console=self._console,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._stdout_console.print()
|
||||||
|
|
||||||
|
# Note that when a cog can be updated
|
||||||
|
# and its up-to-date version does not support the Red version we're updating to,
|
||||||
|
# we don't check whether currently installed version of the cog supports that Red version.
|
||||||
|
# This is intentional - we want to allow cog creators to mark something incompatible
|
||||||
|
# after the fact.
|
||||||
|
summary.after_update.print()
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
async def _update_repos(self) -> None:
|
||||||
|
with detailed_progress(unit="repos", console=self._console) as progress:
|
||||||
|
task_id = progress.add_task(
|
||||||
|
"Updating repos", total=len(_downloader._repo_manager.repos)
|
||||||
|
)
|
||||||
|
updated_count = 0
|
||||||
|
already_up_to_date_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
for repo in _downloader._repo_manager.repos:
|
||||||
|
progress.update(task_id, description=f"Updating {repo.name!r} repo")
|
||||||
|
try:
|
||||||
|
old, new = await repo.update()
|
||||||
|
except _downloader.errors.UpdateError:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_WARN,
|
||||||
|
"Could not update repo ",
|
||||||
|
Text(repo.name, style="bold"),
|
||||||
|
", the results for cogs from it may be inaccurate.",
|
||||||
|
console=self._console,
|
||||||
|
)
|
||||||
|
failed_count += 1
|
||||||
|
else:
|
||||||
|
if old != new:
|
||||||
|
updated_count += 1
|
||||||
|
self._console.print("Updated repo", Text(repo.name, style="bold"))
|
||||||
|
else:
|
||||||
|
already_up_to_date_count += 1
|
||||||
|
self._console.print(
|
||||||
|
"Repo", Text(repo.name, style="bold"), "is already up-to-date."
|
||||||
|
)
|
||||||
|
progress.advance(task_id)
|
||||||
|
|
||||||
|
self._stdout_console.print(
|
||||||
|
f"Successfully updated {updated_count} repos, failed to update {failed_count} repos.\n"
|
||||||
|
f"{already_up_to_date_count} repos were already up-to-date.",
|
||||||
|
highlight=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fill_compatibility_results(
|
||||||
|
self, results: CompatibilityResults, cogs: Iterable[_downloader.Installable]
|
||||||
|
) -> None:
|
||||||
|
latest_version = self.latest_version
|
||||||
|
interpreter_version = self.interpreter_version
|
||||||
|
breaking_update = self.current_version.release[:2] != self.latest_version.release[:2]
|
||||||
|
|
||||||
|
for cog in cogs:
|
||||||
|
info = CogCompatibilityInfo.from_installable(cog)
|
||||||
|
if cog.min_python_version > interpreter_version:
|
||||||
|
info.compatibility_status = CompatibilityStatus.UNSUPPORTED_PYTHON_VERSION
|
||||||
|
results.incompatible_python_version[cog.name] = info
|
||||||
|
elif cog.min_bot_version > latest_version 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 < latest_version
|
||||||
|
):
|
||||||
|
info.compatibility_status = CompatibilityStatus.UNSUPPORTED_BOT_VERSION
|
||||||
|
results.incompatible_bot_version[cog.name] = info
|
||||||
|
elif not breaking_update:
|
||||||
|
info.compatibility_status = CompatibilityStatus.EXPLICITLY_SUPPORTED_NON_BREAKING
|
||||||
|
results.explicitly_supported[cog.name] = info
|
||||||
|
elif latest_version.release[:2] == cog.min_bot_version.release[:2]:
|
||||||
|
# If cog creator explicitly set min_bot_version to 3.x.y,
|
||||||
|
# then 3.x is explicitly supported.
|
||||||
|
info.compatibility_status = (
|
||||||
|
CompatibilityStatus.EXPLICITLY_SUPPORTED_MIN_BOT_VERSION
|
||||||
|
)
|
||||||
|
results.explicitly_supported[cog.name] = info
|
||||||
|
elif latest_version.release[:2] == cog.max_bot_version.release[:2]:
|
||||||
|
# If cog creator explicitly set max_bot_version to 3.x.y,
|
||||||
|
# then 3.x is explicitly supported.
|
||||||
|
info.compatibility_status = (
|
||||||
|
CompatibilityStatus.EXPLICITLY_SUPPORTED_MAX_BOT_VERSION
|
||||||
|
)
|
||||||
|
results.explicitly_supported[cog.name] = info
|
||||||
|
elif f"red-{latest_version.major}-{latest_version.minor}-ready" in cog.tags:
|
||||||
|
# If cog creator explicitly added a "red-3.x-ready" tag,
|
||||||
|
# then 3.x is explicitly supported.
|
||||||
|
# This is similar to the meaning of "Programming Language :: Python :: 3.x"
|
||||||
|
# classifiers in Python packaging.
|
||||||
|
info.compatibility_status = CompatibilityStatus.EXPLICITLY_SUPPORTED_READY_TAG
|
||||||
|
results.explicitly_supported[cog.name] = info
|
||||||
|
else:
|
||||||
|
# If we don't have any explicit signals from the cog's metadata that
|
||||||
|
# Red 3.x is supported, the cog is only *potentially* supported by that version.
|
||||||
|
info.compatibility_status = CompatibilityStatus.POTENTIALLY_SUPPORTED
|
||||||
|
results.potentially_supported[cog.name] = info
|
||||||
|
|
||||||
|
def _evaluate_before_update_compatibility(
|
||||||
|
self, to_check: Iterable[_downloader.Installable]
|
||||||
|
) -> CompatibilityResults:
|
||||||
|
results = CompatibilityResults(
|
||||||
|
latest_version=self.latest_version, interpreter_version=self.interpreter_version
|
||||||
|
)
|
||||||
|
|
||||||
|
self._fill_compatibility_results(results, to_check)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _evaluate_after_update_compatibility(
|
||||||
|
self,
|
||||||
|
to_check: Iterable[_downloader.Installable],
|
||||||
|
update_check_result: _downloader.CogUpdateCheckResult,
|
||||||
|
) -> CompatibilityResults:
|
||||||
|
not_updatable = set(to_check)
|
||||||
|
results = CompatibilityResults(
|
||||||
|
latest_version=self.latest_version, interpreter_version=self.interpreter_version
|
||||||
|
)
|
||||||
|
|
||||||
|
not_updatable.difference_update(update_check_result.incompatible_python_version)
|
||||||
|
not_updatable.difference_update(update_check_result.incompatible_bot_version)
|
||||||
|
not_updatable.difference_update(update_check_result.updatable_cogs)
|
||||||
|
|
||||||
|
self._fill_compatibility_results(results, update_check_result.incompatible_python_version)
|
||||||
|
self._fill_compatibility_results(results, update_check_result.incompatible_bot_version)
|
||||||
|
self._fill_compatibility_results(results, update_check_result.updatable_cogs)
|
||||||
|
|
||||||
|
# not_updatable should now only have cogs that were not updateable. Those cogs
|
||||||
|
# are filled based on metadata of the currently installed ("before update") version.
|
||||||
|
self._fill_compatibility_results(results, not_updatable)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def check_instance(
|
||||||
|
instance: str,
|
||||||
|
*,
|
||||||
|
latest_version: Version,
|
||||||
|
interpreter_version: Version,
|
||||||
|
ignore_prefix: bool = False,
|
||||||
|
) -> CompatibilitySummary:
|
||||||
|
data_manager.load_basic_configuration(instance)
|
||||||
|
red = Red(cli_flags=parse_cli_flags([instance]))
|
||||||
|
driver_cls = _drivers.get_driver_class()
|
||||||
|
await driver_cls.initialize(**data_manager.storage_details())
|
||||||
|
try:
|
||||||
|
checker = CogCompatibilityChecker(
|
||||||
|
red,
|
||||||
|
latest_version=latest_version,
|
||||||
|
interpreter_version=interpreter_version,
|
||||||
|
ignore_prefix=ignore_prefix,
|
||||||
|
)
|
||||||
|
return await checker.check()
|
||||||
|
finally:
|
||||||
|
await driver_cls.teardown()
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from operator import itemgetter
|
||||||
|
from typing import Any, Final, Iterable, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import click
|
||||||
|
import rich
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
from packaging.version import Version
|
||||||
|
from python_discovery import PythonInfo, get_interpreter
|
||||||
|
from rich.console import Console, RenderableType
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from redbot import __version__
|
||||||
|
from redbot.core.utils._internal_utils import (
|
||||||
|
cli_level_to_log_level,
|
||||||
|
get_installed_extras,
|
||||||
|
log_level_to_cli_level,
|
||||||
|
)
|
||||||
|
from redbot.core import data_manager
|
||||||
|
|
||||||
|
_instance_data = data_manager.load_existing_config()
|
||||||
|
INSTANCE_LIST: Final = () if _instance_data is None else tuple(_instance_data.keys())
|
||||||
|
|
||||||
|
|
||||||
|
ICON_SUCCESS = "[green]:white_heavy_check_mark-emoji:[/]"
|
||||||
|
ICON_INFO = "[blue]:information-emoji:[/]"
|
||||||
|
ICON_WARN = "[yellow]:warning-emoji:[/]"
|
||||||
|
ICON_ERROR = "[red]:cross_mark-emoji:[/]"
|
||||||
|
|
||||||
|
INTERNAL_LEGACY_WINDOWS_ENV_VAR = "_RED_UPDATE_INTERNAL_LEGACY_WINDOWS"
|
||||||
|
INTERNAL_UPDATER_METADATA_ENV_VAR = "_RED_UPDATE_INTERNAL_UPDATER_METADATA"
|
||||||
|
_STDERR_CONSOLE: Optional[Console] = None
|
||||||
|
|
||||||
|
RUNNER_DIR_ENV_VAR: Final = "REDBOT_UPDATE_RUNNER_DIR"
|
||||||
|
RUNNER_WRAPPER_EXE_ENV_VAR: Final = "REDBOT_UPDATE_RUNNER_WRAPPER_EXE"
|
||||||
|
|
||||||
|
OLD_VENV_BACKUP_DIR_NAME: Final = "redbot-update-old-venv-backup"
|
||||||
|
|
||||||
|
|
||||||
|
def get_red_dependency_specifier(version: Version, extras: Iterable[str]) -> str:
|
||||||
|
specifier_template = (
|
||||||
|
os.getenv("_RED_UPDATE_PRETEND_SPECIFIER_TEMPLATE")
|
||||||
|
or "Red-DiscordBot {extras} {versionspec}"
|
||||||
|
)
|
||||||
|
joined_extras = ",".join(extras)
|
||||||
|
return specifier_template.format(
|
||||||
|
extras=f"[{joined_extras}]" if joined_extras else "",
|
||||||
|
versionspec=f"=={version}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_red_version() -> Version:
|
||||||
|
return Version(os.getenv("_RED_UPDATE_PRETEND_VERSION") or __version__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_python_version() -> Version:
|
||||||
|
return Version(".".join(map(str, sys.version_info[:3])))
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_column(prefix: RenderableType, *parts: Union[str, Text]) -> Table:
|
||||||
|
output = Table.grid(padding=(0, 2))
|
||||||
|
output.add_column()
|
||||||
|
output.add_column()
|
||||||
|
text = Text()
|
||||||
|
for renderable in parts:
|
||||||
|
if isinstance(renderable, str):
|
||||||
|
text.append_text(Text.from_markup(renderable))
|
||||||
|
else:
|
||||||
|
text.append_text(renderable)
|
||||||
|
output.add_row(prefix, text)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def print_with_prefix_column(
|
||||||
|
prefix: RenderableType, *parts: Union[str, Text], console: Optional[Console] = None
|
||||||
|
) -> None:
|
||||||
|
if console is None:
|
||||||
|
console = rich.get_console()
|
||||||
|
console.print(prefix_column(prefix, *parts))
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_legacy_windows_workaround() -> None:
|
||||||
|
# Rich does not properly support printing to stderr, when stdout is redirected...
|
||||||
|
# This monkeypatch should be enough to workaround this for our purposes.
|
||||||
|
# https://github.com/Textualize/rich/issues/4071
|
||||||
|
if sys.platform == "win32" and not sys.stdout.isatty():
|
||||||
|
import rich._win32_console
|
||||||
|
|
||||||
|
rich._win32_console.STDOUT = -12
|
||||||
|
|
||||||
|
|
||||||
|
def configure_rich() -> None:
|
||||||
|
_apply_legacy_windows_workaround()
|
||||||
|
value = os.getenv(INTERNAL_LEGACY_WINDOWS_ENV_VAR, "")
|
||||||
|
legacy_windows = int(value) if value else None
|
||||||
|
rich.reconfigure(highlight=False, legacy_windows=legacy_windows)
|
||||||
|
global _STDERR_CONSOLE
|
||||||
|
_STDERR_CONSOLE = Console(highlight=False, stderr=True, legacy_windows=legacy_windows)
|
||||||
|
|
||||||
|
|
||||||
|
def get_console(stderr: bool = False) -> Console:
|
||||||
|
global _STDERR_CONSOLE
|
||||||
|
if _STDERR_CONSOLE is None:
|
||||||
|
raise RuntimeError("_STDERR_CONSOLE is not set")
|
||||||
|
return _STDERR_CONSOLE if stderr else rich.get_console()
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(logging_level: int) -> None:
|
||||||
|
configure_rich()
|
||||||
|
level = cli_level_to_log_level(logging_level)
|
||||||
|
base_logger = logging.getLogger("red")
|
||||||
|
base_logger.setLevel(level)
|
||||||
|
base_logger.addHandler(RichHandler(console=get_console(stderr=True), show_path=False))
|
||||||
|
|
||||||
|
|
||||||
|
def get_logging_level() -> int:
|
||||||
|
return logging.getLogger("red").level
|
||||||
|
|
||||||
|
|
||||||
|
def get_log_cli_level() -> int:
|
||||||
|
return log_level_to_cli_level(logging.getLogger("red").level)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_supported_env() -> None:
|
||||||
|
if sys.prefix == sys.base_prefix:
|
||||||
|
print("redbot-update cannot be used when Red is installed outside a virtual environment.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
if not (
|
||||||
|
os.environ.get(RUNNER_DIR_ENV_VAR, "") and os.environ.get(RUNNER_WRAPPER_EXE_ENV_VAR, "")
|
||||||
|
):
|
||||||
|
print("redbot-update was called incorrectly.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_system_interpreters(
|
||||||
|
requires_python: SpecifierSet,
|
||||||
|
) -> List[Tuple[str, Version, PythonInfo]]:
|
||||||
|
interpreters = {}
|
||||||
|
|
||||||
|
def _append_interpreter(info: PythonInfo) -> Literal[False]:
|
||||||
|
version = Version(info.version_str)
|
||||||
|
if version in requires_python:
|
||||||
|
# realpath call is needed because get_interpreter lists
|
||||||
|
# /usr/bin and /bin as separate even though they're the same path
|
||||||
|
interpreters[os.path.realpath(info.executable)] = (version, info)
|
||||||
|
return False
|
||||||
|
|
||||||
|
get_interpreter("cpython", predicate=_append_interpreter)
|
||||||
|
|
||||||
|
ret = [(key, *value) for key, value in interpreters.items()]
|
||||||
|
ret.sort(key=itemgetter(1), reverse=True)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def search_for_interpreters(
|
||||||
|
requires_python: SpecifierSet,
|
||||||
|
) -> List[Tuple[str, Version, PythonInfo]]:
|
||||||
|
console = get_console()
|
||||||
|
with console.status("Searching for compatible Python interpreters on your system..."):
|
||||||
|
interpreters = _get_system_interpreters(requires_python)
|
||||||
|
|
||||||
|
if not interpreters:
|
||||||
|
url = "https://docs.discord.red/en/stable/install_guides/"
|
||||||
|
console.print(
|
||||||
|
f"{ICON_ERROR} Could not find a compatible Python interpreter!\n"
|
||||||
|
'Please follow the steps from the "Installing the pre-requirements" section'
|
||||||
|
" of the install guide for your system:"
|
||||||
|
)
|
||||||
|
console.print(Text(url, style=f"link {url}"))
|
||||||
|
console.print("Once you finish installing the pre-requirements, run this command again.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
return interpreters
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedEnum(enum.Enum):
|
||||||
|
def __ge__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.value >= other.value
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __gt__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.value > other.value
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __le__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.value <= other.value
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other: Any) -> bool:
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
return self.value < other.value
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class VersionParamType(click.ParamType):
|
||||||
|
name = "version"
|
||||||
|
|
||||||
|
def convert(
|
||||||
|
self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
|
||||||
|
) -> Version:
|
||||||
|
if isinstance(value, Version):
|
||||||
|
if len(value.release) < 2:
|
||||||
|
self.fail(
|
||||||
|
f"{value!r} needs to have at least 2 release components (major and minor).",
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.convert(Version(value), param, ctx)
|
||||||
|
except ValueError:
|
||||||
|
self.fail(f"{value!r} is not a valid version number", param, ctx)
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.prompt import Confirm
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from redbot import __version__
|
||||||
|
from redbot.core import _downloader, _drivers, data_manager
|
||||||
|
from redbot.core._cli import asyncio_run, parse_cli_flags
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
from . import changelog, cmd, common, runner
|
||||||
|
from .updater import UpdaterMetadata, get_updater_metadata
|
||||||
|
|
||||||
|
|
||||||
|
FINISH_UPDATE_CMD_NAME = "finish-update"
|
||||||
|
_UPDATE_COGS_CMD_NAME = "update-cogs"
|
||||||
|
_UPDATE_REPOS_OPTION_NAME = "--update-repos"
|
||||||
|
_EXIT_INSTANCE_SITE_PREFIX_MISMATCH = 4
|
||||||
|
_EXIT_INSTANCE_BACKEND_UNSUPPORTED = 5
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.option(cmd.arg_names.DEBUG, "logging_level", count=True)
|
||||||
|
def cli(logging_level: int) -> None:
|
||||||
|
common.ensure_supported_env()
|
||||||
|
common.configure_logging(logging_level)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(_UPDATE_COGS_CMD_NAME)
|
||||||
|
@click.argument("instance_name")
|
||||||
|
@click.option(_UPDATE_REPOS_OPTION_NAME, default=False, is_flag=True)
|
||||||
|
def update_cogs(instance_name: str, update_repos: bool) -> None:
|
||||||
|
asyncio.run(_update_cogs(instance_name, update_repos))
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_cogs(instance: str, update_repos: bool) -> None:
|
||||||
|
data_manager.load_basic_configuration(instance)
|
||||||
|
red = Red(cli_flags=parse_cli_flags([instance]))
|
||||||
|
driver_cls = _drivers.get_driver_class()
|
||||||
|
await driver_cls.initialize(**data_manager.storage_details())
|
||||||
|
try:
|
||||||
|
await _run_cog_update(red, update_repos=update_repos)
|
||||||
|
except _drivers.MissingExtraRequirements:
|
||||||
|
raise SystemExit(_EXIT_INSTANCE_BACKEND_UNSUPPORTED)
|
||||||
|
finally:
|
||||||
|
await driver_cls.teardown()
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_cog_update(bot: Red, *, update_repos: bool) -> None:
|
||||||
|
stdout_console = common.get_console()
|
||||||
|
console = common.get_console(stderr=True)
|
||||||
|
|
||||||
|
instance_name = data_manager.instance_name()
|
||||||
|
last_known_prefix = await bot._config.last_system_info.python_prefix()
|
||||||
|
same_install = False
|
||||||
|
if last_known_prefix is not None:
|
||||||
|
try:
|
||||||
|
same_install = os.path.samefile(last_known_prefix, sys.prefix)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if not same_install:
|
||||||
|
raise SystemExit(_EXIT_INSTANCE_SITE_PREFIX_MISMATCH)
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Started updating cogs for the ",
|
||||||
|
Text(instance_name, style="bold"),
|
||||||
|
" instance.",
|
||||||
|
console=console,
|
||||||
|
)
|
||||||
|
status = Text.assemble(
|
||||||
|
"Update cogs installed on the ", (instance_name, "bold"), " instance..."
|
||||||
|
)
|
||||||
|
with console.status(status):
|
||||||
|
await _downloader._init_without_bot(bot._cog_mgr)
|
||||||
|
result = await _downloader.update_cogs(update_repos=update_repos)
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Finished updating cogs for the ",
|
||||||
|
Text(instance_name, style="bold"),
|
||||||
|
" instance.",
|
||||||
|
console=console,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.checked_cogs:
|
||||||
|
stdout_console.print("There were no cogs to check.")
|
||||||
|
return
|
||||||
|
if not result.updates_available:
|
||||||
|
stdout_console.print("All installed cogs are already up to date.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_cog_versions_map = {cog.name: cog for cog in result.checked_cogs}
|
||||||
|
if result.failed_reqs:
|
||||||
|
console.print(
|
||||||
|
"Failed to install requirements:",
|
||||||
|
Text(", ").join(Text(req, style="bold") for req in result.failed_reqs),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
message = Text("Cog update completed successfully.")
|
||||||
|
|
||||||
|
if result.updated_cogs:
|
||||||
|
cogs_with_changed_eud_statement = set()
|
||||||
|
for cog in result.updated_cogs:
|
||||||
|
current_eud_statement = current_cog_versions_map[cog.name].end_user_data_statement
|
||||||
|
if current_eud_statement != cog.end_user_data_statement:
|
||||||
|
cogs_with_changed_eud_statement.add(cog.name)
|
||||||
|
message.append("\nUpdated: ")
|
||||||
|
message.append_text(
|
||||||
|
Text(", ").join(Text(cog.name, style="bold") for cog in result.updated_cogs)
|
||||||
|
)
|
||||||
|
if cogs_with_changed_eud_statement:
|
||||||
|
message.append("\nEnd user data statements of these cogs have changed: ")
|
||||||
|
message.append_text(
|
||||||
|
Text(", ").join(
|
||||||
|
Text(cog_name, style="bold") for cog_name in cogs_with_changed_eud_statement
|
||||||
|
)
|
||||||
|
)
|
||||||
|
message.append("\nYou can use ")
|
||||||
|
message.append("[p]cog info <repo> <cog>", style="bold")
|
||||||
|
message.append(" to see the updated statements.\n")
|
||||||
|
# If the bot has any slash commands enabled, warn them to sync
|
||||||
|
enabled_slash = await bot.list_enabled_app_commands()
|
||||||
|
if any(enabled_slash.values()):
|
||||||
|
message.append("\nYou may need to resync your slash commands with ")
|
||||||
|
message.append("[p]slash sync")
|
||||||
|
message.append(".")
|
||||||
|
if result.failed_cogs:
|
||||||
|
message.append("\nFailed to update cogs: ")
|
||||||
|
message.append_text(
|
||||||
|
Text(", ").join(Text(cog.name, style="bold") for cog in result.failed_cogs)
|
||||||
|
)
|
||||||
|
if not result.outdated_cogs:
|
||||||
|
message = Text("No cogs were updated.")
|
||||||
|
if result.failed_libs:
|
||||||
|
message.append("\nFailed to install shared libraries: ")
|
||||||
|
message.append_text(
|
||||||
|
Text(", ").join(Text(lib.name, style="bold") for lib in result.failed_libs)
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout_console.print(message)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(FINISH_UPDATE_CMD_NAME)
|
||||||
|
def finish_update() -> None:
|
||||||
|
"""
|
||||||
|
Entrypoint for finishing up the update that runs with the new version of Red.
|
||||||
|
"""
|
||||||
|
asyncio_run(_finish_update())
|
||||||
|
|
||||||
|
|
||||||
|
async def _finish_update() -> None:
|
||||||
|
assert runner.get_request_output().request_type is runner.RequestType.exec
|
||||||
|
updater_metadata = get_updater_metadata()
|
||||||
|
console = common.get_console()
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
if updater_metadata.options.interactive and not updater_metadata.options.update_cogs:
|
||||||
|
msg = Text("It is highly recommended to update 3rd-party cogs after updating Red")
|
||||||
|
if updater_metadata.breaking_update:
|
||||||
|
msg.append(", especially after a major update")
|
||||||
|
msg.append(".")
|
||||||
|
console.print(msg)
|
||||||
|
|
||||||
|
cog_compatibility = updater_metadata.cog_compatibility
|
||||||
|
if cog_compatibility is not None:
|
||||||
|
unsupported_cogs = set()
|
||||||
|
cogs_with_improved_compatibility = set()
|
||||||
|
unaffected_cogs = set()
|
||||||
|
for summary in cog_compatibility.checked.values():
|
||||||
|
for before in summary.before_update.values():
|
||||||
|
cog_name = before.name
|
||||||
|
after = summary.after_update[cog_name]
|
||||||
|
if after.compatibility_status.unsupported:
|
||||||
|
unsupported_cogs.add(cog_name)
|
||||||
|
elif after.compatibility_status.explicitly_supported:
|
||||||
|
if before.compatibility_status.explicitly_supported:
|
||||||
|
unaffected_cogs.add(cog_name)
|
||||||
|
else:
|
||||||
|
cogs_with_improved_compatibility.add(cog_name)
|
||||||
|
elif before.compatibility_status.unsupported:
|
||||||
|
cogs_with_improved_compatibility.add(cog_name)
|
||||||
|
else:
|
||||||
|
unaffected_cogs.add(cog_name)
|
||||||
|
|
||||||
|
if cogs_with_improved_compatibility:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Updating will improve compatibility of ",
|
||||||
|
Text(str(len(cogs_with_improved_compatibility)), style="bold"),
|
||||||
|
" cogs.",
|
||||||
|
)
|
||||||
|
if unsupported_cogs:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_WARN,
|
||||||
|
Text(str(len(unsupported_cogs)), style="bold"),
|
||||||
|
" cogs will remain unsupported after updating:\n",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(cog_name, style="bold") for cog_name in sorted(unsupported_cogs)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
update_cogs = updater_metadata.options.update_cogs
|
||||||
|
if update_cogs is None:
|
||||||
|
if updater_metadata.options.interactive:
|
||||||
|
update_cogs = Confirm.ask("Do you want to update all your cogs?", default=True)
|
||||||
|
else:
|
||||||
|
update_cogs = True
|
||||||
|
if update_cogs:
|
||||||
|
await _handle_cog_updates(updater_metadata)
|
||||||
|
|
||||||
|
with console.status("Cleaning up..."):
|
||||||
|
backup_dir = Path(sys.prefix) / common.OLD_VENV_BACKUP_DIR_NAME
|
||||||
|
shutil.rmtree(backup_dir)
|
||||||
|
|
||||||
|
changelog_markdown = changelog.render_markdown(updater_metadata.changelogs)
|
||||||
|
if changelog_markdown:
|
||||||
|
console.print(Panel(Markdown(changelog_markdown)))
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_SUCCESS,
|
||||||
|
"Update to Red ",
|
||||||
|
Text(__version__, style="bold"),
|
||||||
|
" has been finished!",
|
||||||
|
)
|
||||||
|
|
||||||
|
if changelog_markdown:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
'Remember to follow instructions from the "Read before updating" section,'
|
||||||
|
" if any were provided.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if updater_metadata.backup_dir:
|
||||||
|
additional_text = ""
|
||||||
|
if not updater_metadata.options.backup_dir:
|
||||||
|
additional_text = (
|
||||||
|
"\nNote that this is a temporary directory and may eventually get auto-removed"
|
||||||
|
" by your system."
|
||||||
|
)
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"If needed, you can find the backups of the virtual environment"
|
||||||
|
" and the instances at: ",
|
||||||
|
Text(str(updater_metadata.backup_dir), style="bold"),
|
||||||
|
additional_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_cog_updates(updater_metadata: UpdaterMetadata) -> None:
|
||||||
|
cog_compatibility = updater_metadata.cog_compatibility
|
||||||
|
console = common.get_console()
|
||||||
|
|
||||||
|
instances = (
|
||||||
|
list(cog_compatibility.checked)
|
||||||
|
if cog_compatibility is not None
|
||||||
|
else updater_metadata.options.instances
|
||||||
|
)
|
||||||
|
checked_instances = {}
|
||||||
|
failed_instances = []
|
||||||
|
unsupported_storage_instances = []
|
||||||
|
for instance_name in instances:
|
||||||
|
if instance_name in updater_metadata.options.excluded_instances:
|
||||||
|
continue
|
||||||
|
exit_code, stdout = await _call_cog_update(
|
||||||
|
instance_name, update_repos=cog_compatibility is None
|
||||||
|
)
|
||||||
|
if exit_code == _EXIT_INSTANCE_BACKEND_UNSUPPORTED:
|
||||||
|
unsupported_storage_instances.append(instance_name)
|
||||||
|
elif exit_code == _EXIT_INSTANCE_SITE_PREFIX_MISMATCH:
|
||||||
|
pass
|
||||||
|
elif exit_code:
|
||||||
|
failed_instances.append(instance_name)
|
||||||
|
print(stdout, end="")
|
||||||
|
Text.assemble(
|
||||||
|
"\N{UPWARDS ARROW} " * 3, "Failure for ", (instance_name, "bold"), " instance"
|
||||||
|
)
|
||||||
|
console.rule(
|
||||||
|
Text.assemble(
|
||||||
|
"\N{UPWARDS ARROW} " * 3,
|
||||||
|
"Failure for ",
|
||||||
|
(instance_name, "bold"),
|
||||||
|
" instance above",
|
||||||
|
" \N{UPWARDS ARROW}" * 3,
|
||||||
|
),
|
||||||
|
style="red",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
checked_instances[instance_name] = stdout
|
||||||
|
if stdout:
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
if checked_instances:
|
||||||
|
for instance_name, stdout in checked_instances.items():
|
||||||
|
console.rule(Text(instance_name, style="bold"))
|
||||||
|
print(stdout, end="")
|
||||||
|
console.rule()
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Finished updating cogs.",
|
||||||
|
"\nThe results for each instance are shown above." if checked_instances else "",
|
||||||
|
)
|
||||||
|
if failed_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"Failure occurred while trying to perform update for following instances: ",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(instance_name, style="bold") for instance_name in failed_instances
|
||||||
|
),
|
||||||
|
"\nScroll above to find the errors.",
|
||||||
|
)
|
||||||
|
if unsupported_storage_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"The following instances were skipped as they use a storage backend that is"
|
||||||
|
" not supported by the current Red installation (some requirements are missing): ",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(instance_name, style="bold")
|
||||||
|
for instance_name in unsupported_storage_instances
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not checked_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"There were no",
|
||||||
|
(" other" if failed_instances or unsupported_storage_instances else ""),
|
||||||
|
" instances to update cogs for.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _call_cog_update(instance_name: str, *, update_repos: bool) -> Tuple[int, str]:
|
||||||
|
debug_args = (cmd.arg_names.DEBUG,) * common.get_log_cli_level()
|
||||||
|
args = [
|
||||||
|
"-m",
|
||||||
|
"redbot._update.internal",
|
||||||
|
*debug_args,
|
||||||
|
_UPDATE_COGS_CMD_NAME,
|
||||||
|
instance_name,
|
||||||
|
]
|
||||||
|
if update_repos:
|
||||||
|
args.append(_UPDATE_REPOS_OPTION_NAME)
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# terminal woes
|
||||||
|
console = common.get_console()
|
||||||
|
if console.is_terminal:
|
||||||
|
env["TTY_COMPATIBLE"] = "1"
|
||||||
|
# Rich only checks stdout for Windows console features:
|
||||||
|
# https://github.com/Textualize/rich/blob/fc41075a3206d2a5fd846c6f41c4d2becab814fa/rich/_windows.py#L46
|
||||||
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "1" if console.legacy_windows else "0"
|
||||||
|
else:
|
||||||
|
# Rich does not set legacy_windows correctly when is_terminal is False
|
||||||
|
# https://github.com/Textualize/rich/issues/3647
|
||||||
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "0"
|
||||||
|
env["PYTHONIOENCODING"] = sys.stdout.encoding
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable, *args, env=env, stdout=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
stdout_data, _ = await proc.communicate()
|
||||||
|
decoded_stdout = stdout_data.decode()
|
||||||
|
exit_code = await proc.wait()
|
||||||
|
|
||||||
|
return exit_code, decoded_stdout
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("base_executable")
|
||||||
|
@click.argument("venv_dir", type=click.Path(path_type=Path))
|
||||||
|
@click.argument("scripts_path", type=click.Path(path_type=Path))
|
||||||
|
@click.argument("dependency_specifier")
|
||||||
|
def reinstall(
|
||||||
|
base_executable: str, venv_dir: Path, scripts_path: Path, dependency_specifier: str
|
||||||
|
) -> None:
|
||||||
|
assert runner.get_request_output().request_type is runner.RequestType.exec
|
||||||
|
|
||||||
|
console = common.get_console()
|
||||||
|
with console.status("Creating a new virtual environment..."):
|
||||||
|
subprocess.check_call((base_executable, "-m", "venv", str(venv_dir)))
|
||||||
|
console.print("Created a new virtual environment.")
|
||||||
|
executable = str(scripts_path / f"python{sysconfig.get_config_var('EXE')}")
|
||||||
|
|
||||||
|
common.print_with_prefix_column(common.ICON_INFO, "Starting the install process...")
|
||||||
|
try:
|
||||||
|
subprocess.check_call((executable, "-m", "pip", "install", "-U", "pip"))
|
||||||
|
subprocess.check_call((executable, "-m", "pip", "install", dependency_specifier))
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
console.print()
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"Failed to install new version of Red.",
|
||||||
|
)
|
||||||
|
status = console.status("Attempting to restore old virtual environment...")
|
||||||
|
status.start()
|
||||||
|
try:
|
||||||
|
_remove_new_venv(venv_dir)
|
||||||
|
except Exception:
|
||||||
|
status.stop()
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR, "Failed to remove newly created virtual environment."
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
try:
|
||||||
|
_restore_old_venv(venv_dir)
|
||||||
|
except Exception:
|
||||||
|
status.stop()
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR, "Failed to restore old virtual environment."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO, "The old virtual environment has been restored."
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
# NOTE: this will run with the updated version of Red
|
||||||
|
runner.make_exec_request(executable, "finish-update")
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_new_venv(venv_dir: Path) -> None:
|
||||||
|
backup_dir = venv_dir / common.OLD_VENV_BACKUP_DIR_NAME
|
||||||
|
wrapper_exe = runner.get_wrapper_executable()
|
||||||
|
|
||||||
|
for path in venv_dir.iterdir():
|
||||||
|
if path == backup_dir or path == wrapper_exe:
|
||||||
|
continue
|
||||||
|
if path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_old_venv(venv_dir: Path) -> None:
|
||||||
|
backup_dir = venv_dir / common.OLD_VENV_BACKUP_DIR_NAME
|
||||||
|
for path in backup_dir.iterdir():
|
||||||
|
path.rename(venv_dir / path.name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import enum
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, ClassVar, Dict, Iterable, NoReturn, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from . import cmd, common
|
||||||
|
|
||||||
|
_RUNNER_DIR = Path(os.environ.get(common.RUNNER_DIR_ENV_VAR, ""))
|
||||||
|
|
||||||
|
|
||||||
|
class RequestType(enum.Enum):
|
||||||
|
exec = "exec"
|
||||||
|
spawn_command = "spawn_command"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class RequestInput:
|
||||||
|
request_type: ClassVar[RequestType]
|
||||||
|
request_new_python_exe: str
|
||||||
|
request_new_start_args: Tuple[str, ...]
|
||||||
|
request_set_env_vars: Dict[str, Optional[str]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class RequestOutput:
|
||||||
|
request_type: RequestType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class ExecRequestInput(RequestInput):
|
||||||
|
request_type: ClassVar = RequestType.exec
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class ExecRequestOutput(RequestOutput):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SpawnProcessRequestInput(RequestInput):
|
||||||
|
request_type: ClassVar = RequestType.spawn_command
|
||||||
|
command: str
|
||||||
|
args: Tuple[str, ...]
|
||||||
|
env: Optional[Dict[str, str]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SpawnProcessRequestOutput(RequestOutput):
|
||||||
|
exit_code: int
|
||||||
|
exited: bool
|
||||||
|
pid: int
|
||||||
|
sys: Any
|
||||||
|
sys_usage: Dict[str, Any]
|
||||||
|
system_time: int
|
||||||
|
user_time: int
|
||||||
|
|
||||||
|
|
||||||
|
def make_request(request: RequestInput) -> NoReturn:
|
||||||
|
with open(_RUNNER_DIR / "request_input.json", "w", encoding="utf-8") as fp:
|
||||||
|
data = dataclasses.asdict(request)
|
||||||
|
data["request_type"] = request.request_type.value
|
||||||
|
json.dump(data, fp)
|
||||||
|
raise SystemExit(3)
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_output() -> Union[ExecRequestOutput, SpawnProcessRequestOutput]:
|
||||||
|
with open(_RUNNER_DIR / "request_output.json", encoding="utf-8") as fp:
|
||||||
|
data = json.load(fp)
|
||||||
|
request_type = RequestType(data.pop("request_type"))
|
||||||
|
if request_type == RequestType.exec:
|
||||||
|
return ExecRequestOutput(request_type=request_type)
|
||||||
|
elif request_type == RequestType.spawn_command:
|
||||||
|
return SpawnProcessRequestOutput(request_type=request_type, **data)
|
||||||
|
raise RuntimeError("unreachable code")
|
||||||
|
|
||||||
|
|
||||||
|
def make_spawn_process_request(
|
||||||
|
command: str,
|
||||||
|
*args: str,
|
||||||
|
env: Optional[Dict[str, str]] = None,
|
||||||
|
new_start_args: Iterable[str],
|
||||||
|
new_python_exe: str = sys.executable,
|
||||||
|
set_env_vars: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
) -> NoReturn:
|
||||||
|
if set_env_vars is None:
|
||||||
|
set_env_vars = {}
|
||||||
|
debug_args = (cmd.arg_names.DEBUG,) * common.get_log_cli_level()
|
||||||
|
request = SpawnProcessRequestInput(
|
||||||
|
request_new_python_exe=new_python_exe,
|
||||||
|
request_new_start_args=("-m", "redbot._update.internal", *debug_args, *new_start_args),
|
||||||
|
request_set_env_vars=set_env_vars,
|
||||||
|
command=command,
|
||||||
|
args=args,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
make_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
def make_exec_request(
|
||||||
|
new_python_exe: str,
|
||||||
|
*new_start_args: str,
|
||||||
|
set_env_vars: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
) -> NoReturn:
|
||||||
|
if set_env_vars is None:
|
||||||
|
set_env_vars = {}
|
||||||
|
debug_args = (cmd.arg_names.DEBUG,) * common.get_log_cli_level()
|
||||||
|
request = ExecRequestInput(
|
||||||
|
request_new_python_exe=new_python_exe,
|
||||||
|
request_new_start_args=("-m", "redbot._update.internal", *debug_args, *new_start_args),
|
||||||
|
request_set_env_vars=set_env_vars,
|
||||||
|
)
|
||||||
|
make_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wrapper_executable() -> Path:
|
||||||
|
return Path(os.environ[common.RUNNER_WRAPPER_EXE_ENV_VAR])
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.events import Click
|
||||||
|
from textual.widgets import Footer, Markdown, MarkdownViewer, Static
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from .changelog import Changelogs
|
||||||
|
|
||||||
|
|
||||||
|
# See https://github.com/Textualize/textual/discussions/6449
|
||||||
|
class MarkdownLinkTooltip(Static, inherit_css=False):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
MarkdownLinkTooltip {
|
||||||
|
layer: _tooltips;
|
||||||
|
margin: 1 0;
|
||||||
|
padding: 1 2;
|
||||||
|
background: $panel;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
constrain: inside inflect;
|
||||||
|
max-width: 40;
|
||||||
|
display: none;
|
||||||
|
offset-x: -50%;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class _MarkdownViewer(MarkdownViewer):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
_MarkdownViewer {
|
||||||
|
layers: default _tooltips;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield from super().compose()
|
||||||
|
yield MarkdownLinkTooltip()
|
||||||
|
|
||||||
|
def on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
|
||||||
|
# We don't want the default behavior of opening the browser/navigating to a file on click.
|
||||||
|
message.prevent_default()
|
||||||
|
|
||||||
|
tooltip = self.get_child_by_type(MarkdownLinkTooltip)
|
||||||
|
tooltip.display = True
|
||||||
|
# You can't cycle over the links in MarkdownViewer (see Textualize/textual#3555)
|
||||||
|
# so using mouse position is fine.
|
||||||
|
# Textualize/textual#3555: https://github.com/Textualize/textual/discussions/3555
|
||||||
|
tooltip.absolute_offset = self.app.mouse_position
|
||||||
|
# For some reason, links only render correctly when Text has a span over the whole text
|
||||||
|
# with a link but not when Text just has a style applied to it directly, i.e.:
|
||||||
|
# Text(message.href, style=f"link {message.href}")
|
||||||
|
# will not work.
|
||||||
|
tooltip.update(Text().append(message.href, style=f"link {message.href}"))
|
||||||
|
|
||||||
|
def on_click(self, message: Click) -> None:
|
||||||
|
tooltip = self.get_child_by_type(MarkdownLinkTooltip)
|
||||||
|
tooltip.display = False
|
||||||
|
|
||||||
|
|
||||||
|
class ChangelogReaderResult(enum.Enum):
|
||||||
|
QUIT = enum.auto()
|
||||||
|
CONTINUE = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangelogReaderApp(App[ChangelogReaderResult], inherit_bindings=False):
|
||||||
|
ENABLE_COMMAND_PALETTE = False
|
||||||
|
BINDINGS = [
|
||||||
|
Binding(key="ctrl+c", action="quit", description="Exit redbot-update"),
|
||||||
|
Binding(key="q", action="continue", description="Finish reading the changelog"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, markdown_content: str) -> None:
|
||||||
|
self.markdown_content = markdown_content
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_changelogs(cls, changelogs: Changelogs) -> Self:
|
||||||
|
if not changelogs:
|
||||||
|
return cls("")
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
contributors = sorted(
|
||||||
|
{
|
||||||
|
contributor
|
||||||
|
for changelog in changelogs.values()
|
||||||
|
for contributor in changelog.contributors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if contributors:
|
||||||
|
contributor_thanks = (
|
||||||
|
"# Thanks to our contributors \N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}\n"
|
||||||
|
"**The releases below were made with help from the following people:** \n"
|
||||||
|
)
|
||||||
|
contributor_thanks += ", ".join(
|
||||||
|
f"[@{contributor}](https://github.com/sponsors/{contributor})"
|
||||||
|
for contributor in contributors
|
||||||
|
)
|
||||||
|
parts.append(contributor_thanks)
|
||||||
|
|
||||||
|
parts.append("# Read before updating")
|
||||||
|
for changelog in reversed(changelogs.values()):
|
||||||
|
if changelog.read_before_updating_section:
|
||||||
|
parts.append(f"## {changelog.version}")
|
||||||
|
parts.append(changelog.read_before_updating_section)
|
||||||
|
|
||||||
|
parts.append("# User changelog")
|
||||||
|
for changelog in reversed(changelogs.values()):
|
||||||
|
if changelog.user_changelog_section:
|
||||||
|
parts.append(f"## {changelog.version}")
|
||||||
|
parts.append(changelog.user_changelog_section)
|
||||||
|
|
||||||
|
return cls("\n".join(parts))
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
markdown_viewer = _MarkdownViewer(
|
||||||
|
self.markdown_content, show_table_of_contents=True, open_links=False
|
||||||
|
)
|
||||||
|
markdown_viewer.code_indent_guides = False
|
||||||
|
yield markdown_viewer
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def action_quit(self) -> None:
|
||||||
|
self.exit(ChangelogReaderResult.QUIT)
|
||||||
|
|
||||||
|
def action_continue(self) -> None:
|
||||||
|
self.exit(ChangelogReaderResult.CONTINUE)
|
||||||
@@ -0,0 +1,738 @@
|
|||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, NoReturn, Optional, Set
|
||||||
|
|
||||||
|
import click
|
||||||
|
from packaging.version import Version
|
||||||
|
from python_discovery import PythonInfo
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
||||||
|
from rich.text import Text
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from redbot.core.utils._internal_utils import (
|
||||||
|
AvailableVersion,
|
||||||
|
detailed_progress,
|
||||||
|
fetch_available_red_versions,
|
||||||
|
get_installed_extras,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import changelog, cmd, common, runner
|
||||||
|
from .cog_compatibility_checker import CompatibilitySummary
|
||||||
|
from .tui import ChangelogReaderApp, ChangelogReaderResult
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class UpdaterOptions:
|
||||||
|
"""Update options specified by the user."""
|
||||||
|
|
||||||
|
instances: List[str]
|
||||||
|
excluded_instances: Set[str]
|
||||||
|
ignore_prefix: bool
|
||||||
|
backup_dir: Optional[Path]
|
||||||
|
no_backup: bool
|
||||||
|
red_version: Optional[Version]
|
||||||
|
no_major_updates: bool
|
||||||
|
no_full_changelog: bool
|
||||||
|
no_cog_compatibility_check: bool
|
||||||
|
new_python_interpreter: Optional[PythonInfo]
|
||||||
|
update_cogs: Optional[bool]
|
||||||
|
force_reinstall: bool
|
||||||
|
interactive: bool
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
backup_dir = data["backup_dir"]
|
||||||
|
red_version = data["red_version"]
|
||||||
|
return cls(
|
||||||
|
instances=data["instances"],
|
||||||
|
excluded_instances=set(data["excluded_instances"]),
|
||||||
|
ignore_prefix=data["ignore_prefix"],
|
||||||
|
backup_dir=backup_dir and Path(data["backup_dir"]),
|
||||||
|
no_backup=data["no_backup"],
|
||||||
|
red_version=red_version and Version(red_version),
|
||||||
|
no_major_updates=data["no_major_updates"],
|
||||||
|
no_full_changelog=data["no_full_changelog"],
|
||||||
|
no_cog_compatibility_check=data["no_cog_compatibility_check"],
|
||||||
|
new_python_interpreter=(
|
||||||
|
data["new_python_interpreter"]
|
||||||
|
and PythonInfo.from_dict(data["new_python_interpreter"])
|
||||||
|
),
|
||||||
|
update_cogs=data["update_cogs"],
|
||||||
|
force_reinstall=data["force_reinstall"],
|
||||||
|
interactive=data["interactive"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
data = dataclasses.asdict(self)
|
||||||
|
data["excluded_instances"] = list(self.excluded_instances)
|
||||||
|
data["backup_dir"] = self.backup_dir and str(self.backup_dir)
|
||||||
|
data["red_version"] = self.red_version and str(self.red_version)
|
||||||
|
data["new_python_interpreter"] = (
|
||||||
|
self.new_python_interpreter and self.new_python_interpreter.to_dict()
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class UpdaterCompatibilitySummary:
|
||||||
|
checked: Dict[str, CompatibilitySummary]
|
||||||
|
failed: List[str]
|
||||||
|
skipped: List[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
checked={
|
||||||
|
instance_name: CompatibilitySummary.from_json_dict(results_data)
|
||||||
|
for instance_name, results_data in data["checked"].items()
|
||||||
|
},
|
||||||
|
failed=data["failed"],
|
||||||
|
skipped=data["skipped"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"checked": {
|
||||||
|
instance_name: results.to_json_dict()
|
||||||
|
for instance_name, results in self.checked.items()
|
||||||
|
},
|
||||||
|
"failed": self.failed,
|
||||||
|
"skipped": self.skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class BackupResults:
|
||||||
|
checked: List[str]
|
||||||
|
failed: List[str]
|
||||||
|
skipped: List[str] = dataclasses.field(default_factory=list)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, List[str]]) -> Self:
|
||||||
|
return cls(checked=data["checked"], failed=data["failed"], skipped=data["skipped"])
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, List[str]]:
|
||||||
|
return dataclasses.asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
_PYTHON_VERSION_PLACEHOLDER = Version("0.0.dev0")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class UpdaterMetadata:
|
||||||
|
"""Metadata about the update process."""
|
||||||
|
|
||||||
|
# options specified by the user
|
||||||
|
options: UpdaterOptions
|
||||||
|
# info about Red version to update to (latest available or latest non-major update)
|
||||||
|
latest: AvailableVersion
|
||||||
|
latest_major: AvailableVersion
|
||||||
|
# info about Red/Python versions that we're updating from
|
||||||
|
current_version: Version = dataclasses.field(default_factory=common.get_current_red_version)
|
||||||
|
current_python_version: Version = dataclasses.field(
|
||||||
|
default_factory=common.get_current_python_version
|
||||||
|
)
|
||||||
|
# details about the interpreter that will be used for the new venv
|
||||||
|
interpreter_info: PythonInfo = dataclasses.field(default_factory=PythonInfo.current_system)
|
||||||
|
interpreter_version: Version = _PYTHON_VERSION_PLACEHOLDER
|
||||||
|
interpreter_exe: str = ""
|
||||||
|
# changelogs for version in (current_version, latest> range
|
||||||
|
changelogs: changelog.Changelogs = dataclasses.field(default_factory=dict)
|
||||||
|
# cog compatibility check results
|
||||||
|
cog_compatibility: Optional[UpdaterCompatibilitySummary] = None
|
||||||
|
# backup info
|
||||||
|
to_backup: List[str] = dataclasses.field(default_factory=list)
|
||||||
|
backup_dir: Optional[Path] = None
|
||||||
|
backup_results: Optional[BackupResults] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.interpreter_version is _PYTHON_VERSION_PLACEHOLDER:
|
||||||
|
self.interpreter_version = Version(
|
||||||
|
".".join(map(str, self.interpreter_info.version_info[:3]))
|
||||||
|
)
|
||||||
|
if not self.interpreter_exe:
|
||||||
|
self.interpreter_exe = self.interpreter_info.system_executable
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_dict(cls, data: Dict[str, Any]) -> Self:
|
||||||
|
"""
|
||||||
|
Make an instance of this class from a dictionary,
|
||||||
|
as returned by the `to_json_dict()` method.
|
||||||
|
|
||||||
|
This aims to maintain backwards compatibility with data generated by
|
||||||
|
earlier Red versions as it may be called with such data
|
||||||
|
after the last update step.
|
||||||
|
"""
|
||||||
|
backup_dir = data.get("backup_dir")
|
||||||
|
return cls(
|
||||||
|
options=UpdaterOptions.from_json_dict(data["options"]),
|
||||||
|
latest=AvailableVersion.from_json_dict(data["latest"]),
|
||||||
|
latest_major=AvailableVersion.from_json_dict(data["latest_major"]),
|
||||||
|
current_version=Version(data["current_version"]),
|
||||||
|
current_python_version=Version(data["current_python_version"]),
|
||||||
|
interpreter_version=Version(data["interpreter_version"]),
|
||||||
|
interpreter_info=PythonInfo.from_dict(data["interpreter_info"]),
|
||||||
|
interpreter_exe=data["interpreter_exe"],
|
||||||
|
changelogs={
|
||||||
|
Version(raw_version): changelog.VersionChangelog.from_json_dict(raw_changelog)
|
||||||
|
for raw_version, raw_changelog in data["changelogs"].items()
|
||||||
|
},
|
||||||
|
cog_compatibility=UpdaterCompatibilitySummary.from_json_dict(
|
||||||
|
data["cog_compatibility"]
|
||||||
|
),
|
||||||
|
to_backup=data["to_backup"],
|
||||||
|
backup_dir=backup_dir and Path(backup_dir),
|
||||||
|
backup_results=BackupResults.from_json_dict(data["backup_results"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"options": self.options.to_json_dict(),
|
||||||
|
"latest": self.latest.to_json_dict(),
|
||||||
|
"latest_major": self.latest_major.to_json_dict(),
|
||||||
|
"current_version": str(self.current_version),
|
||||||
|
"current_python_version": str(self.current_python_version),
|
||||||
|
"interpreter_version": str(self.interpreter_version),
|
||||||
|
"interpreter_info": self.interpreter_info.to_dict(),
|
||||||
|
"interpreter_exe": self.interpreter_exe,
|
||||||
|
"changelogs": {str(v): c.to_json_dict() for v, c in self.changelogs.items()},
|
||||||
|
"cog_compatibility": self.cog_compatibility and self.cog_compatibility.to_json_dict(),
|
||||||
|
"to_backup": self.to_backup,
|
||||||
|
"backup_dir": self.backup_dir and str(self.backup_dir),
|
||||||
|
"backup_results": self.backup_results and self.backup_results.to_json_dict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def breaking_update(self) -> bool:
|
||||||
|
return self.current_version.release[:2] != self.latest.version.release[:2]
|
||||||
|
|
||||||
|
|
||||||
|
class Updater:
|
||||||
|
metadata: UpdaterMetadata
|
||||||
|
|
||||||
|
def __init__(self, options: UpdaterOptions) -> None:
|
||||||
|
self.options = options
|
||||||
|
self.console = common.get_console()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest(self) -> AvailableVersion:
|
||||||
|
return self.metadata.latest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_version(self) -> Version:
|
||||||
|
return self.metadata.current_version
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
await self._prepare_metadata()
|
||||||
|
|
||||||
|
new_version_available = self.current_version < self.latest.version
|
||||||
|
if not self.options.force_reinstall and not new_version_available:
|
||||||
|
if self.current_version >= self.metadata.latest_major.version:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_SUCCESS,
|
||||||
|
"You are already running the latest available version of Red.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"There are no non-major updates available.\n",
|
||||||
|
"There is a new major version available: ",
|
||||||
|
Text(str(self.metadata.latest_major.version), style="bold"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_version_available:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_SUCCESS,
|
||||||
|
"New version available: ",
|
||||||
|
Text(str(self.latest.version), style="bold"),
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._show_changelog()
|
||||||
|
self._check_python_requires()
|
||||||
|
if self.options.no_cog_compatibility_check:
|
||||||
|
self.console.print(
|
||||||
|
"Will not make backups as --no-cog-compatibility-check option was passed."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self._check_cog_compatibility()
|
||||||
|
|
||||||
|
if self.options.no_backup:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO, "Will not make backups as --no-backup option was passed."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"The following instances will be backed up before performing the update: ",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(instance_name, style="bold") for instance_name in self.metadata.to_backup
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self.metadata.breaking_update:
|
||||||
|
self.console.print(
|
||||||
|
"[b]Remember that this is a major release and it may have some breaking changes"
|
||||||
|
" that the bot or its cogs may be affected by.[/]"
|
||||||
|
)
|
||||||
|
if self.options.interactive and not Confirm.ask(
|
||||||
|
f"Do you want to continue with the update to [b]Red {self.latest.version}[/]?"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
self.console.print()
|
||||||
|
|
||||||
|
if self.options.no_backup:
|
||||||
|
self.console.print("Will not make backups as --no-backup option was passed.")
|
||||||
|
else:
|
||||||
|
await self._make_backups()
|
||||||
|
|
||||||
|
await self._update_with_fresh_venv()
|
||||||
|
|
||||||
|
async def _prepare_metadata(self) -> None:
|
||||||
|
interpreter_info = self.options.new_python_interpreter or PythonInfo.current_system()
|
||||||
|
with self.console.status("Checking latest version..."):
|
||||||
|
available_versions = await fetch_available_red_versions(
|
||||||
|
include_prereleases=common.get_current_red_version().is_prerelease
|
||||||
|
)
|
||||||
|
latest_major = available_versions[0]
|
||||||
|
|
||||||
|
self.metadata = UpdaterMetadata(
|
||||||
|
self.options,
|
||||||
|
latest=latest_major,
|
||||||
|
latest_major=latest_major,
|
||||||
|
interpreter_info=interpreter_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.options.red_version:
|
||||||
|
if self.options.red_version <= self.current_version:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR, "You can only update to a newer version of Red."
|
||||||
|
)
|
||||||
|
raise SystemExit(2)
|
||||||
|
if (
|
||||||
|
self.options.no_major_updates
|
||||||
|
and self.options.red_version.release[:2] != self.current_version.release[:2]
|
||||||
|
):
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"Updating to the specified version would be a major update"
|
||||||
|
" but --no-major-updates option was specified.",
|
||||||
|
)
|
||||||
|
raise SystemExit(2)
|
||||||
|
for available_version in available_versions:
|
||||||
|
if available_version.version == self.options.red_version:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR, "The provided version does not seem to exist."
|
||||||
|
)
|
||||||
|
raise SystemExit(2)
|
||||||
|
self.metadata.latest = available_version
|
||||||
|
elif self.options.no_major_updates:
|
||||||
|
for available_version in available_versions:
|
||||||
|
if available_version.version.release[:2] == self.current_version.release[:2]:
|
||||||
|
self.metadata.latest = available_version
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if self.current_version < latest_major.version:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"Could not find any version of Red that would not be a major update.",
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
async def _show_changelog(self) -> None:
|
||||||
|
with self.console.status("Fetching changelogs..."):
|
||||||
|
changelogs = await changelog.fetch_changelogs()
|
||||||
|
self.metadata.changelogs = changelogs = changelog.get_changelogs_between(
|
||||||
|
changelogs, self.current_version, self.latest.version
|
||||||
|
)
|
||||||
|
common.print_with_prefix_column(common.ICON_SUCCESS, "Changelogs fetched.")
|
||||||
|
|
||||||
|
if not changelogs:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.options.interactive or self.options.no_full_changelog:
|
||||||
|
self.console.print(Panel(Markdown(changelog.render_markdown(changelogs))))
|
||||||
|
if self.options.interactive and not Confirm.ask("Do you want to continue?"):
|
||||||
|
raise click.Abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
first_changelog_version = min(changelogs)
|
||||||
|
last_changelog_version = max(changelogs)
|
||||||
|
parts = []
|
||||||
|
if first_changelog_version == last_changelog_version:
|
||||||
|
parts.append(
|
||||||
|
"You will now be presented with the changelog for"
|
||||||
|
f" [b]Red {first_changelog_version}[/]."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parts.append(
|
||||||
|
"You will now be presented with the changelogs for"
|
||||||
|
f" [b]Red {first_changelog_version}[/]-[b]{last_changelog_version}[/]."
|
||||||
|
)
|
||||||
|
parts.append(
|
||||||
|
f"\n[bold]{common.ICON_WARN}"
|
||||||
|
' Make sure to read through the [green]"Read before updating"[/] section'
|
||||||
|
f" before continuing. {common.ICON_WARN}[/bold]\n"
|
||||||
|
)
|
||||||
|
if self.metadata.breaking_update:
|
||||||
|
parts.append(
|
||||||
|
f"[bold]{common.ICON_WARN}"
|
||||||
|
" Please note that this is a major release and it may have some changes that"
|
||||||
|
" your bot or its cogs are affected by.[/bold]\n"
|
||||||
|
)
|
||||||
|
parts.append(
|
||||||
|
"After the changelog is open and you're ready to continue, hit the [b]Q[/] key"
|
||||||
|
" to close the changelog and continue the update process.\n\n"
|
||||||
|
"Hit the [b]Enter[/] key to view the changelog."
|
||||||
|
)
|
||||||
|
self.console.input(Panel("".join(parts)), password=True)
|
||||||
|
|
||||||
|
viewer = ChangelogReaderApp.from_changelogs(changelogs)
|
||||||
|
result = await viewer.run_async()
|
||||||
|
if result is None:
|
||||||
|
raise RuntimeError("Unexpected state")
|
||||||
|
if result is ChangelogReaderResult.QUIT:
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
self.console.print("Changelog has been closed.\n")
|
||||||
|
|
||||||
|
def _check_python_requires(self) -> None:
|
||||||
|
if self.metadata.interpreter_version in self.latest.requires_python:
|
||||||
|
return
|
||||||
|
if self.options.new_python_interpreter:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"The latest version of Red requires a different Python version (",
|
||||||
|
Text(str(self.latest.requires_python), style="bold"),
|
||||||
|
") from the version of the interpreter passed to with the --new-python-interpreter"
|
||||||
|
" option (",
|
||||||
|
Text(str(self.metadata.interpreter_version), style="bold"),
|
||||||
|
")",
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_WARN if self.options.interactive else common.ICON_ERROR,
|
||||||
|
"The latest version of Red requires a different Python version (",
|
||||||
|
Text(str(self.latest.requires_python), style="bold"),
|
||||||
|
") from the one that you are currently using (",
|
||||||
|
Text(str(self.metadata.interpreter_version), style="bold"),
|
||||||
|
")",
|
||||||
|
(
|
||||||
|
"\nredbot-update will have to recreate the virtual environment"
|
||||||
|
" with a compatible version of Python."
|
||||||
|
if self.options.interactive
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not self.options.interactive:
|
||||||
|
raise SystemExit(1)
|
||||||
|
interpreters = common.search_for_interpreters(self.latest.requires_python)
|
||||||
|
|
||||||
|
def _render_interpreter(interpreter_exe: str, interpreter_version: Version) -> Text:
|
||||||
|
return Text.assemble(
|
||||||
|
"CPython ",
|
||||||
|
(str(interpreter_version), "repr.number"),
|
||||||
|
" (",
|
||||||
|
(interpreter_exe, "log.path"),
|
||||||
|
")",
|
||||||
|
)
|
||||||
|
|
||||||
|
text = Text("Found the following compatible Python interpreters on your system:")
|
||||||
|
for idx, (interpreter_exe, interpreter_version, python_info) in enumerate(interpreters, 1):
|
||||||
|
text.append_text(Text(f"\n{idx}. ", style="markdown.item.number"))
|
||||||
|
text.append_text(_render_interpreter(interpreter_exe, interpreter_version))
|
||||||
|
self.console.print(Panel(text))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result = IntPrompt.ask(
|
||||||
|
"\nEnter the number of the Python interpreter above that you want to use"
|
||||||
|
" or type 0 to input the path to it yourself. Generally, you should choose"
|
||||||
|
" the interpreter with the latest version on the above list.\n"
|
||||||
|
"Enter your selection",
|
||||||
|
default=1,
|
||||||
|
)
|
||||||
|
if result < 0 or result > len(interpreters):
|
||||||
|
self.console.print("[prompt.invalid] This is not a valid choice.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
response = Prompt.ask(
|
||||||
|
"Please input the path to the Python interpreter that you want to use"
|
||||||
|
)
|
||||||
|
if not response:
|
||||||
|
self.console.print("[prompt.invalid] No path was provided.")
|
||||||
|
continue
|
||||||
|
info = PythonInfo.from_exe(response)
|
||||||
|
interpreter_version = Version(info.version_str)
|
||||||
|
if (
|
||||||
|
info.implementation != "CPython"
|
||||||
|
or interpreter_version not in self.latest.requires_python
|
||||||
|
):
|
||||||
|
self.console.print(
|
||||||
|
"[prompt.invalid] The provided path points to an incompatible Python"
|
||||||
|
" interpreter. Latest version requires CPython"
|
||||||
|
f" {self.latest.requires_python} but the provided interpreter is"
|
||||||
|
f" {info.implementation} {interpreter_version}."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
self.metadata.interpreter_version = interpreter_version
|
||||||
|
self.metadata.interpreter_info = info
|
||||||
|
self.metadata.interpreter_exe = info.executable
|
||||||
|
else:
|
||||||
|
(
|
||||||
|
self.metadata.interpreter_exe,
|
||||||
|
self.metadata.interpreter_version,
|
||||||
|
self.metadata.interpreter_info,
|
||||||
|
) = interpreters[result - 1]
|
||||||
|
|
||||||
|
self.console.print(
|
||||||
|
"\n[b]You selected:[/]",
|
||||||
|
_render_interpreter(
|
||||||
|
self.metadata.interpreter_exe, self.metadata.interpreter_version
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if Confirm.ask("Do you want to continue with this choice?"):
|
||||||
|
self.console.print()
|
||||||
|
break
|
||||||
|
|
||||||
|
async def _check_cog_compatibility(self) -> None:
|
||||||
|
outputs = {}
|
||||||
|
checked_instances = {}
|
||||||
|
skipped_instances = []
|
||||||
|
failed_instances = []
|
||||||
|
unsupported_storage_instances = []
|
||||||
|
for instance_name in self.options.instances:
|
||||||
|
if instance_name in self.options.excluded_instances:
|
||||||
|
skipped_instances.append(instance_name)
|
||||||
|
continue
|
||||||
|
exit_code, stdout, results = await cmd.cog_compatibility.call(
|
||||||
|
instance_name,
|
||||||
|
red_version=self.latest.version,
|
||||||
|
python_version=self.metadata.interpreter_version,
|
||||||
|
ignore_prefix=self.options.ignore_prefix,
|
||||||
|
return_results=True,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
if exit_code == cmd.cog_compatibility.EXIT_INSTANCE_BACKEND_UNSUPPORTED:
|
||||||
|
skipped_instances.append(instance_name)
|
||||||
|
unsupported_storage_instances.append(instance_name)
|
||||||
|
elif exit_code == cmd.cog_compatibility.EXIT_INSTANCE_SITE_PREFIX_MISMATCH:
|
||||||
|
skipped_instances.append(instance_name)
|
||||||
|
elif exit_code:
|
||||||
|
failed_instances.append(instance_name)
|
||||||
|
print(stdout, end="")
|
||||||
|
Text.assemble(
|
||||||
|
"\N{UPWARDS ARROW} " * 3,
|
||||||
|
"Failure for ",
|
||||||
|
(instance_name, "bold"),
|
||||||
|
" instance",
|
||||||
|
)
|
||||||
|
self.console.rule(
|
||||||
|
Text.assemble(
|
||||||
|
"\N{UPWARDS ARROW} " * 3,
|
||||||
|
"Failure for ",
|
||||||
|
(instance_name, "bold"),
|
||||||
|
" instance above",
|
||||||
|
" \N{UPWARDS ARROW}" * 3,
|
||||||
|
),
|
||||||
|
style="red",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert results is not None
|
||||||
|
outputs[instance_name] = stdout
|
||||||
|
checked_instances[instance_name] = results
|
||||||
|
if stdout:
|
||||||
|
self.console.print()
|
||||||
|
self.console.print()
|
||||||
|
if not self.options.no_backup:
|
||||||
|
self.metadata.to_backup = [*checked_instances, *failed_instances]
|
||||||
|
|
||||||
|
if outputs:
|
||||||
|
for instance_name, stdout in outputs.items():
|
||||||
|
self.console.rule(Text(instance_name, style="bold"))
|
||||||
|
print(stdout, end="")
|
||||||
|
self.console.rule()
|
||||||
|
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"Finished checking cog compatibility.",
|
||||||
|
(
|
||||||
|
"\nThe results for each of the checked instances are shown above."
|
||||||
|
if checked_instances
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if failed_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"Failure occurred while trying to check compatibility for following instances: ",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(instance_name, style="bold") for instance_name in failed_instances
|
||||||
|
),
|
||||||
|
"\nScroll above to find the errors.",
|
||||||
|
)
|
||||||
|
if unsupported_storage_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"The following instances were skipped as they use a storage backend that is"
|
||||||
|
" not supported by the current Red installation (some requirements are missing): ",
|
||||||
|
Text(", ").join(
|
||||||
|
Text(instance_name, style="bold")
|
||||||
|
for instance_name in unsupported_storage_instances
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not checked_instances:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_INFO,
|
||||||
|
"There were no",
|
||||||
|
(" other" if failed_instances or unsupported_storage_instances else ""),
|
||||||
|
" instances to check cog compatibility for.",
|
||||||
|
)
|
||||||
|
self.console.print()
|
||||||
|
|
||||||
|
self.metadata.cog_compatibility = UpdaterCompatibilitySummary(
|
||||||
|
checked=checked_instances, failed=failed_instances, skipped=skipped_instances
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _make_backups(self) -> None:
|
||||||
|
self.metadata.backup_dir = backup_dir = self.options.backup_dir or Path(
|
||||||
|
tempfile.mkdtemp(prefix="redbot-update-backup-")
|
||||||
|
)
|
||||||
|
console = common.get_console()
|
||||||
|
console.print("Backups will be created at:", Text(str(backup_dir), style="bold"))
|
||||||
|
venv_archive = backup_dir / "venv.tar.gz"
|
||||||
|
with console.status("Making a backup of the virtual environment directory..."):
|
||||||
|
venv_dir = Path(sys.prefix)
|
||||||
|
venv_files = []
|
||||||
|
for current_dir, _, filenames in os.walk(venv_dir):
|
||||||
|
target_dir = os.path.relpath(current_dir, venv_dir)
|
||||||
|
if target_dir == ".":
|
||||||
|
target_dir = ""
|
||||||
|
for name in filenames:
|
||||||
|
venv_files.append(
|
||||||
|
(os.path.join(current_dir, name), os.path.join(target_dir, name))
|
||||||
|
)
|
||||||
|
with tarfile.open(venv_archive, "w:gz", compresslevel=6) as tar:
|
||||||
|
with detailed_progress(unit="files") as progress:
|
||||||
|
for src, arcname in progress.track(venv_files, description="Compressing..."):
|
||||||
|
tar.add(src, arcname=arcname, recursive=False)
|
||||||
|
console.print(
|
||||||
|
"Created a backup of the virtual environment directory at:",
|
||||||
|
Text(str(venv_archive), style="bold"),
|
||||||
|
)
|
||||||
|
|
||||||
|
checked = []
|
||||||
|
failed = []
|
||||||
|
instance_backups_dir = backup_dir / "instance_backups"
|
||||||
|
instance_backups_dir.mkdir()
|
||||||
|
for instance_name in self.metadata.to_backup:
|
||||||
|
console.print(
|
||||||
|
"Making a backup of the", Text(instance_name, style="bold"), "instance..."
|
||||||
|
)
|
||||||
|
debug_args = (cmd.arg_names.DEBUG,) * common.get_log_cli_level()
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"redbot.setup",
|
||||||
|
"backup",
|
||||||
|
*debug_args,
|
||||||
|
instance_name,
|
||||||
|
str(instance_backups_dir),
|
||||||
|
)
|
||||||
|
if await proc.wait():
|
||||||
|
failed.append(instance_name)
|
||||||
|
else:
|
||||||
|
checked.append(instance_name)
|
||||||
|
|
||||||
|
self.metadata.backup_results = BackupResults(checked=checked, failed=failed)
|
||||||
|
if self.metadata.cog_compatibility:
|
||||||
|
self.metadata.backup_results.skipped.extend(self.metadata.cog_compatibility.skipped)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
common.print_with_prefix_column(
|
||||||
|
common.ICON_ERROR,
|
||||||
|
"The following instances failed during backup: ",
|
||||||
|
Text(", ").join(Text(instance_name, style="bold") for instance_name in failed),
|
||||||
|
"\nScroll above to find the errors.",
|
||||||
|
)
|
||||||
|
# If a backup fails, we cannot allow non-interactive update to continue.
|
||||||
|
# The user can choose to use options such as `--no-backup`, `--instance`,
|
||||||
|
# and `--exclude-instance` to not have the backup step try to backup something
|
||||||
|
# that it can't.
|
||||||
|
if not self.options.interactive or not Confirm.ask(
|
||||||
|
"Do you want to continue with the update regardless?"
|
||||||
|
):
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
async def _update_with_fresh_venv(self) -> NoReturn:
|
||||||
|
console = common.get_console()
|
||||||
|
venv_dir = Path(sys.prefix)
|
||||||
|
backup_dir = venv_dir / common.OLD_VENV_BACKUP_DIR_NAME
|
||||||
|
try:
|
||||||
|
backup_dir.mkdir()
|
||||||
|
except FileExistsError:
|
||||||
|
console.print(
|
||||||
|
"Found that a partial backup of a virtual environment from a past failed update"
|
||||||
|
" exists at",
|
||||||
|
Text(str(backup_dir), style="bold"),
|
||||||
|
"\nThe update will not proceed to avoid overriding it. If you are certain that"
|
||||||
|
" you don't need to restore anything from it, remove it and try updating again.",
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
with console.status("Determining extras to install..."):
|
||||||
|
try:
|
||||||
|
metadata = await self.latest.fetch_core_metadata()
|
||||||
|
except TypeError:
|
||||||
|
extras = get_installed_extras()
|
||||||
|
else:
|
||||||
|
known_extras = metadata.provides_extra or []
|
||||||
|
extras = [extra for extra in get_installed_extras() if extra in known_extras]
|
||||||
|
console.print("Extras to install have been determined.")
|
||||||
|
|
||||||
|
old_executable = Path(sys.executable)
|
||||||
|
rel_executable = old_executable.relative_to(venv_dir)
|
||||||
|
new_executable = backup_dir / rel_executable
|
||||||
|
wrapper_exe = runner.get_wrapper_executable()
|
||||||
|
|
||||||
|
with console.status("Moving old virtual environment..."):
|
||||||
|
for path in venv_dir.iterdir():
|
||||||
|
if path == backup_dir or path == wrapper_exe:
|
||||||
|
continue
|
||||||
|
path.rename(backup_dir / path.name)
|
||||||
|
console.print("Old virtual environment moved.")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", encoding="utf-8", prefix="redbot-update-metadata-", suffix=".json", delete=False
|
||||||
|
) as metadata_file:
|
||||||
|
json.dump(self.metadata.to_json_dict(), metadata_file)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
runner.make_exec_request(
|
||||||
|
str(new_executable),
|
||||||
|
"reinstall",
|
||||||
|
# base executable for venv creation
|
||||||
|
self.metadata.interpreter_exe,
|
||||||
|
# venv dir
|
||||||
|
str(venv_dir),
|
||||||
|
# scripts path
|
||||||
|
self.metadata.interpreter_info.sysconfig_path("scripts", {"base": str(venv_dir)}),
|
||||||
|
# Red dependency specifier
|
||||||
|
common.get_red_dependency_specifier(self.latest.version, extras),
|
||||||
|
set_env_vars={common.INTERNAL_UPDATER_METADATA_ENV_VAR: metadata_file.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_updater_metadata() -> UpdaterMetadata:
|
||||||
|
with open(os.environ[common.INTERNAL_UPDATER_METADATA_ENV_VAR], encoding="utf-8") as fp:
|
||||||
|
return UpdaterMetadata.from_json_dict(json.load(fp))
|
||||||
@@ -30,7 +30,9 @@ _ = Translator("Audio", Path(__file__))
|
|||||||
_RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ")
|
_RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ")
|
||||||
_RE_YOUTUBE_TIMESTAMP: Final[Pattern] = re.compile(r"[&|?]t=(\d+)s?")
|
_RE_YOUTUBE_TIMESTAMP: Final[Pattern] = re.compile(r"[&|?]t=(\d+)s?")
|
||||||
_RE_YOUTUBE_INDEX: Final[Pattern] = re.compile(r"&index=(\d+)")
|
_RE_YOUTUBE_INDEX: Final[Pattern] = re.compile(r"&index=(\d+)")
|
||||||
_RE_SPOTIFY_URL: Final[Pattern] = re.compile(r"(http[s]?://)?(open\.spotify\.com)/")
|
_RE_SPOTIFY_URL: Final[Pattern] = re.compile(
|
||||||
|
r"(http[s]?://)?(open\.spotify\.com)/(?:intl-[a-zA-Z-]+/)?"
|
||||||
|
)
|
||||||
_RE_SPOTIFY_TIMESTAMP: Final[Pattern] = re.compile(r"#(\d+):(\d+)")
|
_RE_SPOTIFY_TIMESTAMP: Final[Pattern] = re.compile(r"#(\d+):(\d+)")
|
||||||
_RE_SOUNDCLOUD_TIMESTAMP: Final[Pattern] = re.compile(r"#t=(\d+):(\d+)s?")
|
_RE_SOUNDCLOUD_TIMESTAMP: Final[Pattern] = re.compile(r"#t=(\d+):(\d+)s?")
|
||||||
_RE_TWITCH_TIMESTAMP: Final[Pattern] = re.compile(r"\?t=(\d+)h(\d+)m(\d+)s")
|
_RE_TWITCH_TIMESTAMP: Final[Pattern] = re.compile(r"\?t=(\d+)h(\d+)m(\d+)s")
|
||||||
|
|||||||
@@ -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.1"
|
||||||
# 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, 25)
|
||||||
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."
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class InvalidYoutubeCredentials(StreamsError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKickCredentials(StreamsError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class YoutubeQuotaExceeded(StreamsError):
|
class YoutubeQuotaExceeded(StreamsError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from operator import is_
|
||||||
import discord
|
import discord
|
||||||
from redbot.core.utils.chat_formatting import humanize_list
|
from redbot.core.utils.chat_formatting import humanize_list
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
@@ -7,6 +8,7 @@ from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replace
|
|||||||
from redbot.core.utils.chat_formatting import escape, inline, pagify
|
from redbot.core.utils.chat_formatting import escape, inline, pagify
|
||||||
|
|
||||||
from .streamtypes import (
|
from .streamtypes import (
|
||||||
|
KickStream,
|
||||||
PicartoStream,
|
PicartoStream,
|
||||||
Stream,
|
Stream,
|
||||||
TwitchStream,
|
TwitchStream,
|
||||||
@@ -14,6 +16,7 @@ from .streamtypes import (
|
|||||||
)
|
)
|
||||||
from .errors import (
|
from .errors import (
|
||||||
APIError,
|
APIError,
|
||||||
|
InvalidKickCredentials,
|
||||||
InvalidTwitchCredentials,
|
InvalidTwitchCredentials,
|
||||||
InvalidYoutubeCredentials,
|
InvalidYoutubeCredentials,
|
||||||
OfflineStream,
|
OfflineStream,
|
||||||
@@ -51,6 +54,7 @@ class Streams(commands.Cog):
|
|||||||
"tokens": {},
|
"tokens": {},
|
||||||
"streams": [],
|
"streams": [],
|
||||||
"notified_owner_missing_twitch_secret": False,
|
"notified_owner_missing_twitch_secret": False,
|
||||||
|
"notified_owner_missing_kick_secret": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
guild_defaults = {
|
guild_defaults = {
|
||||||
@@ -70,6 +74,7 @@ class Streams(commands.Cog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.config: Config = Config.get_conf(self, 26262626)
|
self.config: Config = Config.get_conf(self, 26262626)
|
||||||
self.ttv_bearer_cache: dict = {}
|
self.ttv_bearer_cache: dict = {}
|
||||||
|
self.kick_bearer_cache: dict = {}
|
||||||
self.config.register_global(**self.global_defaults)
|
self.config.register_global(**self.global_defaults)
|
||||||
self.config.register_guild(**self.guild_defaults)
|
self.config.register_guild(**self.guild_defaults)
|
||||||
self.config.register_role(**self.role_defaults)
|
self.config.register_role(**self.role_defaults)
|
||||||
@@ -105,6 +110,8 @@ class Streams(commands.Cog):
|
|||||||
async def on_red_api_tokens_update(self, service_name, api_tokens):
|
async def on_red_api_tokens_update(self, service_name, api_tokens):
|
||||||
if service_name == "twitch":
|
if service_name == "twitch":
|
||||||
await self.get_twitch_bearer_token(api_tokens)
|
await self.get_twitch_bearer_token(api_tokens)
|
||||||
|
elif service_name == "kick":
|
||||||
|
await self.get_kick_bearer_token(api_tokens)
|
||||||
|
|
||||||
async def move_api_keys(self) -> None:
|
async def move_api_keys(self) -> None:
|
||||||
"""Move the API keys from cog stored config to core bot config if they exist."""
|
"""Move the API keys from cog stored config to core bot config if they exist."""
|
||||||
@@ -126,7 +133,7 @@ class Streams(commands.Cog):
|
|||||||
"1. Go to this page: {link}.\n"
|
"1. Go to this page: {link}.\n"
|
||||||
'2. Click "Manage" on your application.\n'
|
'2. Click "Manage" on your application.\n'
|
||||||
'3. Click on "New secret".\n'
|
'3. Click on "New secret".\n'
|
||||||
"5. Copy your client ID and your client secret into:\n"
|
"4. Copy your client ID and your client secret into:\n"
|
||||||
"{command}"
|
"{command}"
|
||||||
"\n\n"
|
"\n\n"
|
||||||
"Note: These tokens are sensitive and should only be used in a private channel "
|
"Note: These tokens are sensitive and should only be used in a private channel "
|
||||||
@@ -142,6 +149,28 @@ class Streams(commands.Cog):
|
|||||||
await send_to_owners_with_prefix_replaced(self.bot, message)
|
await send_to_owners_with_prefix_replaced(self.bot, message)
|
||||||
await self.config.notified_owner_missing_twitch_secret.set(True)
|
await self.config.notified_owner_missing_twitch_secret.set(True)
|
||||||
|
|
||||||
|
async def _notify_owner_about_missing_kick_secret(self) -> None:
|
||||||
|
message = _(
|
||||||
|
"You need a client secret key if you want to use the Kick API on this cog.\n"
|
||||||
|
"Follow these steps:\n"
|
||||||
|
"1. Go to this page: {link}.\n"
|
||||||
|
'2. Click "Manage" on your application.\n'
|
||||||
|
"3. Copy your client ID and your client secret into:\n"
|
||||||
|
"{command}"
|
||||||
|
"\n\n"
|
||||||
|
"Note: These tokens are sensitive and should only be used in a private channel "
|
||||||
|
"or in DM with the bot."
|
||||||
|
).format(
|
||||||
|
link="https://kick.com/settings/developer",
|
||||||
|
command=inline(
|
||||||
|
"[p]set api kick client_id {} client_secret {}".format(
|
||||||
|
_("<your_client_id_here>"), _("<your_client_secret_here>")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await send_to_owners_with_prefix_replaced(self.bot, message)
|
||||||
|
await self.config.notified_owner_missing_kick_secret.set(True)
|
||||||
|
|
||||||
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
||||||
tokens = (
|
tokens = (
|
||||||
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
|
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
|
||||||
@@ -198,9 +227,64 @@ class Streams(commands.Cog):
|
|||||||
self.ttv_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
self.ttv_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
||||||
|
|
||||||
async def maybe_renew_twitch_bearer_token(self) -> None:
|
async def maybe_renew_twitch_bearer_token(self) -> None:
|
||||||
if self.ttv_bearer_cache:
|
if (
|
||||||
if self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60:
|
self.ttv_bearer_cache
|
||||||
await self.get_twitch_bearer_token()
|
and self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
|
||||||
|
):
|
||||||
|
await self.get_twitch_bearer_token()
|
||||||
|
|
||||||
|
async def get_kick_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
||||||
|
tokens = await self.bot.get_shared_api_tokens("kick") if api_tokens is None else api_tokens
|
||||||
|
if tokens.get("client_id"):
|
||||||
|
notified_owner_missing_kick_secret = (
|
||||||
|
await self.config.notified_owner_missing_kick_secret()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tokens["client_secret"]
|
||||||
|
if notified_owner_missing_kick_secret is True:
|
||||||
|
await self.config.notified_owner_missing_kick_secret.set(False)
|
||||||
|
except KeyError:
|
||||||
|
if notified_owner_missing_kick_secret is False:
|
||||||
|
asyncio.create_task(self._notify_owner_about_missing_kick_secret())
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
"https://id.kick.com/oauth/token",
|
||||||
|
params={
|
||||||
|
"client_id": tokens.get("client_id", ""),
|
||||||
|
"client_secret": tokens.get("client_secret", ""),
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
},
|
||||||
|
) as req:
|
||||||
|
try:
|
||||||
|
data = await req.json()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if req.status == 200:
|
||||||
|
pass
|
||||||
|
elif req.status == 401 and data.get("error") == "invalid_client":
|
||||||
|
log.error("Kick API request failed authentication: set Client ID is invalid.")
|
||||||
|
elif "error" in data:
|
||||||
|
log.error(
|
||||||
|
"Kick OAuth2 API request failed with status code %s and error message: %s",
|
||||||
|
req.status,
|
||||||
|
data["error"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.error("Kick OAuth2 API request failed with status code %s", req.status)
|
||||||
|
|
||||||
|
if req.status != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.kick_bearer_cache = data
|
||||||
|
self.kick_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
||||||
|
|
||||||
|
async def maybe_renew_kick_token(self) -> None:
|
||||||
|
if (
|
||||||
|
self.kick_bearer_cache
|
||||||
|
and self.kick_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
|
||||||
|
):
|
||||||
|
await self.get_kick_bearer_token()
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@@ -242,10 +326,19 @@ class Streams(commands.Cog):
|
|||||||
stream = PicartoStream(_bot=self.bot, name=channel_name)
|
stream = PicartoStream(_bot=self.bot, name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
|
async def kickstream(self, ctx: commands.Context, channel_name: str):
|
||||||
|
"""Check if a Kick channel is live."""
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
token = self.kick_bearer_cache.get("access_token")
|
||||||
|
stream = _streamtypes.KickStream(_bot=self.bot, name=channel_name, token=token)
|
||||||
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
async def check_online(
|
async def check_online(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
stream: Union[PicartoStream, YoutubeStream, TwitchStream],
|
stream: Union[PicartoStream, YoutubeStream, TwitchStream, KickStream],
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
info = await stream.is_online()
|
info = await stream.is_online()
|
||||||
@@ -265,6 +358,12 @@ class Streams(commands.Cog):
|
|||||||
"The YouTube API key is either invalid or has not been set. See {command}."
|
"The YouTube API key is either invalid or has not been set. See {command}."
|
||||||
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
||||||
)
|
)
|
||||||
|
except InvalidKickCredentials:
|
||||||
|
await ctx.send(
|
||||||
|
_("The Kick API key is either invalid or has not been set. See {command}.").format(
|
||||||
|
command=inline(f"{ctx.clean_prefix}streamset kicktoken")
|
||||||
|
)
|
||||||
|
)
|
||||||
except YoutubeQuotaExceeded:
|
except YoutubeQuotaExceeded:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
@@ -363,6 +462,18 @@ class Streams(commands.Cog):
|
|||||||
"""Toggle alerts in this channel for a Picarto stream."""
|
"""Toggle alerts in this channel for a Picarto stream."""
|
||||||
await self.stream_alert(ctx, PicartoStream, channel_name, discord_channel)
|
await self.stream_alert(ctx, PicartoStream, channel_name, discord_channel)
|
||||||
|
|
||||||
|
@streamalert.command(name="kick")
|
||||||
|
async def kick_alert(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
channel_name: str,
|
||||||
|
discord_channel: Union[
|
||||||
|
discord.TextChannel, discord.VoiceChannel, discord.StageChannel
|
||||||
|
] = commands.CurrentChannel,
|
||||||
|
):
|
||||||
|
"""Toggle alerts in this channel for a Kick stream."""
|
||||||
|
await self.stream_alert(ctx, KickStream, channel_name, discord_channel)
|
||||||
|
|
||||||
@streamalert.command(name="stop", usage="[disable_all=No]")
|
@streamalert.command(name="stop", usage="[disable_all=No]")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Disable all stream alerts in this channel or server.
|
"""Disable all stream alerts in this channel or server.
|
||||||
@@ -435,6 +546,7 @@ class Streams(commands.Cog):
|
|||||||
token = await self.bot.get_shared_api_tokens(_class.token_name)
|
token = await self.bot.get_shared_api_tokens(_class.token_name)
|
||||||
is_yt = _class.__name__ == "YoutubeStream"
|
is_yt = _class.__name__ == "YoutubeStream"
|
||||||
is_twitch = _class.__name__ == "TwitchStream"
|
is_twitch = _class.__name__ == "TwitchStream"
|
||||||
|
is_kick = _class.__name__ == "KickStream"
|
||||||
if is_yt and not self.check_name_or_id(channel_name):
|
if is_yt and not self.check_name_or_id(channel_name):
|
||||||
stream = _class(_bot=self.bot, id=channel_name, token=token, config=self.config)
|
stream = _class(_bot=self.bot, id=channel_name, token=token, config=self.config)
|
||||||
elif is_twitch:
|
elif is_twitch:
|
||||||
@@ -445,6 +557,10 @@ class Streams(commands.Cog):
|
|||||||
token=token.get("client_id"),
|
token=token.get("client_id"),
|
||||||
bearer=self.ttv_bearer_cache.get("access_token", None),
|
bearer=self.ttv_bearer_cache.get("access_token", None),
|
||||||
)
|
)
|
||||||
|
elif is_kick:
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
token = self.kick_bearer_cache.get("access_token")
|
||||||
|
stream = _class(_bot=self.bot, name=channel_name, token=token)
|
||||||
else:
|
else:
|
||||||
if is_yt:
|
if is_yt:
|
||||||
stream = _class(
|
stream = _class(
|
||||||
@@ -464,8 +580,7 @@ class Streams(commands.Cog):
|
|||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"The YouTube API key is either invalid or has not been set. See "
|
"The YouTube API key is either invalid or has not been set. See {command}."
|
||||||
"{command}."
|
|
||||||
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -476,6 +591,13 @@ class Streams(commands.Cog):
|
|||||||
" Try again later or contact the owner if this continues."
|
" Try again later or contact the owner if this continues."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except InvalidKickCredentials:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"The Kick API key is either invalid or has not been set. See {command}."
|
||||||
|
).format(command=inline(f"{ctx.clean_prefix}streamset kicktoken"))
|
||||||
|
)
|
||||||
|
return
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
log.error(
|
log.error(
|
||||||
"Something went wrong whilst trying to contact the stream service's API.\n"
|
"Something went wrong whilst trying to contact the stream service's API.\n"
|
||||||
@@ -537,6 +659,30 @@ class Streams(commands.Cog):
|
|||||||
|
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
|
@streamset.command()
|
||||||
|
@commands.is_owner()
|
||||||
|
async def kicktoken(self, ctx: commands.Context):
|
||||||
|
"""Explain how to set the Kick token."""
|
||||||
|
message = _(
|
||||||
|
"To get one, do the following:\n"
|
||||||
|
"1. Go to this page: {link}.\n"
|
||||||
|
"2. Click on *Create new*.\n"
|
||||||
|
"3. Fill the name and description, for *Redirection URL* add *http://localhost*.\n"
|
||||||
|
"4. Click on *Create Application*.\n"
|
||||||
|
"5. Copy your client ID and your client secret into:\n"
|
||||||
|
"{command}"
|
||||||
|
"\n\n"
|
||||||
|
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
||||||
|
"or in DM with the bot.\n"
|
||||||
|
).format(
|
||||||
|
link="https://kick.com/settings/developer",
|
||||||
|
command="`{}set api kick client_id {} client_secret {}`".format(
|
||||||
|
ctx.clean_prefix, _("<your_client_id_here>"), _("<your_client_secret_here>")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context):
|
async def youtubekey(self, ctx: commands.Context):
|
||||||
@@ -830,8 +976,7 @@ class Streams(commands.Cog):
|
|||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
is_rerun = False
|
is_rerun, is_schedule = False, False
|
||||||
is_schedule = False
|
|
||||||
if stream.__class__.__name__ == "TwitchStream":
|
if stream.__class__.__name__ == "TwitchStream":
|
||||||
await self.maybe_renew_twitch_bearer_token()
|
await self.maybe_renew_twitch_bearer_token()
|
||||||
embed, is_rerun = await stream.is_online()
|
embed, is_rerun = await stream.is_online()
|
||||||
@@ -839,6 +984,10 @@ class Streams(commands.Cog):
|
|||||||
elif stream.__class__.__name__ == "YoutubeStream":
|
elif stream.__class__.__name__ == "YoutubeStream":
|
||||||
embed, is_schedule = await stream.is_online()
|
embed, is_schedule = await stream.is_online()
|
||||||
|
|
||||||
|
elif stream.__class__.__name__ == "KickStream":
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
embed = await stream.is_online()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
embed = await stream.is_online()
|
embed = await stream.is_online()
|
||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
@@ -1016,6 +1165,8 @@ class Streams(commands.Cog):
|
|||||||
if _class.__name__ == "TwitchStream":
|
if _class.__name__ == "TwitchStream":
|
||||||
raw_stream["token"] = token.get("client_id")
|
raw_stream["token"] = token.get("client_id")
|
||||||
raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None)
|
raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None)
|
||||||
|
elif _class.__name__ == "KickStream":
|
||||||
|
raw_stream["token"] = self.kick_bearer_cache.get("access_token", None)
|
||||||
else:
|
else:
|
||||||
if _class.__name__ == "YoutubeStream":
|
if _class.__name__ == "YoutubeStream":
|
||||||
raw_stream["config"] = self.config
|
raw_stream["config"] = self.config
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ import contextlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from dateutil.parser import parse as parse_time
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime, timezone
|
||||||
from random import choice
|
from random import choice
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
from datetime import datetime, timedelta, timezone
|
from typing import ClassVar, List, Optional, Tuple
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from typing import ClassVar, Optional, List, Tuple
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
|
from dateutil.parser import parse as parse_time
|
||||||
|
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_number
|
||||||
|
|
||||||
from .errors import (
|
from .errors import (
|
||||||
APIError,
|
APIError,
|
||||||
OfflineStream,
|
InvalidKickCredentials,
|
||||||
InvalidTwitchCredentials,
|
InvalidTwitchCredentials,
|
||||||
InvalidYoutubeCredentials,
|
InvalidYoutubeCredentials,
|
||||||
|
OfflineStream,
|
||||||
StreamNotFound,
|
StreamNotFound,
|
||||||
YoutubeQuotaExceeded,
|
YoutubeQuotaExceeded,
|
||||||
)
|
)
|
||||||
from redbot.core.i18n import Translator
|
|
||||||
from redbot.core.utils.chat_formatting import humanize_number, humanize_timedelta
|
|
||||||
|
|
||||||
TWITCH_BASE_URL = "https://api.twitch.tv"
|
TWITCH_BASE_URL = "https://api.twitch.tv"
|
||||||
TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users"
|
TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users"
|
||||||
@@ -35,6 +37,10 @@ YOUTUBE_SEARCH_ENDPOINT = YOUTUBE_BASE_URL + "/search"
|
|||||||
YOUTUBE_VIDEOS_ENDPOINT = YOUTUBE_BASE_URL + "/videos"
|
YOUTUBE_VIDEOS_ENDPOINT = YOUTUBE_BASE_URL + "/videos"
|
||||||
YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
||||||
|
|
||||||
|
KICK_BASE_URL = "https://api.kick.com/public/v1/"
|
||||||
|
KICK_USERS_ENDPOINT = KICK_BASE_URL + "users"
|
||||||
|
KICK_CHANNELS_ENDPOINT = KICK_BASE_URL + "channels"
|
||||||
|
|
||||||
_ = Translator("Streams", __file__)
|
_ = Translator("Streams", __file__)
|
||||||
|
|
||||||
log = logging.getLogger("red.core.cogs.Streams")
|
log = logging.getLogger("red.core.cogs.Streams")
|
||||||
@@ -509,3 +515,108 @@ class PicartoStream(Stream):
|
|||||||
|
|
||||||
embed.set_footer(text=_("{adult}Category: {category} | Tags: {tags}").format(**data))
|
embed.set_footer(text=_("{adult}Category: {category} | Tags: {tags}").format(**data))
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
class KickStream(Stream):
|
||||||
|
token_name = "kick"
|
||||||
|
platform_name = "Kick"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.id = kwargs.pop("id", None)
|
||||||
|
self._display_name = None
|
||||||
|
self._token = kwargs.pop("token", None)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> Optional[str]:
|
||||||
|
return self._display_name or self.name
|
||||||
|
|
||||||
|
@display_name.setter
|
||||||
|
def display_name(self, value: str) -> None:
|
||||||
|
self._display_name = value
|
||||||
|
|
||||||
|
async def get_data(self, url: str, params: dict = {}) -> Tuple[Optional[int], dict]:
|
||||||
|
if self._token is None:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {self._token}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=headers, params=params, timeout=60) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
return resp.status, {}
|
||||||
|
|
||||||
|
data = await resp.json(encoding="utf-8")
|
||||||
|
return resp.status, data["data"][0] if data["data"] else []
|
||||||
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as exc:
|
||||||
|
log.warning("Connection error occurred when fetching Kick stream", exc_info=exc)
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
async def is_online(self):
|
||||||
|
channel_code, channel_data = await self.get_data(
|
||||||
|
KICK_CHANNELS_ENDPOINT, {"slug": self.name}
|
||||||
|
)
|
||||||
|
if not channel_data:
|
||||||
|
raise StreamNotFound()
|
||||||
|
|
||||||
|
if channel_code == 200:
|
||||||
|
if channel_data["stream"]["is_live"] is False:
|
||||||
|
raise OfflineStream()
|
||||||
|
|
||||||
|
self.id = channel_data["broadcaster_user_id"]
|
||||||
|
user_profile_data = await self._fetch_user_profile()
|
||||||
|
|
||||||
|
final_data = dict.fromkeys(
|
||||||
|
("game_name", "followers", "name", "slug", "profile_picture", "view_count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_profile_data is not None:
|
||||||
|
final_data["user_name"] = self.display_name = user_profile_data["name"]
|
||||||
|
final_data["profile_picture"] = user_profile_data["profile_picture"]
|
||||||
|
|
||||||
|
stream_data = channel_data["stream"]
|
||||||
|
final_data["game_name"] = channel_data["category"]["name"]
|
||||||
|
final_data["title"] = channel_data["stream_title"]
|
||||||
|
final_data["thumbnail_url"] = stream_data["thumbnail"]
|
||||||
|
final_data["view_count"] = stream_data["viewer_count"]
|
||||||
|
final_data["slug"] = channel_data["slug"]
|
||||||
|
|
||||||
|
return self.make_embed(final_data)
|
||||||
|
elif channel_code == 401:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
elif channel_code == 400:
|
||||||
|
raise StreamNotFound()
|
||||||
|
else:
|
||||||
|
raise APIError(channel_code, stream_data)
|
||||||
|
|
||||||
|
async def _fetch_user_profile(self):
|
||||||
|
code, data = await self.get_data(KICK_USERS_ENDPOINT, {"id": self.id})
|
||||||
|
if code == 200:
|
||||||
|
if not data:
|
||||||
|
raise StreamNotFound()
|
||||||
|
return data
|
||||||
|
elif code == 400:
|
||||||
|
raise StreamNotFound()
|
||||||
|
elif code == 401:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
else:
|
||||||
|
raise APIError(code, data)
|
||||||
|
|
||||||
|
def make_embed(self, data):
|
||||||
|
url = f"https://www.kick.com/{data['slug']}" if data["slug"] is not None else None
|
||||||
|
logo = (
|
||||||
|
data["profile_picture"] or "https://www.google.com/s2/favicons?domain=kick.com&sz=256"
|
||||||
|
)
|
||||||
|
status = data["title"] or _("Untitled broadcast")
|
||||||
|
embed = discord.Embed(title=status, url=url, color=0x00E701)
|
||||||
|
embed.set_author(name=data["user_name"])
|
||||||
|
embed.add_field(name=_("Total views"), value=humanize_number(data["view_count"]))
|
||||||
|
embed.set_thumbnail(url=logo)
|
||||||
|
if data["thumbnail_url"]:
|
||||||
|
embed.set_image(url=rnd(data["thumbnail_url"]))
|
||||||
|
if data["game_name"]:
|
||||||
|
embed.set_footer(text=_("Playing: ") + data["game_name"])
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{0.__class__.__name__}: {0.name} (ID: {0.id})>".format(self)
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
AUTHOR: Hanumat, Stolas, Hermes, thisisjvgrace, FunTrivia
|
||||||
|
When Kekule claimed to have hallucinated an image that revealed the structure of benzene, what did he say he saw?:
|
||||||
|
- Ouroboros
|
||||||
|
- An ouroboros
|
||||||
|
Which Noble Gas has the highest proton count?:
|
||||||
|
- Radon
|
||||||
|
- Rn
|
||||||
|
When an element undergoes beta radiation, what happens to its atomic number?:
|
||||||
|
- Increase
|
||||||
|
- It increases
|
||||||
|
In Rutherford’s ‘gold foil’ experiment, what was discovered?:
|
||||||
|
- The nucleus
|
||||||
|
- nucleus
|
||||||
|
What is the common term for an electrochemical cell in which terminals are connected to electrodes immersed in a solution of electrolytes?:
|
||||||
|
- Battery
|
||||||
|
- Batteries
|
||||||
|
- A battery
|
||||||
|
What chemical was suspected of potentially having caused the fatalities in what is now known as the 1976 Philadelphia Legionnaires disease outbreak due to the similarity in symptoms between poisoning from it and Legionaires disease?:
|
||||||
|
- Nickel carbonyl
|
||||||
|
- Tetranickelcarbonyl
|
||||||
|
What compound is produced by the Haber process?:
|
||||||
|
- Ammonia
|
||||||
|
What English chemist and physicist discovered hydrogen?:
|
||||||
|
- Cavendish
|
||||||
|
- Henry Cavendish
|
||||||
|
Which chemical causes the burning taste sensation when eating chilies?:
|
||||||
|
- Capsaicin
|
||||||
|
The periodic table’s abbreviation for mercury is Sn, based off an ancient name of the element. What is this short for?:
|
||||||
|
- Stannum
|
||||||
|
What is the proper mineral name of ‘fool’s gold’?:
|
||||||
|
- Pyrite
|
||||||
|
What is the only letter that doesnt appear in the periodic table?:
|
||||||
|
- J
|
||||||
|
What is the atomic number of Thulium?:
|
||||||
|
- 69
|
||||||
|
- Sixty-nine
|
||||||
|
- Sixty-Nine
|
||||||
|
What compound is Uranium in when it undergoes gaseous enrichment?:
|
||||||
|
- Uranium Hexafluoride
|
||||||
|
- Hex
|
||||||
|
What was the first synthetic element created in a laboratory?:
|
||||||
|
- Technetium
|
||||||
|
What piece of equipment, other than a glove box, is often used to perform air-free syntheses?:
|
||||||
|
- Schlenk line
|
||||||
|
Exposure to what chemical substance led to the death of Karen Wetterhaun despite her following proper safety procedures?:
|
||||||
|
- dimethylmercury
|
||||||
|
What is the temperature pressure combination where solid, liquid, and gas phases all exist simultaneously called?:
|
||||||
|
- Triple Point
|
||||||
|
- The Triple Point
|
||||||
|
What colour does potassium nitrate burn?:
|
||||||
|
- Violet
|
||||||
|
What salt substitute was quickly discontinued once it was discovered to be highly toxic?:
|
||||||
|
- Lithium Chloride
|
||||||
|
- LiCl
|
||||||
|
What metal has the highest melting point?:
|
||||||
|
- Tungsten
|
||||||
|
What colour is liquid oxygen?:
|
||||||
|
- Blue
|
||||||
|
Which one of these elements is NOT named after a place - Californium, Seaborgium, Berkelium, Dubnium, or Polonium?:
|
||||||
|
- Seaborgium
|
||||||
|
What is the classical name for mercury that is abbreviated as Hg on the periodic table?:
|
||||||
|
- Hydrargyrum
|
||||||
|
Alfred Nobel, the creator of the Nobel Prize, was most famous for which invention?:
|
||||||
|
- Dynamite
|
||||||
|
If an aromatic substituent is added directly adjacent to an already placed substituent, then the reaction was an ___-substitution:
|
||||||
|
- Ortho
|
||||||
|
What is the nickname of 1-Diazidocarbamoyl-5-azidotetrazole?:
|
||||||
|
- Azidoazide azide
|
||||||
|
What alternative name for element 105 was rejected by IUPAC in favor of ‘Dubnium”?:
|
||||||
|
- Hahnium
|
||||||
|
The most energetic form of radiation is _____ radiation:
|
||||||
|
- Gamma
|
||||||
|
When compared to other elements in terms of their radioactive half-lives, many isotopes of Uranium and Plutonium (among others) stand out as having unusually long half lives. What is this phenomenon referred to as?:
|
||||||
|
- The Island of Stability
|
||||||
|
- Island of Stability
|
||||||
|
What does RDX stand for?:
|
||||||
|
- Research Department Explosive
|
||||||
|
Who established the law of the conservation of mass?:
|
||||||
|
- Lavoisier
|
||||||
|
The four elements of Earth, Air, Wind, and Fire are well known. However, the alchemist Paracelsus rejected these in favor of three ‘principles’. What were these principles?:
|
||||||
|
- Salt, Sulfur, and Mercury
|
||||||
|
- Salt Sulfur Mercury
|
||||||
|
Ozone is an allotrope of which element?:
|
||||||
|
- Oxygen
|
||||||
|
What compound consists of a benzene ring with an attached amino group?:
|
||||||
|
- Aniline
|
||||||
|
What is the rarest naturally occurring element in the earth’s crust?:
|
||||||
|
- Astatine
|
||||||
|
What is the most abundant element in the universe?:
|
||||||
|
- Hydrogen
|
||||||
|
While thermonuclear weapons are often known as ‘hydrogen bombs’, the fusion fuel used are not strictly hydrogen, but rather a mixture of its isotopes, ______ and _______:
|
||||||
|
- deuteride tritium
|
||||||
|
- tritium dueteride
|
||||||
|
What is the only element known to have achieved superfluidity?:
|
||||||
|
- Helium
|
||||||
|
- He
|
||||||
|
Heavy water differs from regular water in having a different isotope of hydrogen. What is this isotope?:
|
||||||
|
- Deuterium
|
||||||
|
What is PVC short for?:
|
||||||
|
- Polyvinyl Chloride
|
||||||
|
What is the proper chemical name for baking soda?:
|
||||||
|
- Sodium bicarbonate
|
||||||
|
What was the alchemical name for mercury?:
|
||||||
|
- quicksilver
|
||||||
|
- quick silver
|
||||||
|
An alpha particle, produced by alpha decay, is identical to which element’s nucleus?:
|
||||||
|
- Helium
|
||||||
|
- He
|
||||||
|
Name one of the periodic tables’ in their original latin. (Only elements with different symbols than their common names will count.):
|
||||||
|
- plumbum
|
||||||
|
- natrium
|
||||||
|
- stanum
|
||||||
|
- kalium
|
||||||
|
- stibium
|
||||||
|
- cuprum
|
||||||
|
- hydrargyrum
|
||||||
|
In NMR spectroscopy, the chemical shifts are most often reported by the Delta scale. What alternative scale is still sometimes used?:
|
||||||
|
- Tau
|
||||||
|
When carbonyl groups are ligated to Molybdenum or other transition metals, the IR stretch of the carbonyl bond decreases in wavenumber due to what effect?:
|
||||||
|
- Pi backbonding
|
||||||
|
What is the last of the first 54 elements?:
|
||||||
|
- Xenon
|
||||||
|
What is responsible for the coloured photon emissions of neons?:
|
||||||
|
- excitation
|
||||||
|
What is used as the reference frame for NMR spectroscopy?:
|
||||||
|
- Tetramethylsilane
|
||||||
|
- TMS
|
||||||
|
DCM is shorthand for what solvent?:
|
||||||
|
- Dichloromethane
|
||||||
|
Green chemistry, chemistry practiced to preserve the environment, has a specific focus on eliminating __________ from reactions?:
|
||||||
|
- heavy metals
|
||||||
|
η is used in organometallic naming conventions to denote what?:
|
||||||
|
- Hapticity
|
||||||
|
- Ligand coordination
|
||||||
|
Tungsten was discovered in 1783 by chemists of which nation?:
|
||||||
|
- Spain
|
||||||
|
While Carbon-12 is the most common isotope of Carbon, it is unusable for NMR because it has no spin. What isotope of Carbon is most often used for NMR spectroscopy?:
|
||||||
|
- Carbon-13
|
||||||
|
- 13
|
||||||
|
The book Silent Spring, which criticizes pesticide use, focuses most heavily on what class of pesticides?:
|
||||||
|
- Organochlorides
|
||||||
|
This element is notable for its nontoxic oxide, used in cosmetology and general white pigments:
|
||||||
|
- bismuth
|
||||||
|
- Bi
|
||||||
|
When was the alchemical dream of turning lead into gold realized?:
|
||||||
|
- 1981
|
||||||
|
Organic compounds are primarily composed of these 3 elements:
|
||||||
|
- carbon hydrogen oxygen
|
||||||
|
- oxygen hydrogen carbon
|
||||||
|
- carbon oxygen hydrogen
|
||||||
|
- oxygen carbon hydrogen
|
||||||
|
- hydrogen oxygen carbon
|
||||||
|
- hydrogen carbon oxygen
|
||||||
|
What is the household name of the simplest ketone?:
|
||||||
|
- acetone
|
||||||
|
What is the household name of the simplest aldehyde?:
|
||||||
|
- formaldehyde
|
||||||
|
Chloroform and diethyl ether have both historically been used as…?:
|
||||||
|
- pain relievers
|
||||||
|
- anaesthetics
|
||||||
|
- anasthetics
|
||||||
|
A fifth state of matter, where quantum micro-effects are realized across the entire substance, is known as…?:
|
||||||
|
- bose-einstein condensate
|
||||||
|
- bose-einstein condensation
|
||||||
|
- bose einstein condensate
|
||||||
|
- bose einstein condensation
|
||||||
|
The Statue of Liberty, in New York, is green due to the patina process of which metal?:
|
||||||
|
- copper
|
||||||
|
- Cu
|
||||||
|
Which synthesis produces fuel oil and other liquid hydrocarbons?:
|
||||||
|
- Fischer-Tropsch
|
||||||
|
What compound gives the blood of the horseshoe crab its distinctive blue color?:
|
||||||
|
- Hemocyanin
|
||||||
|
What common solvent is produced from mixing nail polish, moonshine, and bleach, then reducing with pennies?:
|
||||||
|
- chloroform
|
||||||
|
What device was used by alchemists to distill materials?:
|
||||||
|
- Alembic
|
||||||
|
- the alembic
|
||||||
|
In electrochemistry, what does the abbreviation SHE refer to?:
|
||||||
|
- Standard Hydrogen Electrode
|
||||||
|
Which British chemist is best known for his book "The Sceptical Chymist"?:
|
||||||
|
- Robert Boyle
|
||||||
|
While almost all motion stops at 0 degrees Kelvin, some forms of motion still exist in minute quantities. Name one of these.:
|
||||||
|
- Vibrational
|
||||||
|
- Rotational
|
||||||
|
The release of what chemical is considered to mark the beginning of chemical weapons use in World War 1?:
|
||||||
|
- Chlorine
|
||||||
|
The description of cyanide as smelling like almonds originally referred to a specific kind of almond, which had its distinct smell from its naturally occurring cyanide content. What type of almonds are these?:
|
||||||
|
- Bitter
|
||||||
|
- bitter almonds
|
||||||
|
What chemical compound has been alleged to cause hallucinations in the drinkers of absinthe?:
|
||||||
|
- Thujone
|
||||||
|
This class of pesticides, believed to be particularly hazardous to bees, is named after the fact that its constituents share chemical similarities to nicotine.:
|
||||||
|
- Neonicotinoid
|
||||||
|
- Neonicotinoids
|
||||||
|
What is the full name of the solvent THF?:
|
||||||
|
- Tetrahydrofuran
|
||||||
|
Crown ethers are so named because their structures are what?:
|
||||||
|
- cyclic
|
||||||
|
Drinking a shot of which alcohol causes blindness?:
|
||||||
|
- methanol
|
||||||
|
What rule or principle determines electron filling order?:
|
||||||
|
- pauli exclusion
|
||||||
|
What rule or principle determines aromaticity:
|
||||||
|
- Hückel's rule
|
||||||
|
- 4n+2
|
||||||
|
- huckel’s rule
|
||||||
|
Which renowned French chemist was guillotined for tax fraud?:
|
||||||
|
- Lavoisier
|
||||||
|
"Which of the following has the weakest conjugate base?\nA) Ethanoic Acid\nB) Water\nC) Sodium Hydroxide\nD) Hydrochloric Acid":
|
||||||
|
- D
|
||||||
|
"Vital to soap production, it is widely used as \"caustic\" in industries during cleaning processes. Which of the following compounds am I describing?\nA) Potassium hydroxide\nB) Magnesium hydroxide\nC) Lithium hydroxide\nD) Sodium hydroxide":
|
||||||
|
- D
|
||||||
|
"what do scientists use to measure the acidity of a liquid?":
|
||||||
|
- pH scale
|
||||||
|
- pH
|
||||||
|
"Litmus is a common indicator used to tell whether something is an acid or a base. If you dip a piece of blue litmus paper into an acid, what color will it turn?":
|
||||||
|
- Red
|
||||||
|
"Which of the following is a base?\nA) Vinegar\nB) Tomato juice\nC) Common salt\nD) Caustic soda":
|
||||||
|
- D
|
||||||
|
"We often use vinegar for preserving and seasoning of foods. Vinegar is a highly diluted _______________ acid.":
|
||||||
|
- acetic
|
||||||
|
"Water can act both as an acid or a base according to Brønsted-Lowry theory, either giving out or taking a proton. What are such substances also known to be?":
|
||||||
|
- Amphiprotic
|
||||||
|
"What is the pH of water at 25 degrees celsius?":
|
||||||
|
- 7
|
||||||
|
- seven
|
||||||
|
"The 'fizz' in soft drinks is produced by adding which acid?":
|
||||||
|
- carbonic acid
|
||||||
|
"A base commonly known as 'Caustic Potash' is used for making soft soaps. What is the correct chemical formula for this base?":
|
||||||
|
- KOH
|
||||||
|
"Which acid is found in fruits such as limes and oranges and is classified as a weak acid?":
|
||||||
|
- citric acid
|
||||||
|
"If I react magnesium with hydrochloric acid, what gas is produced?":
|
||||||
|
- Hydrogen
|
||||||
|
"The chemical name for Vitamin C is __________ acid?":
|
||||||
|
- ascorbic
|
||||||
|
"A very well known gas is highly basic when dissolved in water but mildly basic in vapour form. It is used for making explosives such as TNT and fertilizers such as Urea. Which gas is it?":
|
||||||
|
- ammonia
|
||||||
|
"Which acid has the formula HCNO?":
|
||||||
|
- Cyanic Acid
|
||||||
|
"This acid is used in almost all car batteries and is also known as 'Oil of Vitriol'. Which acid is it?":
|
||||||
|
- sulphuric acid
|
||||||
|
- sulphuric
|
||||||
|
- H2SO4
|
||||||
|
"If you were to put some universal indicator in a very strong base, what color would it turn?":
|
||||||
|
- Purple
|
||||||
|
"Which of the following is neutral in water?\nA) Iodine\nB) Baking soda\nC) Diethyl amine\nD) Potassium chloride":
|
||||||
|
- D
|
||||||
|
"What is the so called name of a mixture of nitric acid and hydrochloric acid?":
|
||||||
|
- Aqua regia
|
||||||
|
"Which of the following salts cannot undergo hydrolysis?\nA) Sodium Oxalate\nB) Ammonium Nitrate\nC) Ammonium Ethanoate\nD) Magnesium Chloride":
|
||||||
|
- D
|
||||||
|
"Which acid is used in the production of soda?":
|
||||||
|
- carbonic acid
|
||||||
|
"Which acid is formed when milk is fermented to form yoghurt?":
|
||||||
|
- Lactic acid
|
||||||
|
- Lactic
|
||||||
|
"If a base dissolves in water, by what name is it better known?":
|
||||||
|
- Alkali
|
||||||
|
"Which of the following is a non-essential sulfur-containing amino acid?\nA) methionine\nB) cysteine\nC) valine\nD) glutamate":
|
||||||
|
- B
|
||||||
|
"Diets high in sugar and carbohydrates lead to a sharp rise in blood glucose. What term is given to such high blood glucose levels?":
|
||||||
|
- Hyperglycemia
|
||||||
|
"What is the most abundant protein in the human blood plasma?":
|
||||||
|
- albumin
|
||||||
|
"Which of the following amino acids is the smallest of all amino acids?\nA) Alanine\nB) Valine\nC) Glycine\nD) Serine":
|
||||||
|
- C
|
||||||
|
"Which one of these macromolecules is the most complex of all macromolecules?\nA) proteins\nB) lipids\nC) carbohydrates\nD) nucleic acids":
|
||||||
|
- A
|
||||||
|
"The individual units of proteins are called amino acids. What is the term for short chains of these amino acids?":
|
||||||
|
- Peptides
|
||||||
|
"What is the molecular formula for glucose?":
|
||||||
|
- C6 H12 O6
|
||||||
|
- C6H12O6
|
||||||
|
"Which amino acid can form a special covalent bond called a disulfide bond?":
|
||||||
|
- Cysteine
|
||||||
|
"Ceruloplasmin is a type of?":
|
||||||
|
- Metalloprotein
|
||||||
|
"What is the only macromolecule whose monomers don't undergo dehydration to become long-chain polymers?":
|
||||||
|
- lipids
|
||||||
|
"Lipids are useful as an energy source when glucose is scarce. Specifically, fatty acids undergo a process known as beta oxidation in order to generate energy. Where does beta oxidation take place in our cells?":
|
||||||
|
- Mitochondria
|
||||||
|
"Let's start with a simple combustion! I take some methanol (CH3OH) and burn it with ample oxygen (O2) available. Apart from carbon dioxide (CO2), what other reaction product do I get? Give the formula, not the name!":
|
||||||
|
- H2O
|
||||||
|
"What is the formula for cesium bromide?":
|
||||||
|
- CsBr
|
||||||
|
"Which of these has the formula CH4?\nA) methane\nB) butane\nC) pentane\nD) octane":
|
||||||
|
- A
|
||||||
|
"Nitrous Oxide also known as?":
|
||||||
|
- Laughing Gas
|
||||||
|
"Acetic acid also known as?":
|
||||||
|
- Vinegar
|
||||||
|
"Which chemical has the formula C4H10?":
|
||||||
|
- butane
|
||||||
|
"Abbreviation for Trinitrotoluene?":
|
||||||
|
- TNT
|
||||||
|
"H2TeO3 is the formula for what acid?":
|
||||||
|
- tellurous acid
|
||||||
|
"What is the common name for sodium bicarbonate?":
|
||||||
|
- baking soda
|
||||||
|
"What is the formula for ammonia?":
|
||||||
|
- NH3
|
||||||
|
"What is the correct formula for methyl alcohol?":
|
||||||
|
- CH3OH
|
||||||
|
"Acetylsalicylic acid also known as?":
|
||||||
|
- Aspirin
|
||||||
|
"Sodium hypochlorite also known as?":
|
||||||
|
- Bleach
|
||||||
|
"Dihydrogen monoxide also known as?":
|
||||||
|
- Water
|
||||||
|
"Ions that have a positive charge are called what?":
|
||||||
|
- cations
|
||||||
|
"If an isotope of boron has a mass number of eleven, how many neutrons does it have?":
|
||||||
|
- 6
|
||||||
|
- six
|
||||||
|
"Which of these is the odd one out? (Think chemical composition)\nA) Dry ice\nB) Iodine\nC) Sulfuric acid\nD) Water":
|
||||||
|
- B
|
||||||
|
"Which gas is pale yellow in colour, and is both highly toxic and corrosive?":
|
||||||
|
- fluorine
|
||||||
|
"Methylated spirits is a very common household chemical - but what is its main component?":
|
||||||
|
- ethanol
|
||||||
|
- alcohol
|
||||||
|
"What substance goes milky/cloudy when you bubble carbon dioxide into it?":
|
||||||
|
- Lime water
|
||||||
|
"Which of the following is an ore of Lead?\nA) Garnierite\nB) Cerrusite\nC) Pentlandite\nD) Nicolite":
|
||||||
|
- B
|
||||||
|
"Which of the following is a non-electrolyte?\nA) Calcium hydroxide\nB) Potassium chloride\nC) Ethyl alcohol\nD) Sulfuric acid":
|
||||||
|
- C
|
||||||
|
"Which substance is primarily used as a fire extinguisher for fires originating from electrical equipment?":
|
||||||
|
- carbon dioxide
|
||||||
|
- CO2
|
||||||
|
"Which of the following people won the Nobel Prize in Chemistry?\nA) Marie Curie\nB) Pierre Curie\nC) Henri Becquerel\nD) All of these":
|
||||||
|
- A
|
||||||
|
"Isotopes of the same element have different numbers of what?":
|
||||||
|
- Neutrons
|
||||||
|
"Which noble gas, discovered in 1898, is characterized by its bright orange and green spectral lines?":
|
||||||
|
- krypton
|
||||||
|
"I have a packet of French fries. My friend says that if I put a little acetic acid in the fries, they will taste better. I am astounded. I ask him if my tongue will burn. Then my friend laughs and tells me acetic acid is the chemical name for...":
|
||||||
|
- vinegar
|
||||||
|
"Caro's acid contains sulphur, oxygen and hydrogen. What is its chemical formula?":
|
||||||
|
- H2SO5
|
||||||
|
"What is the main component of biogas and natural gas?":
|
||||||
|
- Methane
|
||||||
|
"Which one of the following is an alkali?\nA) Strontium hydroxide\nB) Sodium hydroxide\nC) Rubidium hydroxide\nD) Aluminium hydroxide":
|
||||||
|
- B
|
||||||
|
"In which year did Mendeleev publish his version of the periodic table?":
|
||||||
|
- 1869
|
||||||
|
"What ion is necessary for forming strong bones?":
|
||||||
|
- Calcium
|
||||||
|
"Which of these solutions is capable of dissolving gold?\nA) Acetone\nB) Water\nC) Aqua regia\nD) Battery acid":
|
||||||
|
- C
|
||||||
|
"Which of these organic compounds is in a different class than the rest?\nA) Ethene\nB) Propene\nC) Pentene\nD) Benzene":
|
||||||
|
- D
|
||||||
|
"Which chemical element is named after the creator of dynamite?":
|
||||||
|
- Nobelium
|
||||||
|
"This gas becomes liquid at -196C. It is essential to life, and is a component of all proteins.":
|
||||||
|
- Nitrogen
|
||||||
|
"What colour flame does Lithium have when it is lit?":
|
||||||
|
- crimson
|
||||||
|
"According to Boyle's Law, if the volume of a gas decreases, its pressure will ________ (assuming temperature is constant)":
|
||||||
|
- increase
|
||||||
|
"Acetic acid and formic acid are mineral acids or organic acids?":
|
||||||
|
- organic
|
||||||
|
- organic acid
|
||||||
|
- organic acids
|
||||||
|
"What is another name for Mercury?":
|
||||||
|
- Quicksilver
|
||||||
|
"Which of these formulas can produce zinc chloride?\nA) acetic acid + zinc\nB) sulphuric acid + zinc\nC) nitric acid + zinc\nD) hydrochloric acid + zinc":
|
||||||
|
- D
|
||||||
|
"What ion of a highly poisonous substance is composed of an atom of carbon plus an atom of nitrogen?":
|
||||||
|
- cyanide
|
||||||
|
"This gas is the second most abundant element in the universe, and has the lowest melting point of any element.":
|
||||||
|
- helium
|
||||||
|
"Kevlar is a type of what?":
|
||||||
|
- polyamide
|
||||||
|
"What is the general building block common to all proteins?":
|
||||||
|
- amino acids
|
||||||
|
- amino acid
|
||||||
|
"This pigment is what affects the colour of the hair, skin and eyes in humans and other mammals. It also helps protect the skin from the Sun's harmful ultra-violet (UV) rays. It is what helps human skin tan in the sun. What is the name of this pigment?":
|
||||||
|
- melanin
|
||||||
|
"Nail polish remover is something that girls just can't do without. Which is the major active ingredient in nail polish remover?":
|
||||||
|
- acetone
|
||||||
|
"What element is graphite made up of?":
|
||||||
|
- carbon
|
||||||
|
"Coal is formed due to the process of?":
|
||||||
|
- Carbonization
|
||||||
|
"In what polyatomic ion does nitrogen have a valency of 5?":
|
||||||
|
- nitrate
|
||||||
|
"What does pH stand for?":
|
||||||
|
- Potential hydrogen
|
||||||
|
"Care must be taken when handling this radioactive gas. Long term exposure, especially in smokers, can cause lung cancer. Which gas is it?":
|
||||||
|
- radon
|
||||||
|
"Beta-carotene, the pigment that gives carrots their orange colour, is an important source of which vitamin that helps us see?":
|
||||||
|
- A
|
||||||
|
- Vitamin A
|
||||||
|
"*Water* is what all life in the world depends on. Is it an element, compound or mixture?":
|
||||||
|
- Compound
|
||||||
|
"Which variety of coal contains the highest percentage of carbon?":
|
||||||
|
- Anthracite
|
||||||
|
"What is the percentage of silver in German Silver?":
|
||||||
|
- 0
|
||||||
|
- zero
|
||||||
|
"Oil of vitriol is the historic name for which acid?":
|
||||||
|
- sulfuric acid
|
||||||
|
- sulfuric
|
||||||
|
"Iron is galvanized to prevent rusting and corrosion. Which metal is primarily used for galvanization?":
|
||||||
|
- zinc
|
||||||
|
"Enter organic chemistry, the study of all things natural and the base on which biology is founded. This deceptively simple study is based on molecules which all contain one element. Which one?":
|
||||||
|
- carbon
|
||||||
|
"Please choose what gas all of these four cases produce? The four cases are acid rain on a limestone statue, a candle burning, a dog panting and fermenting grapes to make wine.":
|
||||||
|
- Carbon dioxide
|
||||||
|
- CO2
|
||||||
|
"In nature, this gas is only ever found combined with other elements. In its pure state however, it is fatal if inhaled.":
|
||||||
|
- chlorine
|
||||||
|
" The change from the solid state to the gaseous state is called?":
|
||||||
|
- Sublimation
|
||||||
|
"Which of the following would you most likely sprinkle on your food?\nA) NaOH\nB) HF\nC) Mg\nD) KCl":
|
||||||
|
- D
|
||||||
|
"Can you smell waxy menthol? With this sharp smell! It's enough as a counterirritant. Used for throat infections! Guess the name?":
|
||||||
|
- Peppermint
|
||||||
|
"Marie and Pierre Curie extracted radium from which ore?":
|
||||||
|
- Uraninite
|
||||||
|
"Plaster of Paris is commonly used for making moulds and statues. Which compound is used to make Plaster of Paris?":
|
||||||
|
- calcium sulphate
|
||||||
|
"Which metal has the lowest vapor pressure and the highest melting point?":
|
||||||
|
- tungsten
|
||||||
|
"The digestive process breaks down food into small molecules. Carbohydrates are broken down into glucose and other sugars, which are in turn degraded further in preparation for entry into the citric acid cycle. The citric acid cycle produces energy and what gas?":
|
||||||
|
- carbon dioxide
|
||||||
|
- CO2
|
||||||
|
"What chemical is made during 'The Haber Process'?":
|
||||||
|
- ammonia
|
||||||
|
"Who published two laws of electrolysis in the 1830s?":
|
||||||
|
- Faraday
|
||||||
|
"What precious metal is obtained from the metallic white mineral called Sperrylite?":
|
||||||
|
- Platinum
|
||||||
|
"The water of a swimming pool is disinfected by passing which nonmetal through the water?":
|
||||||
|
- chlorine
|
||||||
|
"What is the name of the bleaching agent in commercially available tooth whitening products?":
|
||||||
|
- Carbamide peroxide
|
||||||
|
- urea peroxide
|
||||||
|
- urea hydrogen peroxide
|
||||||
|
- percarbamide
|
||||||
|
"Which of these is liquid at room temperature?\nA) Chlorine\nB) Iodine\nC) Oxygen\nD) Bromine":
|
||||||
|
- D
|
||||||
|
"What is the name of this gas is used with helium in making gas lasers, but is no doubt better known for its use in lighting?":
|
||||||
|
- neon
|
||||||
|
"Hydrogen sulfide has how many hydrogen atoms?":
|
||||||
|
- 2
|
||||||
|
- two
|
||||||
|
"Which metal is the most malleable?":
|
||||||
|
- gold
|
||||||
|
"The spider-webs we see around us are constructed out of fine, silky threads of a certain variety of chemical compounds. Which compounds are these?":
|
||||||
|
- proteins
|
||||||
|
- protein
|
||||||
|
"A magnesium ribbon is burnt in the air, to form magnesium oxide (MgO). Is this oxide an element, compound or a mixture?":
|
||||||
|
- compound
|
||||||
|
"The name of this naturally radioactive halogen is?":
|
||||||
|
- Astatine
|
||||||
|
"Brass is an?":
|
||||||
|
- alloy
|
||||||
|
"Which white compound is used as a food seasoning and preservative, and can be *mined* from water?":
|
||||||
|
- Salt
|
||||||
|
- NaCl
|
||||||
|
- Sodium Chloride
|
||||||
|
"Tamarind tastes sour because it contains which acid?":
|
||||||
|
- tartaric acid
|
||||||
|
"Which of the following ions is generally considered insoluble?\nA) Sodium\nB) Potassium\nC) Ammonium\nD) Carbonate":
|
||||||
|
- D
|
||||||
|
"Which of the below compounds is a compound once found in many skin whitening products but which was banned from use by the European Union due to its suspected carcinogenic properties?\nA) Salicylic acid\nB) Sodium benzoate\nC) Hydroquinone\nD) Linalool":
|
||||||
|
- C
|
||||||
|
"Who is credited with coining the idea of Triads?":
|
||||||
|
- Dobereiner
|
||||||
|
"E. coli bacteria also manufacture a certain vitamin for us in our bodies; which one is it?":
|
||||||
|
- Vitamin K
|
||||||
|
- K
|
||||||
|
"The air we breathe - is it an element, compound or mixture?":
|
||||||
|
- mixture
|
||||||
|
"How many Hydrogen atoms are present in a methane molecule?":
|
||||||
|
- 4
|
||||||
|
- four
|
||||||
|
"If you want to make clean seawater pure enough to drink, that water must first be?":
|
||||||
|
- distilled
|
||||||
|
"Which gas is usually mixed with oxygen in scuba diving tanks for breathing?":
|
||||||
|
- Helium
|
||||||
|
- Nitrogen
|
||||||
|
"What phase change occurs when water becomes steam?":
|
||||||
|
- vaporization
|
||||||
|
"What is the chemical formula for table salt?":
|
||||||
|
- NaCl
|
||||||
|
"Photosynthesis is a (bio)chemical process occurring in plants. The plants use sunlight as an energy source to turn carbon dioxide, water and some other nutrients into new plant material and a vital gas for all air breathing organisms (including us!). What is this gas?":
|
||||||
|
- Oxygen
|
||||||
|
"Where might you find americium-241 at in your house?":
|
||||||
|
- Smoke detectors
|
||||||
|
"When alcohol and acid mixes, what organic material forms?":
|
||||||
|
- Ester
|
||||||
|
"This gas when combined with hydrogen tends to make things wet, and we would die without it!":
|
||||||
|
- Oxygen
|
||||||
|
"Poison ivy is one of the most harmful plants; its main potent ingredient is a certain oil. Which oil is this?":
|
||||||
|
- urushiol
|
||||||
|
"Which of the following is an endothermic reaction?\nA) dissolving sodium hydroxide in water\nB) Haber process\nC) neutralization\nD) dissolving sodium chloride in water":
|
||||||
|
- D
|
||||||
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"
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.commands import UserInputOptional, RawUserIdConverter
|
from redbot.core.commands import UserInputOptional, RawUserIdConverter
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils import AsyncIter
|
from redbot.core.utils import AsyncIter
|
||||||
|
from redbot.core.utils.chat_formatting import box, pagify, warning
|
||||||
from redbot.core.utils.views import ConfirmView
|
from redbot.core.utils.views import ConfirmView
|
||||||
from redbot.core.utils.chat_formatting import warning, pagify
|
|
||||||
from redbot.core.utils.menus import menu
|
from redbot.core.utils.menus import menu
|
||||||
|
|
||||||
|
|
||||||
@@ -598,7 +598,7 @@ class Warnings(commands.Cog):
|
|||||||
channel=None,
|
channel=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.group(invoke_without_command=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin()
|
@commands.admin()
|
||||||
async def warnings(self, ctx: commands.Context, member: Union[discord.Member, int]):
|
async def warnings(self, ctx: commands.Context, member: Union[discord.Member, int]):
|
||||||
@@ -640,6 +640,40 @@ class Warnings(commands.Cog):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@warnings.command(name="server", aliases=["guild"])
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin()
|
||||||
|
async def warnings_server(self, ctx: commands.Context):
|
||||||
|
"""List all members with warnings in this server."""
|
||||||
|
settings = await self.config.all_members(guild=ctx.guild)
|
||||||
|
body_parts = []
|
||||||
|
pages = []
|
||||||
|
count_len = len(_("Count")) + 2
|
||||||
|
points_len = len(_("Points")) + 2
|
||||||
|
for member_id, warnings in settings.items():
|
||||||
|
count_len = max(count_len, len(str(len(warnings["warnings"]))))
|
||||||
|
points_len = max(points_len, len(str(warnings["total_points"])))
|
||||||
|
for member_id, warnings in settings.items():
|
||||||
|
member = ctx.guild.get_member(member_id)
|
||||||
|
member_formatted = member_formatted = member.display_name if member else str(member_id)
|
||||||
|
count = len(warnings["warnings"])
|
||||||
|
points = warnings["total_points"]
|
||||||
|
body_parts.append(f" {count:<{count_len}}{points:<{points_len}}{member_formatted:2}")
|
||||||
|
body = "\n".join(body_parts)
|
||||||
|
header = "# {count:{count_len}}{point:{points_len}}{name:2}\n".format(
|
||||||
|
count=_("Count"),
|
||||||
|
count_len=count_len,
|
||||||
|
point=_("Points"),
|
||||||
|
points_len=points_len,
|
||||||
|
name=_("Name"),
|
||||||
|
)
|
||||||
|
for page in pagify(body, shorten_by=len(header) + 20):
|
||||||
|
pages.append(box(header + page, lang="md"))
|
||||||
|
if not pages:
|
||||||
|
await ctx.send(_("This server has no warnings yet."))
|
||||||
|
return
|
||||||
|
await menu(ctx, pages)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def mywarnings(self, ctx: commands.Context):
|
async def mywarnings(self, ctx: commands.Context):
|
||||||
|
|||||||
+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()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union, cast
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union
|
||||||
|
|
||||||
from redbot import VersionInfo, version_info as red_version_info
|
from packaging.version import Version
|
||||||
|
|
||||||
from . import installable
|
from . import installable
|
||||||
from .log import log
|
from .log import log
|
||||||
@@ -21,6 +22,7 @@ class UseDefault:
|
|||||||
|
|
||||||
# sentinel value
|
# sentinel value
|
||||||
USE_DEFAULT = UseDefault()
|
USE_DEFAULT = UseDefault()
|
||||||
|
RED_TAG_READY_PATTERN = re.compile(r"^red-(?:[3-9]|[1-9][0-9]+)\.(?:[1-9][0-9]*)-ready$")
|
||||||
|
|
||||||
|
|
||||||
def ensure_tuple_of_str(
|
def ensure_tuple_of_str(
|
||||||
@@ -67,38 +69,40 @@ def ensure_str(info_file: Path, key_name: str, value: Union[Any, UseDefault]) ->
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def ensure_red_version_info(
|
def create_ensure_red_version(default: Version) -> EnsureCallable:
|
||||||
info_file: Path, key_name: str, value: Union[Any, UseDefault]
|
def ensure_red_version(
|
||||||
) -> VersionInfo:
|
info_file: Path, key_name: str, value: Union[Any, UseDefault]
|
||||||
default = red_version_info
|
) -> Version:
|
||||||
if value is USE_DEFAULT:
|
if value is USE_DEFAULT:
|
||||||
return default
|
return default
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
log.warning(
|
log.warning(
|
||||||
"Invalid value of '%s' key (expected str, got %s)"
|
"Invalid value of '%s' key (expected str, got %s)"
|
||||||
" in JSON information file at path: %s",
|
" in JSON information file at path: %s",
|
||||||
key_name,
|
key_name,
|
||||||
type(value).__name__,
|
type(value).__name__,
|
||||||
info_file,
|
info_file,
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
version_info = VersionInfo.from_str(value)
|
version_info = Version(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Invalid value of '%s' key (given value isn't a valid version string)"
|
"Invalid value of '%s' key (given value isn't a valid version string)"
|
||||||
" in JSON information file at path: %s",
|
" in JSON information file at path: %s",
|
||||||
key_name,
|
key_name,
|
||||||
info_file,
|
info_file,
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
return version_info
|
return version_info
|
||||||
|
|
||||||
|
return ensure_red_version
|
||||||
|
|
||||||
|
|
||||||
def ensure_python_version_info(
|
def ensure_python_version(
|
||||||
info_file: Path, key_name: str, value: Union[Any, UseDefault]
|
info_file: Path, key_name: str, value: Union[Any, UseDefault]
|
||||||
) -> Tuple[int, int, int]:
|
) -> Version:
|
||||||
default = (3, 5, 1)
|
default = Version("3.5.1")
|
||||||
if value is USE_DEFAULT:
|
if value is USE_DEFAULT:
|
||||||
return default
|
return default
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
@@ -130,7 +134,7 @@ def ensure_python_version_info(
|
|||||||
info_file,
|
info_file,
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
return cast(Tuple[int, int, int], tuple(value))
|
return Version(".".join(map(str, value)))
|
||||||
|
|
||||||
|
|
||||||
def ensure_bool(
|
def ensure_bool(
|
||||||
@@ -201,6 +205,48 @@ def ensure_installable_type(
|
|||||||
return installable.InstallableType.UNKNOWN
|
return installable.InstallableType.UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tags(info_file: Path, key_name: str, value: Union[Any, UseDefault]) -> Tuple[str, ...]:
|
||||||
|
default: Tuple[str, ...] = ()
|
||||||
|
if value is USE_DEFAULT:
|
||||||
|
return default
|
||||||
|
if not isinstance(value, list):
|
||||||
|
log.warning(
|
||||||
|
"Invalid value of '%s' key (expected list, got %s)"
|
||||||
|
" in JSON information file at path: %s",
|
||||||
|
key_name,
|
||||||
|
type(value).__name__,
|
||||||
|
info_file,
|
||||||
|
)
|
||||||
|
return default
|
||||||
|
valid_tags = []
|
||||||
|
for item in value:
|
||||||
|
if not isinstance(item, str):
|
||||||
|
log.warning(
|
||||||
|
"Invalid item in '%s' list (expected str, got %s)"
|
||||||
|
" in JSON information file at path: %s",
|
||||||
|
key_name,
|
||||||
|
type(item).__name__,
|
||||||
|
info_file,
|
||||||
|
)
|
||||||
|
return default
|
||||||
|
# `red-` tags are reserved for informational metadata we only support a subset of tags
|
||||||
|
if not item.startswith("red-"):
|
||||||
|
valid_tags.append(item)
|
||||||
|
continue
|
||||||
|
if RED_TAG_READY_PATTERN.match(item):
|
||||||
|
valid_tags.append(item)
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
"Invalid value in '%s' list (tag starts with the reserved 'red-' prefix"
|
||||||
|
" but does not use the only supported reserved tag format: 'red-X.Y-ready')"
|
||||||
|
" in JSON information file at path: %s",
|
||||||
|
key_name,
|
||||||
|
info_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(value)
|
||||||
|
|
||||||
|
|
||||||
EnsureCallable = Callable[[Path, str, Union[Any, UseDefault]], Any]
|
EnsureCallable = Callable[[Path, str, Union[Any, UseDefault]], Any]
|
||||||
SchemaType = Dict[str, EnsureCallable]
|
SchemaType = Dict[str, EnsureCallable]
|
||||||
|
|
||||||
@@ -211,14 +257,18 @@ REPO_SCHEMA: SchemaType = {
|
|||||||
"short": ensure_str,
|
"short": ensure_str,
|
||||||
}
|
}
|
||||||
INSTALLABLE_SCHEMA: SchemaType = {
|
INSTALLABLE_SCHEMA: SchemaType = {
|
||||||
"min_bot_version": ensure_red_version_info,
|
"min_bot_version": create_ensure_red_version(Version("0.0.dev0")),
|
||||||
"max_bot_version": ensure_red_version_info,
|
# Using little-known version epoch feature to represent something that,
|
||||||
"min_python_version": ensure_python_version_info,
|
# for all practical purposes, will be considered higher than any version number
|
||||||
|
# that we may ever have.
|
||||||
|
# https://packaging.python.org/en/latest/specifications/version-specifiers/#version-epochs
|
||||||
|
"max_bot_version": create_ensure_red_version(Version("99999!99999.99999.post99999+hi.mom")),
|
||||||
|
"min_python_version": ensure_python_version,
|
||||||
"hidden": ensure_bool,
|
"hidden": ensure_bool,
|
||||||
"disabled": ensure_bool,
|
"disabled": ensure_bool,
|
||||||
"required_cogs": ensure_required_cogs_mapping,
|
"required_cogs": ensure_required_cogs_mapping,
|
||||||
"requirements": ensure_tuple_of_str,
|
"requirements": ensure_tuple_of_str,
|
||||||
"tags": ensure_tuple_of_str,
|
"tags": ensure_tags,
|
||||||
"type": ensure_installable_type,
|
"type": ensure_installable_type,
|
||||||
"end_user_data_statement": ensure_str,
|
"end_user_data_statement": ensure_str,
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,21 @@ 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
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
from .log import log
|
from .log import log
|
||||||
from .info_schemas import INSTALLABLE_SCHEMA, update_mixin
|
from .info_schemas import INSTALLABLE_SCHEMA, update_mixin
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
|
|
||||||
from redbot.core import VersionInfo
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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
|
||||||
@@ -45,12 +44,12 @@ class Installable(RepoJSONMixin):
|
|||||||
Name(s) of the author(s).
|
Name(s) of the author(s).
|
||||||
end_user_data_statement : `str`
|
end_user_data_statement : `str`
|
||||||
End user data statement of the module.
|
End user data statement of the module.
|
||||||
min_bot_version : `VersionInfo`
|
min_bot_version : `packaging.version.Version`
|
||||||
The minimum bot version required for this Installable.
|
The minimum bot version required for this Installable.
|
||||||
max_bot_version : `VersionInfo`
|
max_bot_version : `packaging.version.Version`
|
||||||
The maximum bot version required for this Installable.
|
The maximum bot version required for this Installable.
|
||||||
Ignored if `min_bot_version` is newer than `max_bot_version`.
|
Ignored if `min_bot_version` is newer than `max_bot_version`.
|
||||||
min_python_version : `tuple` of `int`
|
min_python_version : `packaging.version.Version`
|
||||||
The minimum python version required for this cog.
|
The minimum python version required for this cog.
|
||||||
hidden : `bool`
|
hidden : `bool`
|
||||||
Whether or not this cog will be hidden from the user when they use
|
Whether or not this cog will be hidden from the user when they use
|
||||||
@@ -88,9 +87,9 @@ class Installable(RepoJSONMixin):
|
|||||||
self.commit = commit
|
self.commit = commit
|
||||||
|
|
||||||
self.end_user_data_statement: str
|
self.end_user_data_statement: str
|
||||||
self.min_bot_version: VersionInfo
|
self.min_bot_version: Version
|
||||||
self.max_bot_version: VersionInfo
|
self.max_bot_version: Version
|
||||||
self.min_python_version: Tuple[int, int, int]
|
self.min_python_version: Version
|
||||||
self.hidden: bool
|
self.hidden: bool
|
||||||
self.disabled: bool
|
self.disabled: bool
|
||||||
self.required_cogs: Dict[str, str] # Cog name -> repo URL
|
self.required_cogs: Dict[str, str] # Cog name -> repo URL
|
||||||
@@ -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
@@ -2,7 +2,7 @@ import enum
|
|||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from .. import data_manager
|
from .. import data_manager
|
||||||
from .base import IdentifierData, BaseDriver, ConfigCategory
|
from .base import IdentifierData, BaseDriver, ConfigCategory, MissingExtraRequirements
|
||||||
from .json import JsonDriver
|
from .json import JsonDriver
|
||||||
from .postgres import PostgresDriver
|
from .postgres import PostgresDriver
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ __all__ = [
|
|||||||
"get_driver_class_include_old",
|
"get_driver_class_include_old",
|
||||||
"ConfigCategory",
|
"ConfigCategory",
|
||||||
"IdentifierData",
|
"IdentifierData",
|
||||||
|
"MissingExtraRequirements",
|
||||||
"BaseDriver",
|
"BaseDriver",
|
||||||
"JsonDriver",
|
"JsonDriver",
|
||||||
"PostgresDriver",
|
"PostgresDriver",
|
||||||
|
|||||||
+37
-74
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import platform
|
import platform
|
||||||
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
@@ -9,8 +10,9 @@ from typing import Tuple
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
import importlib.metadata
|
import redbot_update
|
||||||
from packaging.requirements import Requirement
|
from packaging.specifiers import SpecifierSet
|
||||||
|
from packaging.version import Version
|
||||||
from redbot.core import data_manager
|
from redbot.core import data_manager
|
||||||
|
|
||||||
from redbot.core.bot import ExitCodes
|
from redbot.core.bot import ExitCodes
|
||||||
@@ -19,14 +21,13 @@ from redbot.core.i18n import (
|
|||||||
Translator,
|
Translator,
|
||||||
set_contextual_locales_from_guild,
|
set_contextual_locales_from_guild,
|
||||||
)
|
)
|
||||||
from .. import __version__ as red_version, version_info as red_version_info
|
from .. import __version__ as red_version
|
||||||
from . import commands
|
from . import commands
|
||||||
from ._config import get_latest_confs
|
from ._config import get_latest_confs
|
||||||
from .utils._internal_utils import (
|
from .utils._internal_utils import (
|
||||||
fuzzy_command_search,
|
fuzzy_command_search,
|
||||||
format_fuzzy_results,
|
format_fuzzy_results,
|
||||||
expected_version,
|
fetch_latest_red_version,
|
||||||
fetch_latest_red_version_info,
|
|
||||||
send_to_owners_with_prefix_replaced,
|
send_to_owners_with_prefix_replaced,
|
||||||
)
|
)
|
||||||
from .utils.chat_formatting import inline, format_perms_list
|
from .utils.chat_formatting import inline, format_perms_list
|
||||||
@@ -52,7 +53,7 @@ ______ _ ______ _ _ ______ _
|
|||||||
_ = Translator(__name__, __file__)
|
_ = Translator(__name__, __file__)
|
||||||
|
|
||||||
|
|
||||||
def get_outdated_red_messages(pypi_version: str, py_version_req: str) -> Tuple[str, str]:
|
def get_outdated_red_messages(pypi_version: str) -> Tuple[str, str]:
|
||||||
outdated_red_message = _(
|
outdated_red_message = _(
|
||||||
"Your Red instance is out of date! {} is the current version, however you are using {}!"
|
"Your Red instance is out of date! {} is the current version, however you are using {}!"
|
||||||
).format(pypi_version, red_version)
|
).format(pypi_version, red_version)
|
||||||
@@ -61,7 +62,6 @@ def get_outdated_red_messages(pypi_version: str, py_version_req: str) -> Tuple[s
|
|||||||
f"[red]!!![/red]Version [cyan]{pypi_version}[/] is available, "
|
f"[red]!!![/red]Version [cyan]{pypi_version}[/] is available, "
|
||||||
f"but you're using [cyan]{red_version}[/][red]!!![/red]"
|
f"but you're using [cyan]{red_version}[/][red]!!![/red]"
|
||||||
)
|
)
|
||||||
current_python = platform.python_version()
|
|
||||||
extra_update = _(
|
extra_update = _(
|
||||||
"\n\nWhile the following command should work in most scenarios as it is "
|
"\n\nWhile the following command should work in most scenarios as it is "
|
||||||
"based on your current OS, environment, and Python version, "
|
"based on your current OS, environment, and Python version, "
|
||||||
@@ -70,64 +70,15 @@ def get_outdated_red_messages(pypi_version: str, py_version_req: str) -> Tuple[s
|
|||||||
"needs to be done during the update.**"
|
"needs to be done during the update.**"
|
||||||
).format(docs="https://docs.discord.red/en/stable/update_red.html")
|
).format(docs="https://docs.discord.red/en/stable/update_red.html")
|
||||||
|
|
||||||
if not expected_version(current_python, py_version_req):
|
redbot_update_bin = redbot_update.find_redbot_update_bin()
|
||||||
extra_update += _(
|
is_windows = platform.system() == "Windows"
|
||||||
"\n\nYou have Python `{py_version}` and this update "
|
update_command = f'"{redbot_update_bin}"' if is_windows else shlex.quote(redbot_update_bin)
|
||||||
"requires `{req_py}`; you cannot simply run the update command.\n\n"
|
|
||||||
"You will need to follow the update instructions in our docs above, "
|
|
||||||
"if you still need help updating after following the docs go to our "
|
|
||||||
"#support channel in <https://discord.gg/red>"
|
|
||||||
).format(py_version=current_python, req_py=py_version_req)
|
|
||||||
outdated_red_message += extra_update
|
|
||||||
return outdated_red_message, rich_outdated_message
|
|
||||||
|
|
||||||
red_dist = importlib.metadata.distribution("Red-DiscordBot")
|
|
||||||
installed_extras = red_dist.metadata.get_all("Provides-Extra")
|
|
||||||
installed_extras.remove("dev")
|
|
||||||
installed_extras.remove("all")
|
|
||||||
distributions = {}
|
|
||||||
for req_str in red_dist.requires:
|
|
||||||
req = Requirement(req_str)
|
|
||||||
if req.marker is None or req.marker.evaluate():
|
|
||||||
continue
|
|
||||||
for extra in reversed(installed_extras):
|
|
||||||
if not req.marker.evaluate({"extra": extra}):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check that the requirement is met.
|
|
||||||
# This is a bit simplified for our purposes and does not check
|
|
||||||
# whether the requirements of our requirements are met as well.
|
|
||||||
# This could potentially be an issue if we'll ever depend on
|
|
||||||
# a dependency's extra in our extra when we already depend on that
|
|
||||||
# in our base dependencies. However, considering that right now, all
|
|
||||||
# our dependencies are also fully pinned, this should not ever matter.
|
|
||||||
if req.name in distributions:
|
|
||||||
dist = distributions[req.name]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
dist = importlib.metadata.distribution(req.name)
|
|
||||||
except importlib.metadata.PackageNotFoundError:
|
|
||||||
dist = None
|
|
||||||
distributions[req.name] = dist
|
|
||||||
if dist is None or not req.specifier.contains(dist.version, prereleases=True):
|
|
||||||
installed_extras.remove(extra)
|
|
||||||
|
|
||||||
if installed_extras:
|
|
||||||
package_extras = f"[{','.join(installed_extras)}]"
|
|
||||||
else:
|
|
||||||
package_extras = ""
|
|
||||||
|
|
||||||
extra_update += _(
|
extra_update += _(
|
||||||
"\n\nTo update your bot, first shutdown your bot"
|
"\n\nTo update your bot, first shutdown your bot"
|
||||||
" then open a window of {console} (Not as admin) and run the following:"
|
" then open a window of {console} (Not as admin) and run the following: {command}"
|
||||||
"{command_1}\n"
|
|
||||||
"Once you've started up your bot again, we recommend that"
|
|
||||||
" you update any installed 3rd-party cogs with this command in Discord:"
|
|
||||||
"{command_2}"
|
|
||||||
).format(
|
).format(
|
||||||
console=_("Command Prompt") if platform.system() == "Windows" else _("Terminal"),
|
console=_("Command Prompt") if is_windows else _("Terminal"),
|
||||||
command_1=f'```"{sys.executable}" -m pip install -U "Red-DiscordBot{package_extras}"```',
|
command=f"```{update_command}```",
|
||||||
command_2=f"```[p]cog update```",
|
|
||||||
)
|
)
|
||||||
outdated_red_message += extra_update
|
outdated_red_message += extra_update
|
||||||
return outdated_red_message, rich_outdated_message
|
return outdated_red_message, rich_outdated_message
|
||||||
@@ -176,14 +127,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())
|
||||||
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,12 +161,23 @@ 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:
|
|
||||||
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
|
try:
|
||||||
|
latest = 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 = latest.version > Version(red_version)
|
||||||
|
if outdated:
|
||||||
|
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
|
||||||
|
latest.version
|
||||||
|
)
|
||||||
|
rich_console.print(rich_outdated_message)
|
||||||
|
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_command_completion(ctx: commands.Context):
|
async def on_command_completion(ctx: commands.Context):
|
||||||
|
|||||||
@@ -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",)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from datetime import timedelta
|
|||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
Optional,
|
Optional,
|
||||||
Optional as NoParseOptional,
|
Optional as NoParseOptional,
|
||||||
Tuple,
|
Tuple,
|
||||||
@@ -20,6 +21,7 @@ from typing import (
|
|||||||
Dict,
|
Dict,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
Union as UserInputOptional,
|
Union as UserInputOptional,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -237,6 +239,9 @@ class RawUserIdConverter(dpy_commands.Converter):
|
|||||||
there is no user with such ID.
|
there is no user with such ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str) -> int:
|
async def convert(self, ctx: "Context", argument: str) -> int:
|
||||||
# This is for the hackban and unban commands, where we receive IDs that
|
# This is for the hackban and unban commands, where we receive IDs that
|
||||||
# are most likely not in the guild.
|
# are most likely not in the guild.
|
||||||
@@ -269,17 +274,20 @@ if TYPE_CHECKING:
|
|||||||
finite_float = float
|
finite_float = float
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def finite_float(arg: str) -> float:
|
class finite_float(dpy_commands.Converter):
|
||||||
"""
|
"""Converts a user provided string into a finite float."""
|
||||||
This converts a user provided string into a finite float.
|
|
||||||
"""
|
def __or__(self, rhs: Any) -> Any:
|
||||||
try:
|
return Union[self, rhs]
|
||||||
ret = float(arg)
|
|
||||||
except ValueError:
|
async def convert(self, ctx: "Context", arg: str) -> float:
|
||||||
raise BadArgument(_("`{arg}` is not a number.").format(arg=arg))
|
try:
|
||||||
if not math.isfinite(ret):
|
ret = float(arg)
|
||||||
raise BadArgument(_("`{arg}` is not a finite number.").format(arg=ret))
|
except ValueError:
|
||||||
return ret
|
raise BadArgument(_("`{arg}` is not a number.").format(arg=arg))
|
||||||
|
if not math.isfinite(ret):
|
||||||
|
raise BadArgument(_("`{arg}` is not a finite number.").format(arg=ret))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -296,6 +304,9 @@ else:
|
|||||||
self.delims = delims or [" "]
|
self.delims = delims or [" "]
|
||||||
self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
|
self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
|
async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
|
||||||
ret: Dict[str, str] = {}
|
ret: Dict[str, str] = {}
|
||||||
args = self.pattern.split(argument)
|
args = self.pattern.split(argument)
|
||||||
@@ -379,6 +390,9 @@ else:
|
|||||||
self.minimum = minimum
|
self.minimum = minimum
|
||||||
self.maximum = maximum
|
self.maximum = maximum
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
||||||
if self.default_unit and argument.isdecimal():
|
if self.default_unit and argument.isdecimal():
|
||||||
argument = argument + self.default_unit
|
argument = argument + self.default_unit
|
||||||
@@ -487,6 +501,9 @@ else:
|
|||||||
self.allowed_units = allowed_units
|
self.allowed_units = allowed_units
|
||||||
self.default_unit = default_unit
|
self.default_unit = default_unit
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str) -> relativedelta:
|
async def convert(self, ctx: "Context", argument: str) -> relativedelta:
|
||||||
if self.default_unit and argument.isdecimal():
|
if self.default_unit and argument.isdecimal():
|
||||||
argument = argument + self.default_unit
|
argument = argument + self.default_unit
|
||||||
@@ -537,6 +554,9 @@ else:
|
|||||||
class CommandConverter(dpy_commands.Converter):
|
class CommandConverter(dpy_commands.Converter):
|
||||||
"""Converts a command name to the matching `redbot.core.commands.Command` object."""
|
"""Converts a command name to the matching `redbot.core.commands.Command` object."""
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str):
|
async def convert(self, ctx: "Context", argument: str):
|
||||||
arg = argument.strip()
|
arg = argument.strip()
|
||||||
command = ctx.bot.get_command(arg)
|
command = ctx.bot.get_command(arg)
|
||||||
@@ -547,6 +567,9 @@ else:
|
|||||||
class CogConverter(dpy_commands.Converter):
|
class CogConverter(dpy_commands.Converter):
|
||||||
"""Converts a cog name to the matching `redbot.core.commands.Cog` object."""
|
"""Converts a cog name to the matching `redbot.core.commands.Cog` object."""
|
||||||
|
|
||||||
|
def __or__(self, rhs: Any) -> Any:
|
||||||
|
return Union[self, rhs]
|
||||||
|
|
||||||
async def convert(self, ctx: "Context", argument: str):
|
async def convert(self, ctx: "Context", argument: str):
|
||||||
arg = argument.strip()
|
arg = argument.strip()
|
||||||
cog = ctx.bot.get_cog(arg)
|
cog = ctx.bot.get_cog(arg)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from typing import (
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
|
from packaging.version import Version
|
||||||
from redbot.core.data_manager import storage_type
|
from redbot.core.data_manager import storage_type
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
@@ -49,10 +50,11 @@ 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
|
||||||
from .utils._internal_utils import fetch_latest_red_version_info
|
from .utils._internal_utils import fetch_latest_red_version
|
||||||
from .utils.predicates import MessagePredicate
|
from .utils.predicates import MessagePredicate
|
||||||
from .utils.chat_formatting import (
|
from .utils.chat_formatting import (
|
||||||
box,
|
box,
|
||||||
@@ -215,12 +217,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,8 +422,14 @@ 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:
|
||||||
outdated = pypi_version and pypi_version > red_version_info
|
latest = await fetch_latest_red_version()
|
||||||
|
except (aiohttp.ClientError, TimeoutError) as exc:
|
||||||
|
log.error("Failed to fetch latest version information from PyPI.", exc_info=exc)
|
||||||
|
pypi_version = None
|
||||||
|
else:
|
||||||
|
pypi_version = latest.version
|
||||||
|
outdated = pypi_version and pypi_version > Version(__version__)
|
||||||
|
|
||||||
if embed_links:
|
if embed_links:
|
||||||
dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
|
dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
|
||||||
|
|||||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user