Compare commits
249 Commits
3.5.7
...
V3/develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 015f5a00fd | |||
| 6e417419aa | |||
| 316f237397 | |||
| 75e37ca896 | |||
| c701c79ed0 | |||
| 60323c99d1 | |||
| b7c11c016e | |||
| bdc66c3f56 | |||
| e6c6b9874b | |||
| 40b01c7985 | |||
| 666e249413 | |||
| 1bfe2a78fa | |||
| afe4e636b7 | |||
| 45c55418a4 | |||
| 29758fd104 | |||
| 8626aef36f | |||
| 18154465c3 | |||
| 07e6f1b264 | |||
| effc390ddf | |||
| 9afbe363e5 | |||
| 5023f7f1b5 | |||
| 01a3c17f8e | |||
| fe574189d6 | |||
| fec1818e5a | |||
| dd3b9a01d3 | |||
| a809c3604a | |||
| 982f082cb0 | |||
| 6a4d777723 | |||
| 4dc18342d9 | |||
| 71311c446a | |||
| d4a1ee38f0 | |||
| 8507b0165f | |||
| c7651da2f9 | |||
| 4d24365d17 | |||
| d2f0f8d5ed | |||
| cc803579a0 | |||
| 0271dee092 | |||
| 1e5620935d | |||
| 031e57891c | |||
| 72f8952baf | |||
| 0e25534677 | |||
| cfbf2fa401 | |||
| a65509c67c | |||
| 936e17338c | |||
| 6c3c2e8fa7 | |||
| 8c3cdf20a6 | |||
| f68580fab9 | |||
| ec66666036 | |||
| 3fd23d4163 | |||
| 2dbbb51208 | |||
| b177c80b4e | |||
| 029029e9a5 | |||
| c6ff2191f3 | |||
| 6603cd1a86 | |||
| 1daf56f3d8 | |||
| b3f0349ba2 | |||
| bfc3561928 | |||
| 8d8918b3c6 | |||
| 550cf49bc8 | |||
| 313eeffc84 | |||
| 88e1f72467 | |||
| 3c6146d6ca | |||
| bfab9cc5f8 | |||
| 07ee31a88f | |||
| bd61c105e0 | |||
| db0e831a2e | |||
| d7a56f1518 | |||
| 5f2c6d19d1 | |||
| 7b3600ecb1 | |||
| 6f5f34c80a | |||
| 2e902b067e | |||
| 61695daded | |||
| 10889642ce | |||
| a3b254fe8e | |||
| 71554c981d | |||
| e1225029b0 | |||
| 85923d4c0f | |||
| 19b34c63b2 | |||
| 60e819159f | |||
| 0f4c7b0fe6 | |||
| 3bf7c64d01 | |||
| 4558b72082 | |||
| 73958d87f1 | |||
| d6054412f4 | |||
| 5cf69bdc51 | |||
| 2fd6ea88d9 | |||
| 50ad59a6c8 | |||
| dfc1e742f8 | |||
| 769c319ffd | |||
| 3c1f2cddfd | |||
| 1299db0f3a | |||
| c58d208ca2 | |||
| c308ea0184 | |||
| 8e0948d560 | |||
| 952a372652 | |||
| a0c1713e78 | |||
| 8b1daf1ad0 | |||
| dcdef9d798 | |||
| f962aeb7b8 | |||
| b13b1f8f16 | |||
| 6bf2a88995 | |||
| 22888f8014 | |||
| ba44370020 | |||
| cd0e8750c1 | |||
| 679289fd1c | |||
| ce6489325e | |||
| d64cbdf83e | |||
| 66d1c87b5a | |||
| 3888f09cfa | |||
| 8ad9c55d50 | |||
| 3aac07a4d7 | |||
| bd26e7d5af | |||
| 9392077434 | |||
| 1f48919005 | |||
| fdaa869130 | |||
| 18614b1604 | |||
| 016684bcce | |||
| 5cfb8edab8 | |||
| 150692538f | |||
| f4ffc6bc80 | |||
| 9419f2642a | |||
| f0a29e9815 | |||
| d29ae723c1 | |||
| 9920628948 | |||
| 48b2fe77c0 | |||
| 33e0eac741 | |||
| 2871992772 | |||
| 30058c0f73 | |||
| 4134881fae | |||
| 4396323205 | |||
| 4e27059209 | |||
| d3887b595f | |||
| 005b8af10a | |||
| d304da7a16 | |||
| f3c89ad8bd | |||
| 05cf9b7f39 | |||
| 907a3f7561 | |||
| 2595c9de10 | |||
| 8be7b0850c | |||
| 61ec913789 | |||
| 88b11f2b9c | |||
| 9ca0ced2d8 | |||
| a5a178bfaf | |||
| eeb90aaa45 | |||
| b7a59b5e4c | |||
| 2769ea025f | |||
| 818420a641 | |||
| 3c49a77e34 | |||
| 90691ba2b9 | |||
| 68f2806204 | |||
| 903992f48a | |||
| e4b75f5333 | |||
| 254d5a91d6 | |||
| b979a7c4d1 | |||
| 54a29174ea | |||
| 5bbced5b0d | |||
| 5b21c89505 | |||
| 04d856cfb0 | |||
| 701339f8a1 | |||
| 3d04d696c1 | |||
| 0b8bcef86c | |||
| 2d47d75919 | |||
| 7eb26da647 | |||
| 699471f27a | |||
| 2c2080df12 | |||
| fa7236af63 | |||
| 601816abc0 | |||
| bf8c0d03b5 | |||
| 2e40ec4a1a | |||
| 9b9fdf555b | |||
| 7dee8d7963 | |||
| 0281d6c93e | |||
| f4c8077268 | |||
| 7b8acd7ef6 | |||
| 6ee976c341 | |||
| 0b0b23b971 | |||
| 2b1e603124 | |||
| dd61b669b0 | |||
| 57b76bc0d7 | |||
| 573e5c2b40 | |||
| ad1e1aa2ba | |||
| 4242a7adf2 | |||
| e03f97d1cd | |||
| 975c0007fe | |||
| bef3aa5f69 | |||
| cbf8247e6e | |||
| 23c86d7850 | |||
| aa21091b84 | |||
| 72ec88aa3c | |||
| b1f331e51f | |||
| 0c9c210dbb | |||
| 47d4675f52 | |||
| 80d0bab29a | |||
| e61327a65c | |||
| 11ebd40dfa | |||
| 00e41d38f9 | |||
| afb4f6079a | |||
| 97b467939c | |||
| f54499eaba | |||
| f8d6bbb0af | |||
| 24afd61a85 | |||
| 8e118733ea | |||
| f01c0ec675 | |||
| 94d12cb45f | |||
| e9ed52cf16 | |||
| 194dea545d | |||
| 59400204e8 | |||
| 1c863c7b3b | |||
| ad9e00d1d9 | |||
| 463f0c5e6d | |||
| e71312ede0 | |||
| 48d74712bc | |||
| c3a493a500 | |||
| 4034ddd452 | |||
| 2ae1eb9ec9 | |||
| 0b390fe2f6 |
@@ -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/*
|
||||||
@@ -141,6 +137,8 @@
|
|||||||
"Category: Core - API - App Commands Package":
|
"Category: Core - API - App Commands Package":
|
||||||
# Source
|
# Source
|
||||||
- redbot/core/app_commands/*
|
- redbot/core/app_commands/*
|
||||||
|
# Docs
|
||||||
|
- docs/framework_app_commands.rst
|
||||||
# Tests
|
# Tests
|
||||||
- tests/core/test_app_commands.py
|
- tests/core/test_app_commands.py
|
||||||
"Category: Core - API - Commands Package":
|
"Category: Core - API - Commands Package":
|
||||||
@@ -160,6 +158,7 @@
|
|||||||
- any:
|
- any:
|
||||||
- redbot/core/_drivers/**/*
|
- redbot/core/_drivers/**/*
|
||||||
- "!redbot/core/_drivers/**/locales/*"
|
- "!redbot/core/_drivers/**/locales/*"
|
||||||
|
- redbot/core/_config.py
|
||||||
- redbot/core/config.py
|
- redbot/core/config.py
|
||||||
# Docs
|
# Docs
|
||||||
- docs/framework_config.rst
|
- docs/framework_config.rst
|
||||||
@@ -205,14 +204,23 @@
|
|||||||
- 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":
|
||||||
# Source
|
# Source
|
||||||
|
- redbot/core/_i18n.py
|
||||||
- redbot/core/i18n.py
|
- redbot/core/i18n.py
|
||||||
# Locale files
|
# Locale files
|
||||||
- redbot/**/locales/*
|
- redbot/**/locales/*
|
||||||
@@ -259,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":
|
||||||
@@ -269,6 +276,7 @@
|
|||||||
- docs/bot_application_guide.rst
|
- docs/bot_application_guide.rst
|
||||||
- docs/install_guides/**/*
|
- docs/install_guides/**/*
|
||||||
- docs/update_red.rst
|
- docs/update_red.rst
|
||||||
|
- docs/backup_red.rst
|
||||||
"Category: Docs - Other":
|
"Category: Docs - Other":
|
||||||
- docs/host-list.rst
|
- docs/host-list.rst
|
||||||
- docs/index.rst
|
- docs/index.rst
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install script's pre-requirements
|
- name: Install script's pre-requirements
|
||||||
run: |
|
run: |
|
||||||
python -m pip install -U pip
|
python -m pip install -U pip
|
||||||
python -m pip install -U pathspec pyyaml rich
|
python -m pip install -U pathspec pyyaml rich typing_extensions
|
||||||
- name: Check label pattern exhaustiveness
|
- name: Check label pattern exhaustiveness
|
||||||
run: |
|
run: |
|
||||||
python .github/workflows/scripts/check_label_pattern_exhaustiveness.py
|
python .github/workflows/scripts/check_label_pattern_exhaustiveness.py
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -34,14 +34,13 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: 'python'
|
languages: 'python'
|
||||||
# Override the default behavior so that the action doesn't attempt
|
# Override the default behavior so that the action doesn't attempt
|
||||||
# to auto-install Python dependencies
|
# to auto-install Python dependencies
|
||||||
# Learn more...
|
# Learn more...
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#analyzing-python-dependencies
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#analyzing-python-dependencies
|
||||||
setup-python-dependencies: false
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -55,4 +54,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
if: github.repository == 'Cog-Creators/Red-DiscordBot'
|
if: github.repository == 'Cog-Creators/Red-DiscordBot'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ jobs:
|
|||||||
name: Lint Python
|
name: Lint Python
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.ref }}
|
ref: ${{ env.ref }}
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.8"
|
||||||
- run: "python -m pip install git+https://github.com/pycqa/pyflakes@1911c20#egg=pyflakes git+https://github.com/pycqa/pycodestyle@d219c68#egg=pycodestyle git+https://github.com/pycqa/flake8@3.7.9#egg=flake8"
|
- run: >
|
||||||
|
python -m pip install
|
||||||
|
'pyflakes @ https://github.com/pycqa/pyflakes/tarball/1911c20'
|
||||||
|
'pycodestyle @ https://github.com/pycqa/pycodestyle/tarball/d219c68'
|
||||||
|
'flake8 @ https://github.com/pycqa/flake8/tarball/3.7.9'
|
||||||
name: Install Flake8
|
name: Install Flake8
|
||||||
- run: "python -m flake8 . --count --select=E9,F7,F82 --show-source"
|
- run: "python -m flake8 . --count --select=E9,F7,F82 --show-source"
|
||||||
name: Flake8 Linting
|
name: Flake8 Linting
|
||||||
|
|||||||
@@ -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@v3
|
- 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@v3
|
- 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}));
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Checkout repository and install Python
|
# Checkout repository and install Python
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
name: Build package
|
name: Build package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -75,27 +75,68 @@ jobs:
|
|||||||
run: python -m twine check dist/*
|
run: python -m twine check dist/*
|
||||||
|
|
||||||
- name: Upload packaged distributions
|
- name: Upload packaged distributions
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-output
|
name: build-output
|
||||||
path: ./dist
|
path: ./dist
|
||||||
|
|
||||||
|
generate_default_ll_server_config:
|
||||||
|
name: Generate default application.yml
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
|
- name: Install script's dependencies
|
||||||
|
run: python -m pip install PyYAML
|
||||||
|
|
||||||
|
- name: Generate default application.yml
|
||||||
|
env:
|
||||||
|
APP_YML_FILE: "Red-DiscordBot-${{ github.ref_name }}-default-lavalink-application.yml"
|
||||||
|
run: |
|
||||||
|
mkdir -p release_assets
|
||||||
|
python .github/workflows/scripts/get_default_ll_server_config.py "release_assets/$APP_YML_FILE"
|
||||||
|
|
||||||
|
- name: Upload default application.yml
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ll-default-server-config
|
||||||
|
path: ./release_assets
|
||||||
|
|
||||||
release_to_pypi:
|
release_to_pypi:
|
||||||
needs:
|
needs:
|
||||||
- release_information
|
- release_information
|
||||||
- build
|
- build
|
||||||
|
- generate_default_ll_server_config
|
||||||
environment: Release
|
environment: Release
|
||||||
name: Release to PyPI
|
name: Release to PyPI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download packaged distributions
|
- name: Download packaged distributions
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-output
|
name: build-output
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|
||||||
|
- name: Download default application.yml
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ll-default-server-config
|
||||||
|
path: release_assets/
|
||||||
|
|
||||||
|
- name: Upload dists to GitHub Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ github.token }}"
|
||||||
|
run: |
|
||||||
|
gh release upload "$GITHUB_REF_NAME" dist/* release_assets/* --repo "$GITHUB_REPOSITORY"
|
||||||
|
|
||||||
- name: Publish package distributions to PyPI
|
- name: Publish package distributions to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
@@ -106,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
|
||||||
@@ -119,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@v3
|
- 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'
|
||||||
|
|
||||||
@@ -153,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: |
|
||||||
@@ -161,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}));
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- macos-latest
|
- macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository.
|
- name: Checkout the repository.
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python 3.8.
|
- name: Set up Python 3.8.
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
python .github/workflows/scripts/compile_requirements.py
|
python .github/workflows/scripts/compile_requirements.py
|
||||||
|
|
||||||
- name: Upload requirements files.
|
- name: Upload requirements files.
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.compile_requirements.outputs.sys_platform }}
|
name: ${{ steps.compile_requirements.outputs.sys_platform }}
|
||||||
path: requirements/${{ steps.compile_requirements.outputs.sys_platform }}-*.txt
|
path: requirements/${{ steps.compile_requirements.outputs.sys_platform }}-*.txt
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository.
|
- name: Checkout the repository.
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python 3.8.
|
- name: Set up Python 3.8.
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -71,17 +71,17 @@ jobs:
|
|||||||
python -m pip install -U "packaging>=22.0"
|
python -m pip install -U "packaging>=22.0"
|
||||||
|
|
||||||
- name: Download Windows requirements.
|
- name: Download Windows requirements.
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: win32
|
name: win32
|
||||||
path: requirements
|
path: requirements
|
||||||
- name: Download Linux requirements.
|
- name: Download Linux requirements.
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux
|
name: linux
|
||||||
path: requirements
|
path: requirements
|
||||||
- name: Download macOS requirements.
|
- name: Download macOS requirements.
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: darwin
|
name: darwin
|
||||||
path: requirements
|
path: requirements
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
python .github/workflows/scripts/merge_requirements.py
|
python .github/workflows/scripts/merge_requirements.py
|
||||||
|
|
||||||
- name: Upload merged requirements files.
|
- name: Upload merged requirements files.
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: merged
|
name: merged
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ def pip_compile(version: str, name: str) -> None:
|
|||||||
if EXCLUDE_STEM_RE.fullmatch(stem):
|
if EXCLUDE_STEM_RE.fullmatch(stem):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
constraint_flags = [
|
||||||
|
arg
|
||||||
|
for file in REQUIREMENTS_FOLDER.glob(f"{sys.platform}-3.8-*.txt")
|
||||||
|
for arg in ("-c", file.name)
|
||||||
|
]
|
||||||
|
|
||||||
executable = ("py", f"-{version}") if sys.platform == "win32" else (f"python{version}",)
|
executable = ("py", f"-{version}") if sys.platform == "win32" else (f"python{version}",)
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
(
|
(
|
||||||
@@ -30,6 +36,7 @@ def pip_compile(version: str, name: str) -> None:
|
|||||||
f"{name}.in",
|
f"{name}.in",
|
||||||
"--output-file",
|
"--output-file",
|
||||||
f"{stem}.txt",
|
f"{stem}.txt",
|
||||||
|
*constraint_flags,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROOT_FOLDER = Path(__file__).parents[3].absolute()
|
||||||
|
AUDIO_FOLDER = ROOT_FOLDER / "redbot/cogs/audio"
|
||||||
|
|
||||||
|
# We want to import `redbot.cogs.audio.managed_node` package as if it were top-level package
|
||||||
|
# so we have to the `redbot/cogs/audio` directory to Python's path.
|
||||||
|
sys.path.insert(0, str(AUDIO_FOLDER))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
output_file = sys.argv[1]
|
||||||
|
except IndexError:
|
||||||
|
print("Usage:", sys.argv[0], "<output_file>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
import managed_node
|
||||||
|
|
||||||
|
server_config = managed_node.get_default_server_config()
|
||||||
|
with open(output_file, "w", encoding="utf-8") as fp:
|
||||||
|
yaml.safe_dump(server_config, fp)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -49,6 +49,8 @@ def get_requirements(fp: TextIO) -> List[RequirementData]:
|
|||||||
via_prefix = "via "
|
via_prefix = "via "
|
||||||
if source.startswith(via_prefix):
|
if source.startswith(via_prefix):
|
||||||
source = source[len(via_prefix) :]
|
source = source[len(via_prefix) :]
|
||||||
|
if source.startswith("-c ") and source != "-c base.txt":
|
||||||
|
continue
|
||||||
current.comments.add(source)
|
current.comments.add(source)
|
||||||
elif line and not line.startswith(("#", " ")):
|
elif line and not line.startswith(("#", " ")):
|
||||||
current = RequirementData(line)
|
current = RequirementData(line)
|
||||||
@@ -135,25 +137,27 @@ for name in names:
|
|||||||
python_version_marker = (
|
python_version_marker = (
|
||||||
# Requirement present on less Python versions than not.
|
# Requirement present on less Python versions than not.
|
||||||
" or ".join(
|
" or ".join(
|
||||||
f"python_version == '{python_version}'" for python_version in python_versions
|
f"python_version == '{python_version}'"
|
||||||
|
for python_version in sorted(python_versions)
|
||||||
)
|
)
|
||||||
if len(python_versions) < len(all_python_versions - python_versions)
|
if len(python_versions) < len(all_python_versions - python_versions)
|
||||||
# Requirement present on more Python versions than not
|
# Requirement present on more Python versions than not
|
||||||
# This may generate an empty string when Python version is irrelevant.
|
# This may generate an empty string when Python version is irrelevant.
|
||||||
else " and ".join(
|
else " and ".join(
|
||||||
f"python_version != '{python_version}'"
|
f"python_version != '{python_version}'"
|
||||||
for python_version in all_python_versions - python_versions
|
for python_version in sorted(all_python_versions - python_versions)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
platform_marker = (
|
platform_marker = (
|
||||||
# Requirement present on less platforms than not.
|
# Requirement present on less platforms than not.
|
||||||
" or ".join(f"sys_platform == '{platform}'" for platform in platforms)
|
" or ".join(f"sys_platform == '{platform}'" for platform in sorted(platforms))
|
||||||
if len(platforms) < len(all_platforms - platforms)
|
if len(platforms) < len(all_platforms - platforms)
|
||||||
# Requirement present on more platforms than not
|
# Requirement present on more platforms than not
|
||||||
# This may generate an empty string when platform is irrelevant.
|
# This may generate an empty string when platform is irrelevant.
|
||||||
else " and ".join(
|
else " and ".join(
|
||||||
f"sys_platform != '{platform}'" for platform in all_platforms - platforms
|
f"sys_platform != '{platform}'"
|
||||||
|
for platform in sorted(all_platforms - platforms)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,12 +171,12 @@ for name in names:
|
|||||||
# Requirement present on less envs than not.
|
# Requirement present on less envs than not.
|
||||||
" or ".join(
|
" or ".join(
|
||||||
f"(sys_platform == '{platform}' and python_version == '{python_version}')"
|
f"(sys_platform == '{platform}' and python_version == '{python_version}')"
|
||||||
for platform, python_version in iter_envs(envs)
|
for platform, python_version in iter_envs(sorted(envs))
|
||||||
)
|
)
|
||||||
if len(envs) < len(all_envs - envs.keys())
|
if len(envs) < len(all_envs - envs.keys())
|
||||||
else " and ".join(
|
else " and ".join(
|
||||||
f"(sys_platform != '{platform}' and python_version != '{python_version}')"
|
f"(sys_platform != '{platform}' and python_version != '{python_version}')"
|
||||||
for platform, python_version in iter_envs(all_envs - envs.keys())
|
for platform, python_version in iter_envs(sorted(all_envs - envs.keys()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
name: Tox - ${{ matrix.friendly_name }}
|
name: Tox - ${{ matrix.friendly_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.ref }}
|
ref: ${{ env.ref }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.ref }}
|
ref: ${{ env.ref }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ unsafe-load-any-extension=no
|
|||||||
# run arbitrary code
|
# run arbitrary code
|
||||||
extension-pkg-whitelist=
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
|
||||||
# optimizer, which will apply various small optimizations. For instance, it can
|
|
||||||
# be used to obtain the result of joining multiple strings with the addition
|
|
||||||
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
|
||||||
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
|
||||||
# AST will be different than the one from reality.
|
|
||||||
optimize-ast=no
|
|
||||||
|
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
@@ -66,7 +58,6 @@ disable=C, # black is enforcing this for us already, incompatibly
|
|||||||
[REPORTS]
|
[REPORTS]
|
||||||
|
|
||||||
output-format=parseable
|
output-format=parseable
|
||||||
files-output=no
|
|
||||||
reports=no
|
reports=no
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ build:
|
|||||||
os: "ubuntu-22.04"
|
os: "ubuntu-22.04"
|
||||||
tools:
|
tools:
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
|
jobs:
|
||||||
|
install:
|
||||||
|
- 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:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 50 KiB |
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, List, Set
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.io import StringOutput
|
||||||
|
from docutils.nodes import Element
|
||||||
|
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
from sphinx.builders.text import TextBuilder
|
||||||
|
from sphinx.writers.text import TextWriter
|
||||||
|
from sphinx.util import logging
|
||||||
|
from sphinx.util.docutils import SphinxTranslator
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTranslator(SphinxTranslator):
|
||||||
|
builder: PromptBuilder
|
||||||
|
|
||||||
|
def __init__(self, document: nodes.document, builder: PromptBuilder) -> None:
|
||||||
|
super().__init__(document, builder)
|
||||||
|
self.body = ""
|
||||||
|
self.prompts: List[Dict[str, str]] = []
|
||||||
|
|
||||||
|
def visit_document(self, node: Element) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def depart_document(self, node: Element) -> None:
|
||||||
|
if not self.prompts:
|
||||||
|
self.body = ""
|
||||||
|
return
|
||||||
|
if self.builder.out_suffix.endswith(".json"):
|
||||||
|
self.body = json.dumps(self.prompts, indent=4)
|
||||||
|
else:
|
||||||
|
self.body = "\n".join(prompt["content"] for prompt in self.prompts)
|
||||||
|
|
||||||
|
def unknown_visit(self, node: Element) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unknown_departure(self, node: Element) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def visit_prompt(self, node: Element) -> None:
|
||||||
|
self.prompts.append(
|
||||||
|
{
|
||||||
|
"language": node.attributes["language"],
|
||||||
|
"prompts": node.attributes["prompts"],
|
||||||
|
"modifiers": node.attributes["modifiers"],
|
||||||
|
"rawsource": node.rawsource,
|
||||||
|
"content": node.children[0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptWriter(TextWriter):
|
||||||
|
def translate(self) -> None:
|
||||||
|
visitor = self.builder.create_translator(self.document, self.builder)
|
||||||
|
self.document.walkabout(visitor)
|
||||||
|
self.output = visitor.body
|
||||||
|
|
||||||
|
|
||||||
|
class prompt(nodes.literal_block):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PromptBuilder(TextBuilder):
|
||||||
|
"""Extract prompts from documents."""
|
||||||
|
|
||||||
|
format = "json"
|
||||||
|
epilog = "The files with prompts are in %(outdir)s."
|
||||||
|
|
||||||
|
out_suffix = ".json"
|
||||||
|
default_translator_class = PromptTranslator
|
||||||
|
writer: PromptWriter
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
sphinx_prompt = __import__("sphinx-prompt")
|
||||||
|
|
||||||
|
def run(self) -> List[prompt]:
|
||||||
|
self.assert_has_content()
|
||||||
|
rawsource = "\n".join(self.content)
|
||||||
|
language = self.options.get("language") or "text"
|
||||||
|
prompts = [
|
||||||
|
p
|
||||||
|
for p in (
|
||||||
|
self.options.get("prompts") or sphinx_prompt.PROMPTS.get(language, "")
|
||||||
|
).split(",")
|
||||||
|
if p
|
||||||
|
]
|
||||||
|
modifiers = [
|
||||||
|
modifier for modifier in self.options.get("modifiers", "").split(",") if modifier
|
||||||
|
]
|
||||||
|
content = rawsource
|
||||||
|
if "auto" in modifiers:
|
||||||
|
parts = []
|
||||||
|
for line in self.content:
|
||||||
|
for p in prompts:
|
||||||
|
if line.startswith(p):
|
||||||
|
line = line[len(p) + 1 :].rstrip()
|
||||||
|
parts.append(line)
|
||||||
|
content = "\n".join(parts)
|
||||||
|
node = prompt(
|
||||||
|
rawsource,
|
||||||
|
content,
|
||||||
|
directive_content=self.content,
|
||||||
|
language=language,
|
||||||
|
prompts=self.options.get("prompts") or sphinx_prompt.PROMPTS.get(language, ""),
|
||||||
|
modifiers=modifiers,
|
||||||
|
)
|
||||||
|
return [node]
|
||||||
|
|
||||||
|
sphinx_prompt.PromptDirective.run = run
|
||||||
|
|
||||||
|
def prepare_writing(self, docnames: Set[str]) -> None:
|
||||||
|
del docnames
|
||||||
|
self.writer = PromptWriter(self)
|
||||||
|
|
||||||
|
def write_doc(self, docname: str, doctree: nodes.document) -> None:
|
||||||
|
self.writer.write(doctree, StringOutput(encoding="utf-8"))
|
||||||
|
if not self.writer.output:
|
||||||
|
# don't write empty files
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = os.path.join(self.outdir, docname.replace("/", os.path.sep) + self.out_suffix)
|
||||||
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
|
try:
|
||||||
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
f.write(self.writer.output)
|
||||||
|
except OSError as err:
|
||||||
|
logger.warning("error writing file %s: %s", filename, err)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonPromptBuilder(PromptBuilder):
|
||||||
|
name = "jsonprompt"
|
||||||
|
out_suffix = ".json"
|
||||||
|
|
||||||
|
|
||||||
|
class TextPromptBuilder(PromptBuilder):
|
||||||
|
name = "textprompt"
|
||||||
|
out_suffix = ".txt"
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||||
|
app.add_builder(JsonPromptBuilder)
|
||||||
|
app.add_builder(TextPromptBuilder)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": "1.0",
|
||||||
|
"parallel_read_safe": True,
|
||||||
|
"parallel_write_safe": True,
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<p class="first admonition-title">Warning</p>
|
<p class="first admonition-title">Warning</p>
|
||||||
<p class="last">
|
<p class="last">
|
||||||
This document is for Red's development version, which can be significantly different from previous releases.
|
This document is for Red's development version, which can be significantly different from previous releases.
|
||||||
If you're a regular user, you should read the <a href="{{ dict(versions)['stable'] }}">Red documentation for the current stable release</a>.
|
If you're a regular user, you should read the <a href="/{{ rtd_language }}/stable/">Red documentation for the current stable release</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -57,6 +57,8 @@ Paste the following and replace the following:
|
|||||||
<string>username</string>
|
<string>username</string>
|
||||||
<key>InitGroups</key>
|
<key>InitGroups</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>ProcessType</key>
|
||||||
|
<string>Interactive</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ Next, your python :code:`path` can be fetched with the following commands:
|
|||||||
$ source ~/redenv/bin/activate
|
$ source ~/redenv/bin/activate
|
||||||
(redenv) $ /usr/bin/which python
|
(redenv) $ /usr/bin/which python
|
||||||
|
|
||||||
# If redbot is installed in a pyenv virtualenv
|
|
||||||
$ pyenv shell <virtualenv_name>
|
|
||||||
(redenv) $ pyenv which python
|
|
||||||
|
|
||||||
Then create the new service file:
|
Then create the new service file:
|
||||||
|
|
||||||
:code:`sudo nano /etc/systemd/system/red@.service`
|
:code:`sudo nano /etc/systemd/system/red@.service`
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
.. _backup-red:
|
||||||
|
|
||||||
|
============================
|
||||||
|
Backing Up and Restoring Red
|
||||||
|
============================
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. 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:
|
||||||
|
|
||||||
|
#. Stop the bot, ideally with ``[p]shutdown``.
|
||||||
|
#. Activate your venv.
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
|
||||||
|
"%userprofile%\redenv\Scripts\activate.bat"
|
||||||
|
#. Backup your Red instance with the following command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
|
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 ``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.
|
||||||
@@ -21,25 +21,34 @@ Creating a Bot account is a pretty straightforward process.
|
|||||||
.. image:: /.resources/bot-guide/discord_create_app_button.png
|
.. image:: /.resources/bot-guide/discord_create_app_button.png
|
||||||
:alt: The new application button.
|
:alt: The new application button.
|
||||||
|
|
||||||
4. Give the application a name and click "Create".
|
4. Give the application a name, check the box to accept the Terms of Service, and click "Create".
|
||||||
|
|
||||||
.. image:: /.resources/bot-guide/discord_create_app_form.png
|
.. image:: /.resources/bot-guide/discord_create_app_form.png
|
||||||
:alt: The new application form filled in.
|
:alt: The new application form filled in.
|
||||||
|
|
||||||
5. Create a Bot User by navigating to the "Bot" tab and clicking "Add Bot".
|
5. Navigate to the "Install" tab on the left side of the screen.
|
||||||
|
|
||||||
- Click "Yes, do it!" to continue.
|
.. image:: /.resources/bot-guide/discord_installation_tab.png
|
||||||
|
:alt: The installation tab in the application page.
|
||||||
|
|
||||||
.. image:: /.resources/bot-guide/discord_create_bot_user.png
|
6. Uncheck "User Install" and set "Install Link" to "None"
|
||||||
:alt: The Add Bot button.
|
|
||||||
6. If you want others to be able to invite your bot tick the **Public Bot**. Keeping it unticked will prevent others from inviting your bot to their servers and only you will be able to add the bot to servers (provided that you have needed permissions in the server you want to add the bot to).
|
.. image:: /.resources/bot-guide/discord_installation_options.png
|
||||||
|
:alt: How the Installation options should look like for most people.
|
||||||
|
|
||||||
|
7. Navigate to the "Bot" tab on the left side of the screen.
|
||||||
|
|
||||||
|
.. image:: /.resources/bot-guide/discord_bot_tab.png
|
||||||
|
:alt: The bot tab in the application page.
|
||||||
|
|
||||||
|
8. If you want others to be able to invite your bot tick the **Public Bot**. Keeping it unticked will prevent others from inviting your bot to their servers and only you will be able to add the bot to servers (provided that you have needed permissions in the server you want to add the bot to).
|
||||||
|
|
||||||
- Make sure **Require OAuth2 Code Grant** is unchecked.
|
- Make sure **Require OAuth2 Code Grant** is unchecked.
|
||||||
|
|
||||||
.. image:: /.resources/bot-guide/discord_bot_user_options.png
|
.. image:: /.resources/bot-guide/discord_bot_user_options.png
|
||||||
:alt: How the Bot User options should look like for most people.
|
:alt: How the Bot User options should look like for most people.
|
||||||
|
|
||||||
7. Copy the token using the "Copy" button.
|
9. Acquire the token using the "Reset Token" button, then "Copy" after it is revealed.
|
||||||
|
|
||||||
- **This is not the Client Secret at the General Information page**
|
- **This is not the Client Secret at the General Information page**
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ find detailed docs about usage and commands.
|
|||||||
You can see additional help for any command in this guide by using ``[p]help`` with the
|
You can see additional help for any command in this guide by using ``[p]help`` with the
|
||||||
command name, like ``[p]help playlist append``.
|
command name, like ``[p]help playlist append``.
|
||||||
|
|
||||||
In this guide, you will see references to "Lavalink" or the "Lavalink.jar". `Lavalink <https://github.com/freyacodes/Lavalink/>`_ is the
|
In this guide, you will see references to "Lavalink" or the "Lavalink.jar". `Lavalink <https://github.com/lavalink-devs/Lavalink/>`_ is the
|
||||||
Java-based audio backend we use to be able to play music through the bot. Most users will
|
Java-based audio backend we use to be able to play music through the bot. Most users will
|
||||||
not have to worry much about Lavalink or what it is, as Audio manages this process for you
|
not have to worry much about Lavalink or what it is, as Audio manages this process for you
|
||||||
by default. Advanced users can read more about Lavalink and special cases under the
|
by default. Advanced users can read more about Lavalink and special cases under the
|
||||||
@@ -116,18 +116,18 @@ How can I use this playlist link with playlist commands in audio?**
|
|||||||
:ref:`setting up Audio for multiple bots<multibots>`. Otherwise, another process is using the
|
:ref:`setting up Audio for multiple bots<multibots>`. Otherwise, another process is using the
|
||||||
port, so you need to figure out what is using port 2333 and terminate/disconnect it yourself.
|
port, so you need to figure out what is using port 2333 and terminate/disconnect it yourself.
|
||||||
|
|
||||||
**Q: My terminal is saying that I "must install Java 17 or 11 for Lavalink to run". How can I fix this?**
|
**Q: My terminal is saying that I "must install Java 21 or 17 for Lavalink to run". How can I fix this?**
|
||||||
|
|
||||||
You are getting this error because you have a different version of Java installed, or you don't have
|
You are getting this error because you have a different version of Java installed, or you don't have
|
||||||
Java installed at all. As the error states, Java 17 or 11 is required, and can be installed from
|
Java installed at all. As the error states, Java 21 or 17 is required, and can be installed from
|
||||||
`here <https://adoptium.net/temurin/releases/?version=17>`__.
|
`here <https://adoptium.net/temurin/releases/?version=21>`__.
|
||||||
|
|
||||||
If you have Java 17 or 11 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
|
If you have Java 21 or 17 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
|
||||||
Use ``[p]llset java <path_to_java_17_or_11_executable>``, to make Audio launch Lavalink with a
|
Use ``[p]llset java <path_to_java_21_or_17_executable>``, to make Audio launch Lavalink with a
|
||||||
specific Java binary. To do this, you will need to locate your ``java.exe``/``java`` file
|
specific Java binary. To do this, you will need to locate your ``java.exe``/``java`` file
|
||||||
in your **Java 17 or 11 install**.
|
in your **Java 21 or 17 install**.
|
||||||
|
|
||||||
Alternatively, update your PATH settings so that Java 17 or 11 is the one used by ``java``. However,
|
Alternatively, update your PATH settings so that Java 21 or 17 is the one used by ``java``. However,
|
||||||
you should confirm that nothing other than Red is running on the machine that requires Java.
|
you should confirm that nothing other than Red is running on the machine that requires Java.
|
||||||
|
|
||||||
.. _queue_commands:
|
.. _queue_commands:
|
||||||
@@ -550,8 +550,8 @@ 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/freyacodes/Lavalink/#requirements>`__ for more information.
|
Zulu builds are suggested, see `here <https://github.com/lavalink-devs/Lavalink/#requirements>`__ for more information.
|
||||||
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
Setting Up The Lavalink Folder
|
Setting Up The Lavalink Folder
|
||||||
@@ -583,7 +583,7 @@ the ``cd lavalink`` command in the previous section, you can run the following c
|
|||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
curl https://raw.githubusercontent.com/freyacodes/Lavalink/master/LavalinkServer/application.yml.example > application.yml
|
curl https://raw.githubusercontent.com/lavalink-devs/Lavalink/master/LavalinkServer/application.yml.example > application.yml
|
||||||
curl -LOz Lavalink.jar https://github.com/Cog-Creators/Lavalink-Jars/releases/latest/download/Lavalink.jar
|
curl -LOz Lavalink.jar https://github.com/Cog-Creators/Lavalink-Jars/releases/latest/download/Lavalink.jar
|
||||||
|
|
||||||
If you did it properly, the files ``Lavalink.jar`` and ``application.yml`` will show up when we run ``ls``, the Linux command
|
If you did it properly, the files ``Lavalink.jar`` and ``application.yml`` will show up when we run ``ls``, the Linux command
|
||||||
@@ -3651,7 +3651,7 @@ This command shouldn't need to be used most of the time,
|
|||||||
and is only useful if the host machine has conflicting Java versions.
|
and is only useful if the host machine has conflicting Java versions.
|
||||||
|
|
||||||
If changing this make sure that the Java executable you set is supported by Audio.
|
If changing this make sure that the Java executable you set is supported by Audio.
|
||||||
The current supported versions are Java 17 and 11.
|
The current supported versions are Java 21 or 17.
|
||||||
|
|
||||||
**Arguments**
|
**Arguments**
|
||||||
|
|
||||||
|
|||||||
@@ -1829,7 +1829,10 @@ Commands to add servers or channels to the ignore list.
|
|||||||
|
|
||||||
The ignore list will prevent the bot from responding to commands in the configured locations.
|
The ignore list will prevent the bot from responding to commands in the configured locations.
|
||||||
|
|
||||||
.. Note:: Owners and Admins override the ignore list.
|
.. Note::
|
||||||
|
|
||||||
|
- Category ignores are ignored by user-installed commands
|
||||||
|
- Owners and Admins override the ignore list.
|
||||||
|
|
||||||
|
|
||||||
.. _core-command-ignore-channel:
|
.. _core-command-ignore-channel:
|
||||||
@@ -1850,7 +1853,10 @@ Ignore commands in the channel, thread, or category.
|
|||||||
|
|
||||||
Defaults to the current thread or channel.
|
Defaults to the current thread or channel.
|
||||||
|
|
||||||
.. Note:: Owners, Admins, and those with Manage Channel permissions override ignored channels.
|
.. Note::
|
||||||
|
|
||||||
|
- Category ignores are ignored by user-installed commands
|
||||||
|
- Owners and Admins override the ignore list.
|
||||||
|
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
@@ -2867,7 +2873,7 @@ Supports either an attachment or an image URL.
|
|||||||
**Examples:**
|
**Examples:**
|
||||||
- ``[p]set bot avatar`` - With an image attachment, this will set the avatar.
|
- ``[p]set bot avatar`` - With an image attachment, this will set the avatar.
|
||||||
- ``[p]set bot avatar`` - Without an attachment, this will show the command help.
|
- ``[p]set bot avatar`` - Without an attachment, this will show the command help.
|
||||||
- ``[p]set bot avatar https://links.flaree.xyz/k95`` - Sets the avatar to the provided url.
|
- ``[p]set bot avatar https://avatars.githubusercontent.com/u/23690422`` - Sets the avatar to the provided url.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- ``[url]`` - An image url to be used as an avatar. Leave blank when uploading an attachment.
|
- ``[url]`` - An image url to be used as an avatar. Leave blank when uploading an attachment.
|
||||||
@@ -2895,6 +2901,57 @@ Removes Red's avatar.
|
|||||||
**Example:**
|
**Example:**
|
||||||
- ``[p]set bot avatar remove``
|
- ``[p]set bot avatar remove``
|
||||||
|
|
||||||
|
.. _core-command-set-bot-banner:
|
||||||
|
|
||||||
|
""""""""""""""
|
||||||
|
set bot banner
|
||||||
|
""""""""""""""
|
||||||
|
|
||||||
|
.. note:: |owner-lock|
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]set bot banner [url]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Sets Red's banner
|
||||||
|
|
||||||
|
Supports either an attachment or an image URL.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- ``[p]set bot banner`` - With an image attachment, this will set the banner.
|
||||||
|
- ``[p]set bot banner`` - Without an attachment, this will show the command help.
|
||||||
|
- ``[p]set bot banner https://opengraph.githubassets.com`` - Sets the banner to the provided url.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- ``[url]`` - An image url to be used as an banner. Leave blank when uploading an attachment.
|
||||||
|
|
||||||
|
.. _core-command-set-bot-banner-remove:
|
||||||
|
|
||||||
|
"""""""""""""""""""""
|
||||||
|
set bot banner remove
|
||||||
|
"""""""""""""""""""""
|
||||||
|
|
||||||
|
.. note:: |owner-lock|
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]set bot banner remove
|
||||||
|
|
||||||
|
.. tip:: Alias: ``set bot banner clear``
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Removes Red's banner.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
- ``[p]set bot banner remove``
|
||||||
|
|
||||||
.. _core-command-set-bot-custominfo:
|
.. _core-command-set-bot-custominfo:
|
||||||
|
|
||||||
""""""""""""""""""
|
""""""""""""""""""
|
||||||
@@ -4134,7 +4191,7 @@ slash disablecog
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]slash disablecog <cog_name>
|
[p]slash disablecog <cog_names...>
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
@@ -4144,7 +4201,7 @@ This command does NOT sync the enabled commands with Discord, that must be done
|
|||||||
with ``[p]slash sync`` for commands to appear in users' clients.
|
with ``[p]slash sync`` for commands to appear in users' clients.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- ``<cog_name>`` - The cog to disable commands from. This argument is case sensitive.
|
- ``<cog_names>`` - The cogs to disable commands from. This argument is case sensitive.
|
||||||
|
|
||||||
.. _core-command-slash-enable:
|
.. _core-command-slash-enable:
|
||||||
|
|
||||||
@@ -4179,7 +4236,7 @@ slash enablecog
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]slash enablecog <cog_name>
|
[p]slash enablecog <cog_names...>
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
@@ -4189,7 +4246,7 @@ This command does NOT sync the enabled commands with Discord, that must be done
|
|||||||
with ``[p]slash sync`` for commands to appear in users' clients.
|
with ``[p]slash sync`` for commands to appear in users' clients.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- ``<cog_name>`` - The cog to enable commands from. This argument is case sensitive.
|
- ``<cog_names>`` - The cogs to enable commands from. This argument is case sensitive.
|
||||||
|
|
||||||
.. _core-command-slash-list:
|
.. _core-command-slash-list:
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,23 @@ modset dm
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]modset dm [enabled]
|
[p]modset dm
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Settings for messaging the user when being kicked or banned.
|
||||||
|
|
||||||
|
.. _mod-command-modset-dm-sendmessage:
|
||||||
|
|
||||||
|
"""""""""""""""""""""
|
||||||
|
modset dm sendmessage
|
||||||
|
"""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]modset dm sendmessage [enabled]
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
@@ -266,6 +282,94 @@ and reason as to why they were kicked/banned.
|
|||||||
|
|
||||||
* ``[enabled]``: Whether a message should be sent to a user when they are kicked/banned. |bool-input|
|
* ``[enabled]``: Whether a message should be sent to a user when they are kicked/banned. |bool-input|
|
||||||
|
|
||||||
|
.. _mod-command-modset-banshowextrafield:
|
||||||
|
|
||||||
|
"""""""""""""""""""""""""""
|
||||||
|
modset dm banshowextrafield
|
||||||
|
"""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]modset dm banshowextrafield [enabled]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Toggle whether to show an extra customizable field when banning.
|
||||||
|
|
||||||
|
This can be used to add additional information for the banned user, such as a ban appeal link.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``[enabled]``: If an extra customizable embed field should appear when banning. |bool-input|
|
||||||
|
|
||||||
|
.. _mod-command-modset-banextrafieldtitle:
|
||||||
|
|
||||||
|
""""""""""""""""""""""""""""
|
||||||
|
modset dm banextrafieldtitle
|
||||||
|
""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]modset dm banextrafieldtitle [title]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Set the title for the optional extra embed on ban.
|
||||||
|
|
||||||
|
Cannot be over 252 characters long.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``[title]``: The title of the embed field. Can by any string of text under 252 charcters long.
|
||||||
|
|
||||||
|
.. _mod-command-modset-banextrafieldcontents:
|
||||||
|
|
||||||
|
"""""""""""""""""""""""""""""""
|
||||||
|
modset dm banextrafieldcontents
|
||||||
|
"""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]modset dm banextrafieldcontents [contents]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Set the contents for the optional extra embed on ban
|
||||||
|
|
||||||
|
Cannot be over 1024 characters long.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``[contents]``: The contents of the embed field. Can by any string of text under 1024 charcters long.
|
||||||
|
|
||||||
|
.. _mod-command-modset-requirereason:
|
||||||
|
|
||||||
|
""""""""""""""""""""
|
||||||
|
modset requirereason
|
||||||
|
""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]modset requirereason [enabled]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Toggle whether a reason is required for mod actions.
|
||||||
|
|
||||||
|
If this is enabled, the bot will require a reason to be provided for all mod actions.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``[enabled]``: Whether a reason should be required when performing mod actions. |bool-input|
|
||||||
|
|
||||||
.. _mod-command-modset-hierarchy:
|
.. _mod-command-modset-hierarchy:
|
||||||
|
|
||||||
""""""""""""""""
|
""""""""""""""""
|
||||||
|
|||||||
@@ -145,28 +145,6 @@ If no time interval is provided this will be cleared.
|
|||||||
|
|
||||||
* ``[time]``: The length of time for a default mute.
|
* ``[time]``: The length of time for a default mute.
|
||||||
|
|
||||||
.. _mutes-command-muteset-forcerole:
|
|
||||||
|
|
||||||
"""""""""""""""""
|
|
||||||
muteset forcerole
|
|
||||||
"""""""""""""""""
|
|
||||||
|
|
||||||
.. note:: |owner-lock|
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
[p]muteset forcerole <true_or_false>
|
|
||||||
|
|
||||||
**Description**
|
|
||||||
|
|
||||||
Whether or not to force role only mutes on the bot.
|
|
||||||
|
|
||||||
**Arguments**
|
|
||||||
|
|
||||||
* ``<true_or_false>``: Whether to enable or disable this setting, must provide ``true`` or ``false``.
|
|
||||||
|
|
||||||
.. _mutes-command-muteset-makerole:
|
.. _mutes-command-muteset-makerole:
|
||||||
|
|
||||||
""""""""""""""""
|
""""""""""""""""
|
||||||
@@ -238,8 +216,8 @@ muteset role
|
|||||||
|
|
||||||
Sets the role to be applied when muting a user.
|
Sets the role to be applied when muting a user.
|
||||||
|
|
||||||
If no role is setup the bot will attempt to mute a user by setting
|
If no role is setup the bot will attempt to mute a user
|
||||||
channel overwrites in all channels to prevent the user from sending messages.
|
by utilizing server timeouts.
|
||||||
|
|
||||||
.. Note::
|
.. Note::
|
||||||
|
|
||||||
@@ -362,6 +340,34 @@ Unmute a user in this channel (or in the parent of this thread).
|
|||||||
* ``<users...>``: A space separated list of usernames, ID's, or mentions.
|
* ``<users...>``: A space separated list of usernames, ID's, or mentions.
|
||||||
* ``[reason]``: The reason for the unmute.
|
* ``[reason]``: The reason for the unmute.
|
||||||
|
|
||||||
|
.. _mutes-command-timeout:
|
||||||
|
|
||||||
|
^^^^^^^
|
||||||
|
timeout
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
.. note:: |mod-lock|
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]timeout <users...> [time_and_reason]
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Timeout users.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* ``[p]timeout @member1 @member2 spam 5 hours``
|
||||||
|
* ``[p]timeout @member1 3 days``
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``<users...>``: A space separated list of usernames, ID's, or mentions.
|
||||||
|
* ``[time_and_reason]``: The time and reason. If no time is provided, the mute will use the default set time or give an error if this hasn't been configured.
|
||||||
|
|
||||||
.. _mutes-command-voicemute:
|
.. _mutes-command-voicemute:
|
||||||
|
|
||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
@@ -372,7 +378,7 @@ voicemute
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]voicemute <users...> [reason]
|
[p]voicemute <users...> [time_and_reason]
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -334,6 +334,26 @@ will use all of the specified lists to select questions from.
|
|||||||
|
|
||||||
- ``<categories...>`` The category to play. Can be multiple.
|
- ``<categories...>`` The category to play. Can be multiple.
|
||||||
|
|
||||||
|
.. _trivia-command-trivia-info:
|
||||||
|
|
||||||
|
^^^^^^^^^^^
|
||||||
|
trivia info
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]trivia info <category>
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Get information about a trivia category.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``<category>``: The category to get the information for.
|
||||||
|
|
||||||
.. _trivia-command-trivia-leaderboard:
|
.. _trivia-command-trivia-leaderboard:
|
||||||
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ warningset showmoderator
|
|||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
Decide whether the name of the moderator warning a user should be included in the DM to that user.
|
Decide whether the name of the moderator warning a user should be included in the DM to that user when being warned or self requesting their warnings.
|
||||||
|
|
||||||
**Arguments**
|
**Arguments**
|
||||||
|
|
||||||
@@ -337,6 +337,26 @@ Set the channel where warnings should be sent to.
|
|||||||
|
|
||||||
* ``[channel]``: |channel-input| Leave empty to use the channel ``[p]warn`` command was called in.
|
* ``[channel]``: |channel-input| Leave empty to use the channel ``[p]warn`` command was called in.
|
||||||
|
|
||||||
|
.. _warnings-command-warningset-mywarnings-sendtodms:
|
||||||
|
|
||||||
|
"""""""""""""""""""""""""""""""
|
||||||
|
warningset mywarnings sendtodms
|
||||||
|
"""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
**Syntax**
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]warningset mywarnings sendtodms <true_or_false>
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Whether a member self requesting their warnings with ``[p]mywarnings`` should get them sent to DMs or in the current channel.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
* ``<true_or_false>``: |bool-input|
|
||||||
|
|
||||||
.. _warnings-command-warnreason:
|
.. _warnings-command-warnreason:
|
||||||
|
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|||||||
@@ -44,8 +44,11 @@ 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
@@ -139,6 +142,9 @@ html_context = {
|
|||||||
"github_user": "Cog-Creators",
|
"github_user": "Cog-Creators",
|
||||||
"github_repo": "Red-DiscordBot",
|
"github_repo": "Red-DiscordBot",
|
||||||
"github_version": "V3/develop",
|
"github_version": "V3/develop",
|
||||||
|
"version_slug": os.environ.get("READTHEDOCS_VERSION", ""),
|
||||||
|
"rtd_language": os.environ.get("READTHEDOCS_LANGUAGE", ""),
|
||||||
|
"READTHEDOCS": os.environ.get("READTHEDOCS", "") == "True",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
@@ -226,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":
|
||||||
@@ -250,7 +264,10 @@ intersphinx_mapping = {
|
|||||||
extlinks = {
|
extlinks = {
|
||||||
"dpy_docs": (f"{dpy_docs_url}%s", None),
|
"dpy_docs": (f"{dpy_docs_url}%s", None),
|
||||||
"issue": ("https://github.com/Cog-Creators/Red-DiscordBot/issues/%s", "#%s"),
|
"issue": ("https://github.com/Cog-Creators/Red-DiscordBot/issues/%s", "#%s"),
|
||||||
"ghuser": ("https://github.com/%s", "@%s"),
|
# below URL redirects to user page, if they don't have GH Sponsors set up,
|
||||||
|
# while allowing us to direct readers directly at a sponsorship opportunity,
|
||||||
|
# if they do
|
||||||
|
"ghuser": ("https://github.com/sponsors/%s", "@%s"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Doctest
|
# Doctest
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.. red app_commands module documentation
|
||||||
|
|
||||||
|
====================
|
||||||
|
App Commands Package
|
||||||
|
====================
|
||||||
|
|
||||||
|
This package acts almost identically to :doc:`discord.ext.app_commands <dpy:interactions/api>`; i.e.
|
||||||
|
all of the attributes from discord.py's are also in ours.
|
||||||
|
Some of these attributes, however, have been slightly modified, while others have been added to
|
||||||
|
extend functionalities used throughout the bot, as outlined below.
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.app_commands.UserFeedbackCheckFailure
|
||||||
|
:members:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ extend functionalities used throughout the bot, as outlined below.
|
|||||||
|
|
||||||
.. autoclass:: redbot.core.commands.DMContext
|
.. autoclass:: redbot.core.commands.DMContext
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.UserFeedbackCheckFailure
|
||||||
|
:members:
|
||||||
|
|
||||||
.. automodule:: redbot.core.commands.requires
|
.. automodule:: redbot.core.commands.requires
|
||||||
:members: PrivilegeLevel, PermState, Requires
|
:members: PrivilegeLevel, PermState, Requires
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,22 @@ Here is an example of the :code:`async with` syntax:
|
|||||||
blah.append(new_blah)
|
blah.append(new_blah)
|
||||||
await ctx.send("The new blah value has been added!")
|
await ctx.send("The new blah value has been added!")
|
||||||
|
|
||||||
|
There is also a :py:meth:`Group.all` method. This will return all the stored data associated
|
||||||
|
with a specific config group as a :py:class:`dict`. By negating the need to excessively call config,
|
||||||
|
this method can be particularly useful when multiple values are to be retrieved from the same group.
|
||||||
|
|
||||||
|
Here is an example of :py:meth:`Group.all` usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def getall(self, ctx):
|
||||||
|
all_global_data = await self.config.all()
|
||||||
|
await ctx.send("Foobar is {foobar}, foo baz is {foo_baz}".format(
|
||||||
|
foobar=str(all_global_data["foobar"]),
|
||||||
|
foo_baz=str(all_global_data["foo"]["baz"])
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
@@ -398,7 +414,7 @@ We're responsible pet owners here, so we've also got to have a way to feed our p
|
|||||||
# We could accomplish the same thing a slightly different way
|
# We could accomplish the same thing a slightly different way
|
||||||
await self.config.user(ctx.author).pets.get_attr(pet_name).hunger.set(new_hunger)
|
await self.config.user(ctx.author).pets.get_attr(pet_name).hunger.set(new_hunger)
|
||||||
|
|
||||||
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
|
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger))
|
||||||
|
|
||||||
Of course, if we're less than responsible pet owners, there are consequences::
|
Of course, if we're less than responsible pet owners, there are consequences::
|
||||||
|
|
||||||
@@ -430,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
|
||||||
************************************
|
************************************
|
||||||
@@ -481,7 +454,7 @@ Config prioritizes being a safe data store without developers needing to
|
|||||||
know how end users have configured their bot.
|
know how end users have configured their bot.
|
||||||
|
|
||||||
This does come with some performance costs, so keep the following in mind when choosing to
|
This does come with some performance costs, so keep the following in mind when choosing to
|
||||||
develop using config
|
develop using config.
|
||||||
|
|
||||||
* Config use in events should be kept minimal and should only occur
|
* Config use in events should be kept minimal and should only occur
|
||||||
after confirming the event needs to interact with config
|
after confirming the event needs to interact with config
|
||||||
|
|||||||
@@ -14,36 +14,96 @@ Basic Usage
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n, set_contextual_locales_from_guild
|
||||||
|
|
||||||
|
# The translator should be defined in the module scope, with __file__ as the second parameter
|
||||||
_ = Translator("ExampleCog", __file__)
|
_ = Translator("ExampleCog", __file__)
|
||||||
|
|
||||||
|
# This decorator must be used for cog and command docstrings to be translated!
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class ExampleCog:
|
class ExampleCog(commands.Cog):
|
||||||
"""description"""
|
"""Cog description"""
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def mycom(self, ctx):
|
async def mycom(self, ctx):
|
||||||
"""command description"""
|
"""Command description"""
|
||||||
await ctx.send(_("This is a test command"))
|
# Correct way to translate strings:
|
||||||
|
await ctx.send(_("This is a test command run by {author}!").format(author=ctx.author.display_name))
|
||||||
|
|
||||||
|
# !!! Do not do this - String interpolation should happen after translation
|
||||||
|
await ctx.send(_("This is a test command run by {author}!".format(author=ctx.author.display_name)))
|
||||||
|
|
||||||
|
# !!! Do not use f-strings - String interpolation should happen after translation
|
||||||
|
await ctx.send(_(f"This is a test command run by {ctx.author.display_name}!"))
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message(self, message):
|
||||||
|
# In non-command locations, you must manually call this method for guild locale settings to apply
|
||||||
|
await set_contextual_locales_from_guild(self.bot, message.guild)
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
await message.channel.send(_("This is a non command with translation support!"))
|
||||||
|
|
||||||
--------
|
--------
|
||||||
Tutorial
|
Tutorial
|
||||||
--------
|
--------
|
||||||
|
|
||||||
After making your cog, generate a :code:`messages.pot` file
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Preparing your cog for translations
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
We recommend using redgettext - a modified version of pygettext for Red.
|
The first step to adding translations to your cog is to add Red's internationalization framework
|
||||||
You can install redgettext by running :code:`pip install redgettext` in a command prompt.
|
to the strings in your cog. The first step is to instantiate an instance of
|
||||||
|
`redbot.core.i18n.Translator` just after the imports in each file. This object is traditionally
|
||||||
|
stored in the variable ``_`` to reduce its character count and visual impact on the code. Next,
|
||||||
|
add the `redbot.core.i18n.cog_i18n` decorator to your cog class. This will allow docstrings of
|
||||||
|
the class and its commands to be translated. Every user-facing string that is not a docstring
|
||||||
|
should then be wrapped by the Translator object. If variables are included in a string,
|
||||||
|
``.format()`` must be used, and should be called after the translation function call. This is
|
||||||
|
because ``.format()`` within the translation function call and f-strings cause the interpolation
|
||||||
|
to happen **before** the translation is applied. The translation logic needs to match the template
|
||||||
|
string to translate it, and will be unable to successfully match after interpolation occurs.
|
||||||
|
Finally, any non-command portions of your code, including listeners, tasks, and views, should call
|
||||||
|
`redbot.core.i18n.set_contextual_locales_from_guild` prior to translating any strings, as only
|
||||||
|
commands are able to implicitly determine which guild's configured locale to use. See the example
|
||||||
|
above for the exact recommended syntax.
|
||||||
|
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Generating a messages.pot file
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A ``messages.pot`` file is a template for translating all of the strings in your cog. It should
|
||||||
|
be generated using ``redgettext`` - a modified version of ``pygettext`` for use with Red cogs.
|
||||||
|
You can install ``redgettext`` by running :code:`pip install redgettext` in your development
|
||||||
|
environment.
|
||||||
|
|
||||||
|
Once you have ``redgettext`` installed, you will now need to run
|
||||||
|
|
||||||
|
:code:`python -m redgettext -c [path_to_cog_folder]`
|
||||||
|
|
||||||
|
This will generate a ``messages.pot`` file in ``path_to_cog_folder/locales``. This file will
|
||||||
|
contain all strings to be translated, including docstrings.
|
||||||
|
|
||||||
To generate the :code:`messages.pot` file, you will now need to run
|
|
||||||
:code:`python -m redgettext -c [path_to_cog]`
|
|
||||||
This file will contain all strings to be translated, including
|
|
||||||
docstrings.
|
|
||||||
(For advanced usage check :code:`python -m redgettext -h`)
|
(For advanced usage check :code:`python -m redgettext -h`)
|
||||||
|
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Creating language specific translations
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
You can now use a tool like `poedit
|
You can now use a tool like `poedit
|
||||||
<https://poedit.net/>`_ to translate the strings in your messages.pot file.
|
<https://poedit.net/>`_ to translate the strings in your ``messages.pot`` file.
|
||||||
|
|
||||||
|
Alternatively, you can use any text editor to manually create translations. To do this, first
|
||||||
|
create a copy of the ``messages.pot`` file in the same folder, and name the copy
|
||||||
|
``LANGUAGE-CODE.po``, where ``LANGUAGE-CODE`` is a five character language code supported by
|
||||||
|
``[p]set locale``. Open the copy in your text editor of choice. This file contains the strings
|
||||||
|
in your cog prefixed by ``msgid`` and an empty string for you to apply translations prefixed by
|
||||||
|
``msgstr``. The original string should be translated to the target language by modifying the
|
||||||
|
associated ``msgstr``. Any variables within curly braces should **not** be translated to avoid
|
||||||
|
breaking the code when translations are applied. If keyword arguments were used in ``.format()``
|
||||||
|
calls, it may be safe to re-order variables if the grammer of the language requires doing so.
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
API Reference
|
API Reference
|
||||||
@@ -51,4 +111,4 @@ API Reference
|
|||||||
|
|
||||||
.. automodule:: redbot.core.i18n
|
.. automodule:: redbot.core.i18n
|
||||||
:members:
|
:members:
|
||||||
:special-members: __call__
|
:special-members: __call__, __init__
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ If you want to do it, follow these steps.
|
|||||||
2. **Install Linux**
|
2. **Install Linux**
|
||||||
|
|
||||||
Most of the VPS providers have tools for installing Linux automatically. If
|
Most of the VPS providers have tools for installing Linux automatically. If
|
||||||
you're a beginner, we recommend **Ubuntu 22.04 LTS**.
|
you're a beginner, we recommend **Ubuntu 24.04 LTS**.
|
||||||
|
|
||||||
For Raspberry Pi users, just install `Raspbian
|
For Raspberry Pi users, just install `Raspbian
|
||||||
<https://www.raspberrypi.org/software/>`_ on a micro-SD card.
|
<https://www.raspberrypi.org/software/>`_ on a micro-SD card.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -31,7 +31,7 @@ Open a terminal or command prompt and type one of the following
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
||||||
link below. **The development version of the bot contains experimental changes. It is not
|
value below. **The development version of the bot contains experimental changes. It is not
|
||||||
intended for normal users.** We will not support anyone using the development version in any
|
intended for normal users.** We will not support anyone using the development version in any
|
||||||
support channels. Using the development version may break third party cogs and not all core
|
support channels. Using the development version may break third party cogs and not all core
|
||||||
commands may work. Downgrading to stable after installing the development version may cause
|
commands may work. Downgrading to stable after installing the development version may cause
|
||||||
@@ -40,7 +40,7 @@ Open a terminal or command prompt and type one of the following
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot
|
Red-DiscordBot @ https://github.com/Cog-Creators/Red-DiscordBot/tarball/V3/develop
|
||||||
|
|
||||||
|
|
||||||
(Windows users may need to use :code:`py -3.8` or :code:`python` instead of :code:`python3.8`)
|
(Windows users may need to use :code:`py -3.8` or :code:`python` instead of :code:`python3.8`)
|
||||||
@@ -55,7 +55,7 @@ the purposes of this example, we'll call this :code:`mycog`).
|
|||||||
In this folder, create three files: :code:`__init__.py`,
|
In this folder, create three files: :code:`__init__.py`,
|
||||||
:code:`mycog.py`, and :code:`info.json`. Open the folder in
|
:code:`mycog.py`, and :code:`info.json`. Open the folder in
|
||||||
a text editor or IDE (examples include `Sublime Text 3 <https://www.sublimetext.com/>`_,
|
a text editor or IDE (examples include `Sublime Text 3 <https://www.sublimetext.com/>`_,
|
||||||
`Visual Studio Code <https://code.visualstudio.com/>`_, `Atom <https://atom.io/>`_, and
|
`Visual Studio Code <https://code.visualstudio.com/>`_, and
|
||||||
`PyCharm <http://www.jetbrains.com/pycharm/>`_).
|
`PyCharm <http://www.jetbrains.com/pycharm/>`_).
|
||||||
|
|
||||||
.. attention::
|
.. attention::
|
||||||
@@ -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
|
||||||
@@ -147,7 +147,7 @@ have successfully created a cog!
|
|||||||
------ __init__.py
|
------ __init__.py
|
||||||
------ coolcog.py
|
------ coolcog.py
|
||||||
|
|
||||||
You would then use :code:`[p]addpath D:\red-cogs` to add the path
|
You would then use :code:`[p]addpath D:\\red-cogs` to add the path
|
||||||
and then you can use :code:`[p]load mycog` or :code:`[p]load coolcog`
|
and then you can use :code:`[p]load mycog` or :code:`[p]load coolcog`
|
||||||
to load them
|
to load them
|
||||||
|
|
||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -41,17 +41,16 @@ Any Cog Creator that does not follow these requirements will have their repo rem
|
|||||||
- Repo-wide ``info.json`` file with the keys
|
- Repo-wide ``info.json`` file with the keys
|
||||||
|
|
||||||
- ``author``
|
- ``author``
|
||||||
- ``name``
|
|
||||||
- ``short``
|
- ``short``
|
||||||
- ``description``
|
- ``description``
|
||||||
|
|
||||||
- Cog ``info.json`` files with the keys
|
- Cog ``info.json`` files with the keys
|
||||||
|
|
||||||
- ``author``
|
- ``author``
|
||||||
- ``name``
|
|
||||||
- ``short``
|
- ``short``
|
||||||
- ``requirements`` (if applicable)
|
- ``requirements`` (if applicable)
|
||||||
- ``description``
|
- ``description``
|
||||||
|
- ``min_python_version`` (if applicable)
|
||||||
|
|
||||||
See `info-json-format` for more information on how to set up ``info.json`` files.
|
See `info-json-format` for more information on how to set up ``info.json`` files.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -47,7 +47,7 @@ Keys common to both repo and cog info.json (case sensitive)
|
|||||||
is installed or a repo is added
|
is installed or a repo is added
|
||||||
|
|
||||||
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||||
used for installing.
|
used for installing, and ``[botname]`` to show the bot's username.
|
||||||
|
|
||||||
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||||
is displayed when a user executes ``[p]cog list``
|
is displayed when a user executes ``[p]cog list``
|
||||||
|
|||||||
@@ -41,12 +41,33 @@ If there are multiple authors, we can separate them with commas.
|
|||||||
|
|
||||||
AUTHOR: Red, Rojo, Rouge
|
AUTHOR: Red, Rojo, Rouge
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
Description Field
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
We can also add an optional ``DESCRIPTION`` to our trivia list, which
|
||||||
|
will show from the output of ``[p]trivia info <category>``. The
|
||||||
|
description should indicate to the user what the trivia list is
|
||||||
|
about and what kind of questions they can expect to face.
|
||||||
|
|
||||||
|
For example, if you were writing a logo quiz trivia list, you could
|
||||||
|
create a description like this:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
AUTHOR: Kreusada
|
||||||
|
DESCRIPTION: >-
|
||||||
|
A quiz to test your logo knowledge to the limit. This trivia
|
||||||
|
will send image URLs and ask you to identify the company's name
|
||||||
|
from the logo that is sent.
|
||||||
|
|
||||||
---------------------
|
---------------------
|
||||||
Questions and Answers
|
Questions and Answers
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
Writing questions and answers is simple. Once you've finished your
|
Writing questions and answers is simple. Once you've finished your
|
||||||
``AUTHOR`` field, you can move on to your questions just below.
|
``AUTHOR`` field and ``DESCRIPTION`` field, you can move on to your questions
|
||||||
|
just below.
|
||||||
|
|
||||||
Questions should consist of at least one answer, with other
|
Questions should consist of at least one answer, with other
|
||||||
possible answers included if necessary. You must put a colon at the end
|
possible answers included if necessary. You must put a colon at the end
|
||||||
@@ -98,6 +119,7 @@ As you've added more questions, your file should look something like this:
|
|||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
AUTHOR: Red
|
AUTHOR: Red
|
||||||
|
DESCRIPTION: A general quiz to test your knowledge.
|
||||||
How many days are there in a regular year?:
|
How many days are there in a regular year?:
|
||||||
- 365
|
- 365
|
||||||
- three hundred and sixty five
|
- three hundred and sixty five
|
||||||
|
|||||||
@@ -32,12 +32,11 @@ First, we would like to make something clear:
|
|||||||
Hosting on a VPS or Dedicated Server
|
Hosting on a VPS or Dedicated Server
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
| You can host Red in a VPS running Linux or Windows. Using a Linux VPS is the
|
| You can host Red on a VPS running Linux or Windows. Using a Linux VPS is the
|
||||||
recommended option. Dedicated servers also work but are overpowered and cost
|
recommended option. Dedicated servers also work but are overpowered and cost
|
||||||
ineffective unless one plans to run a very large bot or use their server for
|
ineffective unless one plans to run a very large bot or use their server for
|
||||||
more than just hosting Red. If you have already created an instance, Red can be moved to a different
|
more than just hosting Red. If you have already created an instance, Red can be moved to a different
|
||||||
server for hosting with a backup/restore process. More information and guidance
|
server for hosting using the :doc:`backup/restore process </backup_red>`.
|
||||||
about this process is available in the `Red Support Server <https://discord.com/invite/red>`_.
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
Please be aware that a Linux server is controlled through a command line.
|
Please be aware that a Linux server is controlled through a command line.
|
||||||
@@ -87,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
|
||||||
@@ -114,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.
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ For Developers
|
|||||||
Removals
|
Removals
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
|
``SimpleMenu.select_menu`` attribute
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated-removed:: 3.5.14 60
|
||||||
|
|
||||||
|
The `SimpleMenu.select_menu` attribute has been deprecated.
|
||||||
|
|
||||||
|
Any behaviour enabled by the usage of this attribute should no longer be depended on.
|
||||||
|
If you need this for something and cannot replace it with the other functionality,
|
||||||
|
create an issue on Red's issue tracker.
|
||||||
|
|
||||||
Downloader's shared libraries
|
Downloader's shared libraries
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
----
|
----
|
||||||
@@ -15,6 +15,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
install_guides/index
|
install_guides/index
|
||||||
bot_application_guide
|
bot_application_guide
|
||||||
update_red
|
update_red
|
||||||
|
backup_red
|
||||||
about_venv
|
about_venv
|
||||||
autostart_windows
|
autostart_windows
|
||||||
autostart_mac
|
autostart_mac
|
||||||
@@ -61,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
|
||||||
@@ -71,6 +71,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
framework_bot
|
framework_bot
|
||||||
framework_checks
|
framework_checks
|
||||||
framework_commands
|
framework_commands
|
||||||
|
framework_app_commands
|
||||||
framework_config
|
framework_config
|
||||||
framework_datamanager
|
framework_datamanager
|
||||||
framework_events
|
framework_events
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
To install/update pyenv, run the following command:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
command -v pyenv && pyenv update || curl https://pyenv.run | bash
|
|
||||||
|
|
||||||
After this command, you will see a warning about 'pyenv' not being in the load path. To address this,
|
|
||||||
you should run these commands:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
profile=$([ -n "$ZSH_VERSION" ] && echo ~/.zprofile || ([ -f ~/.bash_profile ] && echo ~/.bash_profile || echo ~/.profile))
|
|
||||||
rcfile=$([ -n "$ZSH_VERSION" ] && echo ~/.zshrc || echo ~/.bashrc)
|
|
||||||
printf '%s\n%s\n%s\n' 'export PYENV_ROOT="$HOME/.pyenv"' 'export PATH="$PYENV_ROOT/bin:$PATH"' "$([ -f "$profile" ] && cat "$profile")" > "$profile"
|
|
||||||
echo 'eval "$(pyenv init --path)"' >> "$profile"
|
|
||||||
echo 'eval "$(pyenv init -)"' >> "$rcfile"
|
|
||||||
echo 'eval "$(pyenv virtualenv-init -)"' >> "$rcfile"
|
|
||||||
|
|
||||||
Then **log out and log back in** and run the following command:
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
------------------------------
|
|
||||||
Creating a Virtual Environment
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
|
|
||||||
If you want to learn more about virtual environments, see page: `about-venvs`
|
|
||||||
|
|
||||||
We require installing Red into a virtual environment. Don't be scared, it's very
|
|
||||||
straightforward.
|
|
||||||
|
|
||||||
**************************
|
|
||||||
Using ``pyenv virtualenv``
|
|
||||||
**************************
|
|
||||||
|
|
||||||
Using ``pyenv virtualenv`` saves you the headache of remembering where you installed your virtual
|
|
||||||
environments. This option is only available if you installed Python with pyenv.
|
|
||||||
|
|
||||||
First, ensure your pyenv interpreter is set to python 3.8.1 or greater with the following command:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv version
|
|
||||||
|
|
||||||
Now, create a virtual environment with the following command:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv virtualenv <name>
|
|
||||||
|
|
||||||
Replace ``<name>`` with whatever you like. If you ever forget what you named it,
|
|
||||||
you can always use the command ``pyenv versions`` to list all virtual environments.
|
|
||||||
|
|
||||||
Now activate your virtualenv with the following command:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv shell <name>
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
You must activate the virtual environment with the above command every time you open a new
|
|
||||||
shell to run, install or update Red. You can check out other commands like ``pyenv local`` and
|
|
||||||
``pyenv global`` if you wish to keep the virtualenv activated all the time.
|
|
||||||
@@ -8,6 +8,7 @@ To install without additional config backend support:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
:modifiers: red-install-guide-install-normal
|
||||||
|
|
||||||
python -m pip install -U pip wheel
|
python -m pip install -U pip wheel
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
@@ -16,6 +17,7 @@ Or, to install with PostgreSQL support:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
:modifiers: red-install-guide-install-postgres
|
||||||
|
|
||||||
python -m pip install -U pip wheel
|
python -m pip install -U pip wheel
|
||||||
python -m pip install -U "Red-DiscordBot[postgres]"
|
python -m pip install -U "Red-DiscordBot[postgres]"
|
||||||
@@ -29,6 +31,7 @@ After installation, set up your instance with the following command:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
:modifiers: red-install-guide-setup
|
||||||
|
|
||||||
redbot-setup
|
redbot-setup
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
:prompts: (redenv) $
|
:prompts: (redenv) $
|
||||||
|
:modifiers: red-install-guide-run
|
||||||
|
|
||||||
redbot <your instance name>
|
redbot <your instance name>
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,14 @@
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Red Hat Enterprise Linux (RHEL) 8.6-8.x and its derivatives have all required packages available in official repositories.
|
Red Hat Enterprise Linux (RHEL) 8.10 and its derivatives have all required packages available in official repositories.
|
||||||
Install them with dnf:
|
Install them with dnf:
|
||||||
|
|
||||||
.. TODO: Use Python 3.11 once RHEL 8.6 goes EOL in 2024.
|
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo dnf -y update
|
sudo dnf -y update
|
||||||
sudo dnf -y group install development
|
sudo dnf -y group install development
|
||||||
sudo dnf -y install python39 python39-devel java-17-openjdk-headless nano git
|
sudo dnf -y install python3.11 python3.11-devel java-17-openjdk-headless nano git
|
||||||
|
|
||||||
Set ``java`` executable to point to Java 17:
|
Set ``java`` executable to point to Java 17:
|
||||||
|
|
||||||
@@ -25,6 +23,6 @@ Set ``java`` executable to point to Java 17:
|
|||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.9.rst
|
.. include:: _includes/create-env-with-venv3.11.rst
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
.. include:: _includes/install-and-setup-red-unix.rst
|
||||||
|
|||||||
@@ -6,17 +6,15 @@
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Red Hat Enterprise Linux (RHEL) 9 and its derivatives have all required packages available in official repositories.
|
Red Hat Enterprise Linux (RHEL) 9.4-9.x and its derivatives have all required packages available in official repositories.
|
||||||
Install them with dnf:
|
Install them with dnf:
|
||||||
|
|
||||||
.. TODO: Use Python 3.11 once RHEL 9.0 goes EOL in 2024.
|
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
sudo dnf -y install python39 python3-devel git java-17-openjdk-headless @development nano
|
sudo dnf -y install python3.11 python3.11-devel git java-17-openjdk-headless @development nano
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.9.rst
|
.. include:: _includes/create-env-with-venv3.11.rst
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
.. include:: _includes/install-and-setup-red-unix.rst
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
----------------------------
|
|
||||||
Installing Python with pyenv
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
On distributions where Python 3.9 needs to be compiled from source, we recommend the use of pyenv.
|
|
||||||
This simplifies the compilation process and has the added bonus of simplifying setting up Red in a
|
|
||||||
virtual environment.
|
|
||||||
|
|
||||||
.. include:: _includes/_install-pyenv-and-setup-path.rst
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.9.18 -v
|
|
||||||
|
|
||||||
This may take a long time to complete, depending on your hardware. For some machines (such as
|
|
||||||
Raspberry Pis and micro-tier VPSes), it may take over an hour; in this case, you may wish to remove
|
|
||||||
the ``CONFIGURE_OPTS=--enable-optimizations`` part from the front of the command, which will
|
|
||||||
drastically reduce the install time. However, be aware that this will make Python run about 10%
|
|
||||||
slower.
|
|
||||||
|
|
||||||
After that is finished, run:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv global 3.9.18
|
|
||||||
|
|
||||||
Pyenv is now installed and your system should be configured to run Python 3.9.
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
For safety reasons, DO NOT install Red with a root user. If you are unsure how to create
|
For safety reasons, DO NOT install Red with a root user. If you are unsure how to create
|
||||||
a new user on Linux, see `DigitalOcean's tutorial: How To Create a New Sudo-enabled User
|
a new user on Linux, see `DigitalOcean's tutorial: How To Create a New Sudo-enabled User
|
||||||
<https://www.digitalocean.com/community/tutorials/how-to-create-a-new-sudo-enabled-user-on-ubuntu-20-04-quickstart>`_.
|
<https://www.digitalocean.com/community/tutorials/how-to-create-a-new-sudo-enabled-user-on-ubuntu>`_.
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.. _install-amazon-linux-2023:
|
||||||
|
|
||||||
|
===================================
|
||||||
|
Installing Red on Amazon Linux 2023
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. include:: _includes/supported-arch-x64+aarch64.rst
|
||||||
|
|
||||||
|
.. include:: _includes/linux-preamble.rst
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
Installing the pre-requirements
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Amazon Linux 2023 has all required packages available in official repositories. Install
|
||||||
|
them with dnf:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
sudo dnf -y install python3.11 python3.11-devel git java-17-amazon-corretto-headless @development nano
|
||||||
|
|
||||||
|
.. Include common instructions:
|
||||||
|
|
||||||
|
.. include:: _includes/create-env-with-venv3.11.rst
|
||||||
|
|
||||||
|
.. include:: _includes/install-and-setup-red-unix.rst
|
||||||
@@ -18,20 +18,20 @@ Install the pre-requirements with pacman:
|
|||||||
|
|
||||||
sudo pacman -Syu git jre17-openjdk-headless base-devel nano
|
sudo pacman -Syu git jre17-openjdk-headless base-devel nano
|
||||||
|
|
||||||
On Arch Linux, Python 3.10 can be installed from the Arch User Repository (AUR) from the ``python310`` package.
|
On Arch Linux, Python 3.11 can be installed from the Arch User Repository (AUR) from the ``python311`` package.
|
||||||
|
|
||||||
The manual build process is the Arch-supported install method for AUR packages. You can install ``python310`` package with the following commands:
|
The manual build process is the Arch-supported install method for AUR packages. You can install ``python311`` package with the following commands:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|
||||||
git clone https://aur.archlinux.org/python310.git /tmp/python310
|
git clone https://aur.archlinux.org/python311.git /tmp/python311
|
||||||
cd /tmp/python310
|
cd /tmp/python311
|
||||||
makepkg -sicL
|
makepkg -sicL
|
||||||
cd -
|
cd -
|
||||||
rm -rf /tmp/python310
|
rm -rf /tmp/python311
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.10.rst
|
.. include:: _includes/create-env-with-venv3.11.rst
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
.. include:: _includes/install-and-setup-red-unix.rst
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
.. _install-centos-7:
|
|
||||||
|
|
||||||
==========================
|
|
||||||
Installing Red on CentOS 7
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-x64+aarch64.rst
|
|
||||||
|
|
||||||
.. include:: _includes/linux-preamble.rst
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
Installing the pre-requirements
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Install the pre-requirements with yum:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo yum -y groupinstall development
|
|
||||||
sudo yum -y install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz-devel java-11-openjdk-headless nano git
|
|
||||||
|
|
||||||
In order to install gcc 8, we'll use SCL repository:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo yum -y install centos-release-scl
|
|
||||||
sudo yum -y install devtoolset-8-gcc devtoolset-8-gcc-c++
|
|
||||||
echo "source scl_source enable devtoolset-8" >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
In order to install Git 2.11 or greater, we recommend adding the IUS repository:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo yum -y install https://repo.ius.io/ius-release-el7.rpm
|
|
||||||
sudo yum -y swap git git236
|
|
||||||
|
|
||||||
.. Include common instructions:
|
|
||||||
|
|
||||||
.. Python 3.10 requires OpenSSL 1.1.1 which CentOS 7 doesn't provide in base repository.
|
|
||||||
|
|
||||||
.. include:: _includes/install-python39-pyenv.rst
|
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-pyenv-virtualenv.rst
|
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.. _install-centos-stream-8:
|
|
||||||
|
|
||||||
=================================
|
|
||||||
Installing Red on CentOS Stream 8
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. include:: _includes/install-guide-rhel8-derivatives.rst
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
.. _install-debian-11:
|
|
||||||
|
|
||||||
====================================
|
|
||||||
Installing Red on Debian 11 Bullseye
|
|
||||||
====================================
|
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-x64+aarch64+armv7l.rst
|
|
||||||
|
|
||||||
.. include:: _includes/linux-preamble.rst
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
Installing the pre-requirements
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Debian 11 "Bullseye" has all required packages available in official repositories. Install them
|
|
||||||
with apt:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo apt update
|
|
||||||
sudo apt -y install python3 python3-dev python3-venv git openjdk-17-jre-headless build-essential nano
|
|
||||||
|
|
||||||
.. Include common instructions:
|
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.9.rst
|
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
|
||||||
@@ -12,12 +12,14 @@ Installing Red on Fedora Linux
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Fedora Linux 38 and above has all required packages available in official repositories. Install
|
Fedora Linux 42 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 java-17-openjdk-headless @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 -y install temurin-17-jre
|
||||||
|
|
||||||
.. Include common instructions:
|
.. Include common instructions:
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Installing Red
|
|||||||
The list below shows the installation guides available based on the operating system being used.
|
The list below shows the installation guides available based on the operating system being used.
|
||||||
|
|
||||||
If you want to host Red on a VPS and are unsure what operating system you should choose,
|
If you want to host Red on a VPS and are unsure what operating system you should choose,
|
||||||
we recommend **Ubuntu 22.04 LTS**.
|
we recommend **Ubuntu 24.04 LTS**.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
@@ -15,23 +15,20 @@ we recommend **Ubuntu 22.04 LTS**.
|
|||||||
mac
|
mac
|
||||||
alma-linux-8
|
alma-linux-8
|
||||||
alma-linux-9
|
alma-linux-9
|
||||||
|
amazon-linux-2023
|
||||||
arch
|
arch
|
||||||
centos-7
|
|
||||||
centos-stream-8
|
|
||||||
centos-stream-9
|
centos-stream-9
|
||||||
debian-11
|
|
||||||
debian-12
|
debian-12
|
||||||
fedora
|
fedora
|
||||||
opensuse-leap-15
|
opensuse-leap-15
|
||||||
opensuse-tumbleweed
|
opensuse-tumbleweed
|
||||||
oracle-linux-8
|
oracle-linux-8
|
||||||
oracle-linux-9
|
oracle-linux-9
|
||||||
raspberry-pi-os-11
|
|
||||||
raspberry-pi-os-12
|
raspberry-pi-os-12
|
||||||
rhel-8
|
rhel-8
|
||||||
rhel-9
|
rhel-9
|
||||||
rocky-linux-8
|
rocky-linux-8
|
||||||
rocky-linux-9
|
rocky-linux-9
|
||||||
ubuntu-2004
|
|
||||||
ubuntu-2204
|
ubuntu-2204
|
||||||
|
ubuntu-2404
|
||||||
ubuntu-non-lts
|
ubuntu-non-lts
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ one-by-one:
|
|||||||
|
|
||||||
brew install python@3.11
|
brew install python@3.11
|
||||||
brew install git
|
brew install git
|
||||||
brew tap homebrew/cask-versions
|
brew install temurin@17
|
||||||
brew install --cask temurin17
|
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. _install-opensuse-leap-15:
|
.. _install-opensuse-leap-15:
|
||||||
|
|
||||||
=====================================
|
=====================================
|
||||||
Installing Red on openSUSE Leap 15.5+
|
Installing Red on openSUSE Leap 15.6+
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-x64+aarch64.rst
|
.. include:: _includes/supported-arch-x64+aarch64.rst
|
||||||
@@ -12,7 +12,7 @@ Installing Red on openSUSE Leap 15.5+
|
|||||||
Installing the pre-requirements
|
Installing the pre-requirements
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
openSUSE Leap 15.5+ has all required dependencies available in official repositories. Install them
|
openSUSE Leap 15.6+ has all required dependencies available in official repositories. Install them
|
||||||
with zypper:
|
with zypper:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
.. _install-raspberry-pi-os-11:
|
|
||||||
|
|
||||||
======================================================
|
|
||||||
Installing Red on Raspberry Pi OS (Legacy) 11 Bullseye
|
|
||||||
======================================================
|
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-aarch64+armv7l.rst
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
While we do provide support and install instructions for running Red
|
|
||||||
on Raspberry Pi OS (Legacy) 11 Bullseye, we highly recommend installing/upgrading to
|
|
||||||
the new version - Raspberry Pi OS 12 Bookworm.
|
|
||||||
|
|
||||||
If you're not sure what version you are using,
|
|
||||||
you can check your version of Raspberry Pi OS by running:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
lsb_release -a
|
|
||||||
|
|
||||||
If you're running Bookworm already, read `install-raspberry-pi-os-12` document instead.
|
|
||||||
|
|
||||||
If you're using Bullseye, please consider performing a clean install of Bookworm if possible.
|
|
||||||
|
|
||||||
.. include:: _includes/linux-preamble.rst
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
Installing the pre-requirements
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Raspberry Pi OS "Bullseye" has all required packages available in official repositories. Install them
|
|
||||||
with apt:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo apt update
|
|
||||||
sudo apt -y install python3 python3-dev python3-venv git openjdk-17-jre-headless build-essential nano
|
|
||||||
|
|
||||||
.. Include common instructions:
|
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.9.rst
|
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
.. _install-raspberry-pi-os-12:
|
.. _install-raspberry-pi-os-12:
|
||||||
|
|
||||||
=============================================
|
======================================================
|
||||||
Installing Red on Raspberry Pi OS 12 Bookworm
|
Installing Red on Raspberry Pi OS (Legacy) 12 Bookworm
|
||||||
=============================================
|
======================================================
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-aarch64+armv7l.rst
|
.. include:: _includes/supported-arch-aarch64+armv7l.rst
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. _install-rhel-8:
|
.. _install-rhel-8:
|
||||||
|
|
||||||
=========================================================
|
======================================================
|
||||||
Installing Red on Red Hat Enterprise Linux (RHEL) 8.6-8.x
|
Installing Red on Red Hat Enterprise Linux (RHEL) 8.10
|
||||||
=========================================================
|
======================================================
|
||||||
|
|
||||||
.. include:: _includes/install-guide-rhel8-derivatives.rst
|
.. include:: _includes/install-guide-rhel8-derivatives.rst
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. _install-rhel-9:
|
.. _install-rhel-9:
|
||||||
|
|
||||||
===================================================
|
=========================================================
|
||||||
Installing Red on Red Hat Enterprise Linux (RHEL) 9
|
Installing Red on Red Hat Enterprise Linux (RHEL) 9.4-9.x
|
||||||
===================================================
|
=========================================================
|
||||||
|
|
||||||
.. include:: _includes/install-guide-rhel9-derivatives.rst
|
.. include:: _includes/install-guide-rhel9-derivatives.rst
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
.. _install-ubuntu-2004:
|
|
||||||
|
|
||||||
==================================
|
|
||||||
Installing Red on Ubuntu 20.04 LTS
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-x64+aarch64.rst
|
|
||||||
|
|
||||||
.. include:: _includes/linux-preamble.rst
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
Installing the pre-requirements
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Ubuntu 20.04 LTS has all required packages available in official repositories. Install them
|
|
||||||
with apt:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo apt update
|
|
||||||
sudo apt -y install python3.9 python3.9-dev python3.9-venv git openjdk-17-jre-headless build-essential nano
|
|
||||||
|
|
||||||
.. Include common instructions:
|
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.9.rst
|
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.. _install-ubuntu-2404:
|
||||||
|
|
||||||
|
==================================
|
||||||
|
Installing Red on Ubuntu 24.04 LTS
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. include:: _includes/supported-arch-x64+aarch64.rst
|
||||||
|
|
||||||
|
.. include:: _includes/linux-preamble.rst
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
Installing the pre-requirements
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
We recommend adding the ``deadsnakes`` ppa to install Python 3.11:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt -y install software-properties-common
|
||||||
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
|
||||||
|
Now install the pre-requirements with apt:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
sudo apt -y install python3.11 python3.11-dev python3.11-venv git openjdk-17-jre-headless build-essential nano
|
||||||
|
|
||||||
|
.. Include common instructions:
|
||||||
|
|
||||||
|
.. include:: _includes/create-env-with-venv3.11.rst
|
||||||
|
|
||||||
|
.. include:: _includes/install-and-setup-red-unix.rst
|
||||||
@@ -4,23 +4,10 @@
|
|||||||
Installing Red on Ubuntu non-LTS versions
|
Installing Red on Ubuntu non-LTS versions
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
.. include:: _includes/supported-arch-x64+aarch64.rst
|
Latest Ubuntu non-LTS version (24.10 at the time of writing) is not supported at current time
|
||||||
|
due to lack of availability of Python 3.11 or older in its repositories.
|
||||||
|
|
||||||
.. include:: _includes/linux-preamble.rst
|
The support should come back once we get back on track with supporting current Python versions.
|
||||||
|
|
||||||
-------------------------------
|
We recommend usage of latest Ubuntu **LTS** versions instead, you can find
|
||||||
Installing the pre-requirements
|
`an install guide for Ubuntu 24.04 <ubuntu-2404>` in our docs.
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Now install the pre-requirements with apt:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
sudo apt update
|
|
||||||
sudo apt -y install python3.11 python3.11-dev python3.11-venv git openjdk-17-jre-headless build-essential nano
|
|
||||||
|
|
||||||
.. Include common instructions:
|
|
||||||
|
|
||||||
.. include:: _includes/create-env-with-venv3.11.rst
|
|
||||||
|
|
||||||
.. include:: _includes/install-and-setup-red-unix.rst
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ Run **one** of the following set of commands, depending on what extras you want
|
|||||||
|
|
||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
:modifiers: red-install-guide-install-normal
|
||||||
|
|
||||||
python -m pip install -U pip wheel
|
python -m pip install -U pip wheel
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
@@ -141,6 +142,7 @@ Run **one** of the following set of commands, depending on what extras you want
|
|||||||
|
|
||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
:modifiers: red-install-guide-install-postgres
|
||||||
|
|
||||||
python -m pip install -U pip wheel
|
python -m pip install -U pip wheel
|
||||||
python -m pip install -U Red-DiscordBot[postgres]
|
python -m pip install -U Red-DiscordBot[postgres]
|
||||||
@@ -153,6 +155,7 @@ After installation, set up your instance with the following command:
|
|||||||
|
|
||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
:modifiers: red-install-guide-setup
|
||||||
|
|
||||||
redbot-setup
|
redbot-setup
|
||||||
|
|
||||||
@@ -164,6 +167,7 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
|
|
||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
:modifiers: red-install-guide-run
|
||||||
|
|
||||||
redbot <your instance name>
|
redbot <your instance name>
|
||||||
|
|
||||||
|
|||||||
@@ -8,40 +8,20 @@ About (privileged) intents and public bots
|
|||||||
==========================================
|
==========================================
|
||||||
|
|
||||||
This page aims to explain Red's current intents requirements,
|
This page aims to explain Red's current intents requirements,
|
||||||
our stance regarding "public bots" and the impact of some announced
|
our stance regarding "public bots", and the discord bot verification process.
|
||||||
Discord changes coming in April 2022.
|
|
||||||
|
|
||||||
To clarify:
|
To clarify:
|
||||||
|
|
||||||
- **Small bots** are bots under 100 servers. They currently do not need to undergo Discord's
|
- **Small bots** are bots under 100 servers. They currently do not need to undergo Discord's
|
||||||
bot verification process
|
bot verification process
|
||||||
- **Public bots** (or big bots) are bots that have reached 100 servers. They need to be
|
- **Public bots** (or big bots) are bots that have reached 100 servers. They need to be
|
||||||
`verified <https://support.discord.com/hc/en-us/articles/360040720412-Bot-Verification-and-Data-Whitelisting>`_
|
`verified <https://support-dev.discord.com/hc/en-us/articles/23926564536471-How-Do-I-Get-My-App-Verified>`_
|
||||||
by Discord to join more than 100 servers and gain privileged intents
|
by Discord to join more than 100 servers and gain privileged intents
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
It is **very** important that you fully read this page if you're the owner of a public bot or strive to scale your bot at that level.
|
It is **very** important that you fully read this page if you're the owner of a public bot or strive to scale your bot at that level.
|
||||||
|
|
||||||
.. _intents-intents:
|
|
||||||
|
|
||||||
-------
|
|
||||||
Intents
|
|
||||||
-------
|
|
||||||
|
|
||||||
Red currently requires **all intents** to be active in order to function properly.
|
|
||||||
|
|
||||||
The reason for this requirement is that there are some technical challenges that need
|
|
||||||
to be overcome before we're able to adapt Red to function with only *some* intents:
|
|
||||||
these challenges are mainly due to the modular / extensible nature of Red and the fact
|
|
||||||
that Red has a long history (dating back to 2016!), making big changes naturally slower
|
|
||||||
to happen. In comparison, intents have been introduced fairly recently. |br|
|
|
||||||
This is not a problem if you have a small bot: you can simply go to the
|
|
||||||
`Discord development portal <https://discord.com/developers/applications/me>`_
|
|
||||||
and enable them. However, if you have a public bot Discord will want you to attain
|
|
||||||
verified status: you should read :ref:`our stance regarding public bots <intents-public-bots>`
|
|
||||||
and our guidelines for the :ref:`verification process <intents-bot-verification-process>`.
|
|
||||||
|
|
||||||
.. _intents-public-bots:
|
.. _intents-public-bots:
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
@@ -54,8 +34,10 @@ Red was designed with one single goal in mind: a bot that you can host on your o
|
|||||||
and customize to your needs, making it really *your* bot. **The target audience of Red are server
|
and customize to your needs, making it really *your* bot. **The target audience of Red are server
|
||||||
owners with a few servers**, often with specific needs that can be covered by the vast cog ecosystem
|
owners with a few servers**, often with specific needs that can be covered by the vast cog ecosystem
|
||||||
that the community has built over the years. |br| Red was never built with big bots in mind,
|
that the community has built over the years. |br| Red was never built with big bots in mind,
|
||||||
bots with thousands upon thousands of servers: these bots face unique challenges.
|
bots with thousands upon thousands of servers: these bots face unique challenges. Large bots need
|
||||||
Such Red instances *do exist*, it is not impossible to adapt Red and meet those criteria,
|
to be extremely efficient to handle the large amount of requests they receive, and often need to
|
||||||
|
distribute this work across multiple processes or machines to keep up.
|
||||||
|
Such Red instances *do exist*, and it is not impossible to adapt Red and meet those criteria,
|
||||||
but it requires work and bot owners with the technical knowledge to make it happen.
|
but it requires work and bot owners with the technical knowledge to make it happen.
|
||||||
It is **not** something that we support. |br|
|
It is **not** something that we support. |br|
|
||||||
When your bot reaches the public bot scale and it is therefore required to be verified it
|
When your bot reaches the public bot scale and it is therefore required to be verified it
|
||||||
@@ -75,8 +57,8 @@ the verification process.
|
|||||||
|
|
||||||
Regardless of our stance, we do feel the need to give some pointers: many bot owners reach this point
|
Regardless of our stance, we do feel the need to give some pointers: many bot owners reach this point
|
||||||
and become fairly lost, as they've simply been *users* so far.
|
and become fairly lost, as they've simply been *users* so far.
|
||||||
They have installed their bot, some cogs, personalized it, yadda yadda. Again, they have been users,
|
They have installed their bot, some cogs, personalized it, but have not needed to write any code.
|
||||||
not developers. Unless they also have an interest in development, they will likely not have a clue about
|
Unless they also have an interest in development, they will likely not have a clue about
|
||||||
what's going under the hood, much like you're not expected to be a mechanic to drive your car. And there's
|
what's going under the hood, much like you're not expected to be a mechanic to drive your car. And there's
|
||||||
nothing wrong with that! Red has been designed to be as user friendly as possible. |br|
|
nothing wrong with that! Red has been designed to be as user friendly as possible. |br|
|
||||||
The problem is this: Red is an outlier. Discord has built the bot verification process with the expectation
|
The problem is this: Red is an outlier. Discord has built the bot verification process with the expectation
|
||||||
@@ -94,41 +76,44 @@ out your application:
|
|||||||
of people that in their naivety went with the bad answer and it seems that at this point merely mentioning Red
|
of people that in their naivety went with the bad answer and it seems that at this point merely mentioning Red
|
||||||
is a guaranteed way to have your application rejected.
|
is a guaranteed way to have your application rejected.
|
||||||
|
|
||||||
.. _intents-slash-commands:
|
.. _intents-intents:
|
||||||
|
|
||||||
---------------------------------
|
-------
|
||||||
Message intent and slash commands
|
Intents
|
||||||
---------------------------------
|
-------
|
||||||
|
|
||||||
.. warning::
|
Red expects **all intents** to be active. It is possible, but not recommended, to disable
|
||||||
|
specific intents using the ``--disable-intent`` flag. If an intent is missing, you may
|
||||||
|
experience errors due to Red expecting information provided by the intent to be present.
|
||||||
|
|
||||||
If you own a public bot it is extremely important that you read this section.
|
Discord currently considers 3 intents to be
|
||||||
|
`privileged <https://support-dev.discord.com/hc/en-us/articles/6205754771351-How-do-I-get-Privileged-Intents-for-my-bot>`_,
|
||||||
|
and requires large bots to additionally apply for access to these intents. **If you have a small
|
||||||
|
bot**, you can simply follow :ref:`these instructions <enabling-privileged-intents>` to enable them.
|
||||||
|
|
||||||
Discord has announced that **starting April 2022** the content of users' messages
|
A breakdown of how privileged intents are used in Red is provided below.
|
||||||
`will be "locked" behind message intent <https://support-dev.discord.com/hc/en-us/articles/4404772028055>`_ |br|
|
|
||||||
If you're the owner of a small bot, fear not, this is yet another box that you have to tick from the
|
|
||||||
`Discord development portal <https://discord.com/developers/applications/me>`_. |br|
|
|
||||||
But if you're the owner of a public bot, things might be a lot less pleasant.
|
|
||||||
|
|
||||||
To recap, unless you have
|
The **Message Content** intent is required to use text based commands and inputs for
|
||||||
message intent, you will only receive message content for:
|
configuration and all built in functionality. App commands (also known as slash commands)
|
||||||
|
are limited to a total of 100 top level commands, which is difficult to manage on
|
||||||
|
a modular bot. The approach we have taken to address this issue is to allow 3rd party
|
||||||
|
cogs to provide slash commands, but require bot owners to pick which slash commands
|
||||||
|
they actually want to use with the ``[p]slash`` command.
|
||||||
|
Under this system, bot management commands that are not exposed to users are still
|
||||||
|
expected to be provided as text commands, which requires the bot to be able to access
|
||||||
|
message content. There are no current plans to provide slash versions of core commands.
|
||||||
|
|
||||||
- Messages that your bot sends
|
.. note::
|
||||||
- Messages that your bot receives in DM
|
It is possible to work around this intent by using the ``--mentionable``
|
||||||
- Messages in which your bot is mentioned
|
flag, and using the bot mention as a prefix to use text based commands.
|
||||||
|
|
||||||
In case it's not clear by now, your bot needs message content to parse (see) the commands it receives. And if
|
The **Guild Members** intent is required to properly cache member information, including
|
||||||
you don't attain message intent, your bot will not be able to... well, do anything. |br|
|
what users are in each server, what roles they have, what their name is, etc. It is also
|
||||||
The *bandaid fix* is for you to change your bot's prefix to a mention and a good portion of your commands will likely
|
required to receive events corresponding to when members join or leave a server, and when
|
||||||
still work. You will however lose many functions, namely anything that relies on seeing message content to act. |br|
|
they change their nickname or other server options. Almost all cogs expect to be able
|
||||||
The more *proper fix* is also not easy. You will need to justify your need for the message intent to Discord and
|
to reference the member cache in order to avoid making API requests, and are not set
|
||||||
they will only accept "compelling use cases".
|
up to check if the intent is present before doing so.
|
||||||
`It is not known what those even entail <https://gist.github.com/spiralw/091714718718379b6efcdbcaf807a024#q-what-usecases-will-be-valid>`_ at this point, but they have already stated that "parsing commands" is not a valid justification. |br|
|
|
||||||
To make the matter worse, Discord is making `a huge push for all bot developers to implement slash commands <https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ>`_, which at the moment
|
The **Guild Presences** intent is required to view the activities and status of
|
||||||
are rather lacking in features and cannot cover all the functionalities that standard commands offer. |br|
|
users. Cogs which perform actions on users based on their activity or status will
|
||||||
Discord staff
|
be unable to access this information if this intent is not enabled.
|
||||||
`stated that they will want your bot to have slash commands when you ask for message intent <https://gist.github.com/spiralw/091714718718379b6efcdbcaf807a024#q-if-we-are-granted-this-intent-will-bots-be-sanctioned-if-they-use-it-for-their-own-use-case-but-also-to-continue-to-run-normal-non-slash-commands-or-do-we-assume-that-if-you-are-granted-the-intent-you-are-trusted-with-it-and-are-allowed-to-use-it-for-additional-uses>`_. |br|
|
|
||||||
Slash commands might very well turn out to be a big undertaking for the Red team to implement, even more now that our
|
|
||||||
underlying library, `discord.py <https://github.com/Rapptz/discord.py>`_, has been discontinued. |br|
|
|
||||||
The time window that Discord is giving us to adapt is very narrow: **Red will likely not be able to support slash
|
|
||||||
commands for April 2022** and you should plan accordingly.
|
|
||||||
|
|||||||
@@ -25,13 +25,57 @@ 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.
|
||||||
|
#. Activate your venv with the following command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
|
||||||
|
"%userprofile%\redenv\Scripts\activate.bat"
|
||||||
|
#. Update Red with this command:
|
||||||
|
|
||||||
|
.. prompt:: batch
|
||||||
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
|
redbot-update
|
||||||
|
#. Start your bot.
|
||||||
|
|
||||||
|
Linux & Mac
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If you have Red 3.5.25 or newer, 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) $
|
||||||
|
|
||||||
|
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.
|
#. Shut your bot down.
|
||||||
#. Activate your venv with the following command:
|
#. Activate your venv with the following command:
|
||||||
@@ -55,7 +99,7 @@ If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
|||||||
Linux & Mac
|
Linux & Mac
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
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.
|
#. Shut your bot down.
|
||||||
#. Activate your virtual environment.
|
#. Activate your virtual environment.
|
||||||
@@ -66,11 +110,6 @@ If you have Red 3.5.0 or newer, you can upgrade by following these steps:
|
|||||||
|
|
||||||
source ~/redenv/bin/activate
|
source ~/redenv/bin/activate
|
||||||
|
|
||||||
If you used ``pyenv`` for your virtual environment, use:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv shell <name>
|
|
||||||
#. Update Red with this command:
|
#. Update Red with this command:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
@@ -113,7 +152,6 @@ If you have a Red version between 3.2.0 and 3.4.19, you can upgrade by following
|
|||||||
#. Start your bot with ``--no-cogs --load-cogs downloader`` flags, for example:
|
#. Start your bot with ``--no-cogs --load-cogs downloader`` flags, for example:
|
||||||
|
|
||||||
.. prompt:: batch
|
.. prompt:: batch
|
||||||
|
|
||||||
:prompts: (redenv) C:\\>
|
:prompts: (redenv) C:\\>
|
||||||
|
|
||||||
redbot <your instance name> --no-cogs --load-cogs downloader
|
redbot <your instance name> --no-cogs --load-cogs downloader
|
||||||
@@ -138,11 +176,6 @@ If you have a Red version between 3.2.0 and 3.4.19, you can upgrade by following
|
|||||||
|
|
||||||
source ~/redenv/bin/activate
|
source ~/redenv/bin/activate
|
||||||
|
|
||||||
If you used ``pyenv`` for your virtual environment, use:
|
|
||||||
|
|
||||||
.. prompt:: bash
|
|
||||||
|
|
||||||
pyenv shell <name>
|
|
||||||
#. Update Red with this command:
|
#. Update Red with this command:
|
||||||
|
|
||||||
.. prompt:: bash
|
.. prompt:: bash
|
||||||
|
|||||||
@@ -50,38 +50,33 @@ their end-of-life date.
|
|||||||
================================ ======================= ============================================================
|
================================ ======================= ============================================================
|
||||||
Operating system version Supported architectures Ideally supported until
|
Operating system version Supported architectures Ideally supported until
|
||||||
================================ ======================= ============================================================
|
================================ ======================= ============================================================
|
||||||
Windows 10 x86-64 `End/Retirement Date <https://docs.microsoft.com/en-us/lifecycle/products/windows-10-home-and-pro>`__
|
Windows 10 x86-64 2026-10-13 (`End of Consumer Extended Security Updates (ESU) program <https://www.microsoft.com/en-us/windows/extended-security-updates>`__)
|
||||||
Windows 11 x86-64 `Retirement Date <https://docs.microsoft.com/en-us/lifecycle/products/windows-11-home-and-pro-version-21h2>`__
|
Windows 11 x86-64 `Retirement Date <https://docs.microsoft.com/en-us/lifecycle/products/windows-11-home-and-pro-version-21h2>`__
|
||||||
macOS 12 (Monterey) x86-64, aarch64 ~2024-10
|
|
||||||
macOS 13 (Ventura) x86-64, aarch64 ~2025-10
|
|
||||||
macOS 14 (Sonoma) x86-64, aarch64 ~2026-10
|
macOS 14 (Sonoma) x86-64, aarch64 ~2026-10
|
||||||
Alma Linux 8 x86-64, aarch64 2029-05-31 (`How long will CloudLinux support AlmaLinux? <https://wiki.almalinux.org/FAQ.html#how-long-will-almalinux-be-supported>`__)
|
macOS 15 (Sequoia) x86-64, aarch64 ~2027-10
|
||||||
Alma Linux 9 x86-64, aarch64 2032-05-31
|
macOS 26 (Tahoe) x86-64, aarch64 ~2028-10
|
||||||
|
Alma Linux 8 x86-64, aarch64 2029-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>`__)
|
||||||
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 7 x86-64, aarch64 2024-06-30 (`end of Maintenance Updates <https://wiki.centos.org/About/Product>`__)
|
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`Expected EOL <https://centos.org/stream9/#timeline>`__)
|
||||||
CentOS Stream 8 x86-64, aarch64 2024-05-31 (`end of Maintenance Updates <https://wiki.centos.org/About/Product>`__)
|
Debian 12 Bookworm x86-64, aarch64, armv7l 2026-06-10 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
|
||||||
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`expected EOL <https://centos.org/stream9/#timeline>`__)
|
Fedora Linux 42 x86-64, aarch64 2026-05-13 (`End of Life <https://fedorapeople.org/groups/schedule/f-42/f-42-key-tasks.html>`__)
|
||||||
Debian 11 Bullseye x86-64, aarch64, armv7l ~2024-07 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
|
Fedora Linux 43 x86-64, aarch64 2026-12-09 (`End of Life <https://fedorapeople.org/groups/schedule/f-43/f-43-key-tasks.html>`__)
|
||||||
Debian 12 Bookworm x86-64, aarch64, armv7l ~2026-09 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
|
openSUSE Leap 15.6 x86-64, aarch64 2025-12-31 (`end of maintenance lifecycle <https://en.opensuse.org/Lifetime#openSUSE_Leap>`__)
|
||||||
Fedora Linux 38 x86-64, aarch64 2024-05-14 (`End of Life <https://docs.fedoraproject.org/en-US/releases/lifecycle/#_maintenance_schedule>`__)
|
|
||||||
Fedora Linux 39 x86-64, aarch64 2024-11-12 (`End of Life <https://docs.fedoraproject.org/en-US/releases/lifecycle/#_maintenance_schedule>`__)
|
|
||||||
openSUSE Leap 15.5 x86-64, aarch64 2024-12-31 (`end of maintenance life cycle <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>`__)
|
||||||
Raspberry Pi OS (Legacy) 11 armv7l ~2025-10 (approximate date of release of Raspberry Pi OS 13)
|
Raspberry Pi OS (Legacy) 12 aarch64, armv7l ~2027-10 (approximate date of release of Raspberry Pi OS 14)
|
||||||
Raspberry Pi OS 12 aarch64, armv7l ~2025-10 (approximate date of release of Raspberry Pi OS 13)
|
|
||||||
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.6 x86-64, aarch64 2024-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 8.8 x86-64, aarch64 2025-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.0 x86-64, aarch64 2024-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
RHEL 9.4 x86-64, aarch64 2026-04-30 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
|
||||||
RHEL 9.2 x86-64, aarch64 2025-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 (`end-of-life <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 (`end-of-life <https://rockylinux.org/download/>`__)
|
Rocky Linux 9 x86-64, aarch64 2032-05-31 (`End of Life <https://wiki.rockylinux.org/rocky/version/>`__)
|
||||||
Ubuntu 20.04 LTS x86-64, aarch64 2025-04-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 22.04 LTS x86-64, aarch64 2027-04-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 23.10 x86-64, aarch64 2024-07-31 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
|
|
||||||
================================ ======================= ============================================================
|
================================ ======================= ============================================================
|
||||||
|
|
||||||
.. _developer-guarantees:
|
.. _developer-guarantees:
|
||||||
|
|||||||
@@ -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.7"
|
_VERSION = "3.5.25.dev1"
|
||||||
|
|
||||||
__version__, version_info = VersionInfo._get_version()
|
__version__, version_info = VersionInfo._get_version()
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -397,7 +379,8 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
|
|||||||
"With that out of the way, depending on who you want to be considered as owner,"
|
"With that out of the way, depending on who you want to be considered as owner,"
|
||||||
" you can:\n"
|
" you can:\n"
|
||||||
"a) pass --team-members-are-owners when launching Red"
|
"a) pass --team-members-are-owners when launching Red"
|
||||||
" - in this case Red will treat all members of the bot application's team as owners\n"
|
" - in this case Red will treat members of the bot application's team as owners,"
|
||||||
|
" if their team role is Owner, Admin, or Developer\n"
|
||||||
f"b) set owner manually with `redbot --edit {cli_flags.instance_name}`\n"
|
f"b) set owner manually with `redbot --edit {cli_flags.instance_name}`\n"
|
||||||
"c) pass owner ID(s) when launching Red with --owner"
|
"c) pass owner ID(s) when launching Red with --owner"
|
||||||
" (and --co-owner if you need more than one) flag\n"
|
" (and --co-owner if you need more than one) flag\n"
|
||||||
@@ -421,20 +404,13 @@ def handle_early_exit_flags(cli_flags: Namespace):
|
|||||||
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
sys.exit(ExitCodes.INVALID_CLI_USAGE)
|
||||||
|
|
||||||
|
|
||||||
async def shutdown_handler(red, signal_type=None, exit_code=None):
|
async def signal_shutdown_handler(red: Red, signal_type: signal.Signals) -> NoReturn:
|
||||||
if signal_type:
|
|
||||||
log.info("%s received. Quitting...", signal_type.name)
|
log.info("%s received. Quitting...", signal_type.name)
|
||||||
# Do not collapse the below line into other logic
|
|
||||||
# We need to renter this function
|
|
||||||
# after it interrupts the event loop.
|
|
||||||
sys.exit(ExitCodes.SHUTDOWN)
|
sys.exit(ExitCodes.SHUTDOWN)
|
||||||
elif exit_code is None:
|
|
||||||
log.info("Shutting down from unhandled exception")
|
|
||||||
red._shutdown_mode = ExitCodes.CRITICAL
|
|
||||||
|
|
||||||
if exit_code is not None:
|
|
||||||
|
async def shutdown_handler(red: Red, exit_code: int) -> None:
|
||||||
red._shutdown_mode = exit_code
|
red._shutdown_mode = exit_code
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not red.is_closed():
|
if not red.is_closed():
|
||||||
await red.close()
|
await red.close()
|
||||||
@@ -472,7 +448,8 @@ def red_exception_handler(red, red_task: asyncio.Future):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.critical("The main bot task didn't handle an exception and has crashed", exc_info=exc)
|
log.critical("The main bot task didn't handle an exception and has crashed", exc_info=exc)
|
||||||
log.warning("Attempting to die as gracefully as possible...")
|
log.warning("Attempting to die as gracefully as possible...")
|
||||||
asyncio.create_task(shutdown_handler(red))
|
log.info("Shutting down from unhandled exception")
|
||||||
|
sys.exit(ExitCodes.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -483,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:
|
||||||
@@ -498,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.
|
||||||
@@ -506,7 +483,7 @@ def main():
|
|||||||
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
|
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
|
||||||
for s in signals:
|
for s in signals:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(
|
||||||
s, lambda s=s: asyncio.create_task(shutdown_handler(red, s))
|
s, lambda s=s: asyncio.create_task(signal_shutdown_handler(red, s))
|
||||||
)
|
)
|
||||||
|
|
||||||
exc_handler = functools.partial(global_exception_handler, red)
|
exc_handler = functools.partial(global_exception_handler, red)
|
||||||
@@ -523,7 +500,7 @@ def main():
|
|||||||
log.warning("Please do not use Ctrl+C to Shutdown Red! (attempting to die gracefully...)")
|
log.warning("Please do not use Ctrl+C to Shutdown Red! (attempting to die gracefully...)")
|
||||||
log.error("Received KeyboardInterrupt, treating as interrupt")
|
log.error("Received KeyboardInterrupt, treating as interrupt")
|
||||||
if red is not None:
|
if red is not None:
|
||||||
loop.run_until_complete(shutdown_handler(red, signal.SIGINT))
|
loop.run_until_complete(signal_shutdown_handler(red, signal.SIGINT))
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
# We also have to catch this one here. Basically any exception which normally
|
# We also have to catch this one here. Basically any exception which normally
|
||||||
# Kills the python interpreter (Base Exceptions minus asyncio.cancelled)
|
# Kills the python interpreter (Base Exceptions minus asyncio.cancelled)
|
||||||
@@ -535,11 +512,11 @@ def main():
|
|||||||
exit_code_name = "UNKNOWN"
|
exit_code_name = "UNKNOWN"
|
||||||
log.info("Shutting down with exit code: %s (%s)", exit_code, exit_code_name)
|
log.info("Shutting down with exit code: %s (%s)", exit_code, exit_code_name)
|
||||||
if red is not None:
|
if red is not None:
|
||||||
loop.run_until_complete(shutdown_handler(red, None, exc.code))
|
loop.run_until_complete(shutdown_handler(red, exc.code))
|
||||||
except Exception as exc: # Non standard case.
|
except Exception as exc: # Non standard case.
|
||||||
log.exception("Unexpected exception (%s): ", type(exc), exc_info=exc)
|
log.exception("Unexpected exception (%s): ", type(exc), exc_info=exc)
|
||||||
if red is not None:
|
if red is not None:
|
||||||
loop.run_until_complete(shutdown_handler(red, None, ExitCodes.CRITICAL))
|
loop.run_until_complete(shutdown_handler(red, ExitCodes.CRITICAL))
|
||||||
finally:
|
finally:
|
||||||
# Allows transports to close properly, and prevent new ones from being opened.
|
# Allows transports to close properly, and prevent new ones from being opened.
|
||||||
# Transports may still not be closed correctly on windows, see below
|
# Transports may still not be closed correctly on windows, see below
|
||||||
|
|||||||
@@ -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,215 @@
|
|||||||
|
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()
|
||||||
|
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,736 @@
|
|||||||
|
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()
|
||||||
|
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))
|
||||||
@@ -122,7 +122,7 @@ msgstr "\n"
|
|||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:275
|
#: redbot/cogs/admin/admin.py:275
|
||||||
msgid "{author} ({author.id}) changed the colour of role '{role.name}'"
|
msgid "{author} ({author.id}) changed the colour of role '{role.name}'"
|
||||||
msgstr ""
|
msgstr "{author}({author.id}) تغيير لون رول {role.name}"
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:294 redbot/cogs/admin/admin.py:327
|
#: redbot/cogs/admin/admin.py:294 redbot/cogs/admin/admin.py:327
|
||||||
msgid "Done."
|
msgid "Done."
|
||||||
@@ -144,7 +144,7 @@ msgstr "\n"
|
|||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:308
|
#: redbot/cogs/admin/admin.py:308
|
||||||
msgid "{author} ({author.id}) changed the name of role '{old_name}' to '{name}'"
|
msgid "{author} ({author.id}) changed the name of role '{old_name}' to '{name}'"
|
||||||
msgstr ""
|
msgstr "{author}({author.id}) تغيير اسم رول {old_name} إلى {name}"
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:332
|
#: redbot/cogs/admin/admin.py:332
|
||||||
#, docstring
|
#, docstring
|
||||||
@@ -185,7 +185,7 @@ msgstr "تم تحديث قناة الإعلانات إلى {channel.mention}"
|
|||||||
#: redbot/cogs/admin/admin.py:375
|
#: redbot/cogs/admin/admin.py:375
|
||||||
#, docstring
|
#, docstring
|
||||||
msgid "Unsets the channel for announcements."
|
msgid "Unsets the channel for announcements."
|
||||||
msgstr ""
|
msgstr "إزالة القناة من الإعلانات."
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:400
|
#: redbot/cogs/admin/admin.py:400
|
||||||
#, docstring
|
#, docstring
|
||||||
@@ -194,7 +194,11 @@ msgid "\n"
|
|||||||
" Server admins must have configured the role as user settable.\n"
|
" Server admins must have configured the role as user settable.\n"
|
||||||
" NOTE: The role is case sensitive!\n"
|
" NOTE: The role is case sensitive!\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr "\n"
|
||||||
|
" أزاله أو ازالة الرول الشخصي منك.\n\n"
|
||||||
|
" يجب أن يكون لدى أدمن السيرفر اعدادت للتعيين من قبل المستخدم.\n"
|
||||||
|
" ملاحظة: الرول حساس لحالة الأحرف\n"
|
||||||
|
" "
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:413
|
#: redbot/cogs/admin/admin.py:413
|
||||||
#, docstring
|
#, docstring
|
||||||
@@ -253,15 +257,15 @@ msgstr "لا يمكنني السماح لك بإضافة {role.name} لرتبة
|
|||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:475
|
#: redbot/cogs/admin/admin.py:475
|
||||||
msgid "The role \"{role.name}\" is already a selfrole."
|
msgid "The role \"{role.name}\" is already a selfrole."
|
||||||
msgstr ""
|
msgstr "الرول \"{role.name}\" هو بالفعل رول شخصي."
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:481
|
#: redbot/cogs/admin/admin.py:481
|
||||||
msgid "Added {count} selfroles."
|
msgid "Added {count} selfroles."
|
||||||
msgstr ""
|
msgstr "إضافة {count} رول شخصي."
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:483
|
#: redbot/cogs/admin/admin.py:483
|
||||||
msgid "Added 1 selfrole."
|
msgid "Added 1 selfrole."
|
||||||
msgstr ""
|
msgstr "تم اضافة 1 رول شخصي."
|
||||||
|
|
||||||
#: redbot/cogs/admin/admin.py:489
|
#: redbot/cogs/admin/admin.py:489
|
||||||
#, docstring
|
#, docstring
|
||||||
|
|||||||