mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-08 10:22:31 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e34975001c | ||
|
|
f3b282062b | ||
|
|
84732a24fa | ||
|
|
dad775b494 | ||
|
|
05ad3fcd5c | ||
|
|
6ae02d2d02 | ||
|
|
757a3114dc | ||
|
|
94b9878c6c | ||
|
|
7775b16199 | ||
|
|
f01d48f9ae | ||
|
|
179883094e | ||
|
|
971ccf9df4 | ||
|
|
07eb6bf88e | ||
|
|
5afd8174ca | ||
|
|
f1fea38712 | ||
|
|
f275c6e5e7 | ||
|
|
5ec25959df | ||
|
|
4f270f3aab | ||
|
|
4028dd3009 | ||
|
|
706b04610d | ||
|
|
014e3baea0 | ||
|
|
92ca7c935a | ||
|
|
5c9b1c9a3d | ||
|
|
5ebee60c97 | ||
|
|
3337a9cbab | ||
|
|
54975eb812 | ||
|
|
537531803a | ||
|
|
f4b640126b | ||
|
|
1de3251127 | ||
|
|
7e98076e4a | ||
|
|
c58c55b752 | ||
|
|
928be5717f | ||
|
|
ccbaa926ce | ||
|
|
d1208d7d19 | ||
|
|
099fe59a97 | ||
|
|
889acaec82 | ||
|
|
c42e9d4c5c | ||
|
|
4378e5295d | ||
|
|
73a427f6aa | ||
|
|
abfee70eb3 | ||
|
|
77cdbf8dd6 | ||
|
|
28bc68c916 | ||
|
|
ecb64cc2ec | ||
|
|
23706a1ba9 | ||
|
|
d3f406a34a | ||
|
|
55afc7eb33 | ||
|
|
7a70d12efd | ||
|
|
1ecaf6f8d5 | ||
|
|
e01cdbb091 | ||
|
|
b88b5a2601 | ||
|
|
e7476edd68 | ||
|
|
cbbeb412f9 | ||
|
|
f544890f00 | ||
|
|
72560fa6d0 | ||
|
|
4637ff78c0 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -43,6 +43,7 @@ redbot/cogs/streams/* @Twentysix26 @palmtree5
|
|||||||
redbot/cogs/trivia/* @Tobotimus
|
redbot/cogs/trivia/* @Tobotimus
|
||||||
redbot/cogs/dataconverter/* @mikeshardmind
|
redbot/cogs/dataconverter/* @mikeshardmind
|
||||||
redbot/cogs/reports/* @mikeshardmind
|
redbot/cogs/reports/* @mikeshardmind
|
||||||
|
redbot/cogs/permissions/* @mikeshardmind
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|||||||
116
.github/CONTRIBUTING.md
vendored
116
.github/CONTRIBUTING.md
vendored
@@ -1,14 +1,34 @@
|
|||||||
# Introduction
|
# Contents
|
||||||
### Welcome!
|
* [1. Introduction](#1-introduction)
|
||||||
First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
|
* [1.1 Why do these guidelines exist?](#11-why-do-these-guidelines-exist)
|
||||||
|
* [1.2 What kinds of contributions are we looking for?](#12-what-kinds-of-contributions-are-we-looking-for)
|
||||||
|
* [2. Ground Rules](#2-ground-rules)
|
||||||
|
* [3. Your First Contribution](#3-your-first-contribution)
|
||||||
|
* [4. Getting Started](#4-getting-started)
|
||||||
|
* [4.1 Setting up your development environment](#41-setting-up-your-development-environment)
|
||||||
|
* [4.2 Testing](#42-testing)
|
||||||
|
* [4.3 Style](#43-style)
|
||||||
|
* [4.4 Make](#44-make)
|
||||||
|
* [4.5 Keeping your dependencies up to date](#45-keeping-your-dependencies-up-to-date)
|
||||||
|
* [4.6 To contribute changes](#46-to-contribute-changes)
|
||||||
|
* [4.7 How To Report A Bug](#47-how-to-report-a-bug)
|
||||||
|
* [4.8 How To Suggest A Feature Or Enhancement](#48-how-to-suggest-a-feature-or-enhancement)
|
||||||
|
* [5. Code Review Process](#5-code-review-process)
|
||||||
|
* [5.1 Issues](#51-issues)
|
||||||
|
* [5.2 Pull Requests](#52-pull-requests)
|
||||||
|
* [5.3 Differences between "new features" and "improvements"](#53-differences-between-new-features-and-improvements)
|
||||||
|
* [6. Community](#6-community)
|
||||||
|
|
||||||
### Why do these guidelines exist?
|
# 1. Introduction
|
||||||
|
**Welcome!** First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
|
||||||
|
|
||||||
|
### 1.1 Why do these guidelines exist?
|
||||||
Red is an open source project. This means that each and every one of the developers and contributors who have helped make Red what it is today have done so by volunteering their time and effort. It takes a lot of time to coordinate and organize issues and new features and to review and test pull requests. By following these guidelines you will help the developers streamline the contribution process and save them time. In doing so we hope to get back to each and every issue and pull request in a timely manner.
|
Red is an open source project. This means that each and every one of the developers and contributors who have helped make Red what it is today have done so by volunteering their time and effort. It takes a lot of time to coordinate and organize issues and new features and to review and test pull requests. By following these guidelines you will help the developers streamline the contribution process and save them time. In doing so we hope to get back to each and every issue and pull request in a timely manner.
|
||||||
|
|
||||||
### What kinds of contributions are we looking for?
|
### 1.2 What kinds of contributions are we looking for?
|
||||||
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
|
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
|
||||||
|
|
||||||
# Ground Rules
|
# 2. Ground Rules
|
||||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||||
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
||||||
@@ -17,7 +37,7 @@ We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub
|
|||||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||||
6. Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/).
|
6. Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/).
|
||||||
|
|
||||||
# Your First Contribution
|
# 3. Your First Contribution
|
||||||
Unsure of how to get started contributing to Red? Please take a look at the Issues section of this repo and sort by the following labels:
|
Unsure of how to get started contributing to Red? Please take a look at the Issues section of this repo and sort by the following labels:
|
||||||
|
|
||||||
* beginner - issues that can normally be fixed in just a few lines of code and maybe a test or two.
|
* beginner - issues that can normally be fixed in just a few lines of code and maybe a test or two.
|
||||||
@@ -27,35 +47,89 @@ Unsure of how to get started contributing to Red? Please take a look at the Issu
|
|||||||
|
|
||||||
At this point you're ready to start making changes. Feel free to ask for help; everyone was a beginner at some point!
|
At this point you're ready to start making changes. Feel free to ask for help; everyone was a beginner at some point!
|
||||||
|
|
||||||
# Getting Started
|
# 4. Getting Started
|
||||||
### Testing
|
|
||||||
We've recently started adding unit-testing into Red. All current tests can be found in the `tests/` directory at the root level of the repository. You will need `py.test` installed in order to run them (which is already in `requirement.txt`). Tests can be run by simply calling `pytest` once you've `cd`'d into the Red repository folder.
|
|
||||||
|
|
||||||
### To contribute changes
|
Red's repository is configured to follow a particular development workflow, using various reputable tools. We kindly ask that you stick to this workflow when contributing to Red, by following the guides below. This will help you to easily produce quality code, identify errors early, and streamline the code review process.
|
||||||
1. Create your own fork of the Red repository.
|
|
||||||
2. Make the changes in your own fork.
|
### 4.1 Setting up your development environment
|
||||||
|
The following requirements must be installed prior to setting up:
|
||||||
|
- Python 3.6
|
||||||
|
- git
|
||||||
|
- pip
|
||||||
|
- pipenv
|
||||||
|
|
||||||
|
If you're not on Windows, you can optionally install [pyenv](https://github.com/pyenv/pyenv), which will help you run tests for different python versions.
|
||||||
|
|
||||||
|
1. Fork and clone the repository to a directory on your local machine.
|
||||||
|
2. Open a command line in that directory and execute the following commands:
|
||||||
|
```bash
|
||||||
|
pip install pipenv
|
||||||
|
pipenv install --dev
|
||||||
|
```
|
||||||
|
Red, its dependencies, and all required development tools, are now installed to a virtual environment. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
||||||
|
3. Activate the new virtual environment with the command:
|
||||||
|
```bash
|
||||||
|
pipenv shell
|
||||||
|
```
|
||||||
|
From here onwards, we will assume you are executing commands from within this shell. Each time you open a new command line, you should execute this command first.
|
||||||
|
|
||||||
|
Note: If you haven't used `pipenv` before but are comfortable with virtualenvs, just run `pip install pipenv` in the virtualenv you're already using and invoke the command above from the cloned Red repo. It will do the correct thing.
|
||||||
|
|
||||||
|
### 4.2 Testing
|
||||||
|
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||||
|
|
||||||
|
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||||
|
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on both python 3.5 and 3.6 (test environments `py35` and `py36` respectively)
|
||||||
|
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||||
|
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||||
|
|
||||||
|
To run all of these tests, just run the command `tox` in the project directory.
|
||||||
|
|
||||||
|
To run a subset of these tests, use the command `tox -e <env>`, where `<env>` is the test environment you want tox to run. The test environments are noted in the dot points above.
|
||||||
|
|
||||||
|
Your PR will not be merged until all of these tests pass.
|
||||||
|
|
||||||
|
### 4.3 Style
|
||||||
|
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
|
||||||
|
|
||||||
|
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
||||||
|
|
||||||
|
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
|
||||||
|
|
||||||
|
### 4.4 Make
|
||||||
|
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||||
|
1. `make reformat`: Reformat all python files in the project with Black
|
||||||
|
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
|
||||||
|
|
||||||
|
### 4.5 Keeping your dependencies up to date
|
||||||
|
Whenever you pull from upstream (V3/develop on the main repository) and you notice the file `Pipfile.lock` has been changed, it usually means one of the package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `pipenv install --dev` again.
|
||||||
|
|
||||||
|
### 4.6 To contribute changes
|
||||||
|
|
||||||
|
1. Create a new branch on your fork
|
||||||
|
2. Make the changes
|
||||||
3. If you like the changes and think the main Red project could use it:
|
3. If you like the changes and think the main Red project could use it:
|
||||||
* Ensure your code follows (generally) the PEP8 Python style guide
|
* Run tests with `tox` to ensure your code is up to scratch
|
||||||
* Create a Pull Request on GitHub with your changes
|
* Create a Pull Request on GitHub with your changes
|
||||||
|
|
||||||
### How To Report A Bug
|
### 4.7 How To Report A Bug
|
||||||
Please see our **ISSUES.MD** for more information.
|
Please see our **ISSUES.MD** for more information.
|
||||||
|
|
||||||
### How To Suggest A Feature Or Enhancement
|
### 4.8 How To Suggest A Feature Or Enhancement
|
||||||
The goal of Red is to be as useful to as many people as possible, this means that all features must be useful to anyone and any server that uses Red.
|
The goal of Red is to be as useful to as many people as possible, this means that all features must be useful to anyone and any server that uses Red.
|
||||||
|
|
||||||
If you find yourself wanting a feature that Red does not already have, you're probably not alone. There's bound to be a great number of users out there needing the same thing and a lot of the features that Red has today have been added because of the needs of our users. Open an issue on our issues list and describe the feature you would like to see, how you would use it, how it should work, and why it would be useful to the Red community as a whole.
|
If you find yourself wanting a feature that Red does not already have, you're probably not alone. There's bound to be a great number of users out there needing the same thing and a lot of the features that Red has today have been added because of the needs of our users. Open an issue on our issues list and describe the feature you would like to see, how you would use it, how it should work, and why it would be useful to the Red community as a whole.
|
||||||
|
|
||||||
# Code Review Process
|
# 5. Code Review Process
|
||||||
|
|
||||||
We have a core team working tirelessly to implement new features and fix bugs for the Red community. This core team looks at and evaluates new issues and PRs on a daily basis.
|
We have a core team working tirelessly to implement new features and fix bugs for the Red community. This core team looks at and evaluates new issues and PRs on a daily basis.
|
||||||
|
|
||||||
The decisions we make are based on a simple majority of that team or by decree of the project owner.
|
The decisions we make are based on a simple majority of that team or by decree of the project owner.
|
||||||
|
|
||||||
### Issues
|
### 5.1 Issues
|
||||||
Any new issues will be looked at and evaluated for validity of a bug or for the usefulness of a suggested feature. If we have questions about your issue we will get back as soon as we can (usually in a day or two) and will try to make a decision within a week.
|
Any new issues will be looked at and evaluated for validity of a bug or for the usefulness of a suggested feature. If we have questions about your issue we will get back as soon as we can (usually in a day or two) and will try to make a decision within a week.
|
||||||
|
|
||||||
### Pull Requests
|
### 5.2 Pull Requests
|
||||||
Pull requests are evaluated by their quality and how effectively they solve their corresponding issue. The process for reviewing pull requests is as follows:
|
Pull requests are evaluated by their quality and how effectively they solve their corresponding issue. The process for reviewing pull requests is as follows:
|
||||||
|
|
||||||
1. A pull request is submitted
|
1. A pull request is submitted
|
||||||
@@ -66,10 +140,10 @@ Pull requests are evaluated by their quality and how effectively they solve thei
|
|||||||
4. If any feedback is given we expect a response within 1 week or we may decide to close the PR.
|
4. If any feedback is given we expect a response within 1 week or we may decide to close the PR.
|
||||||
5. If your pull request is not vetoed and no core member requests changes then it will be approved and merged into the project.
|
5. If your pull request is not vetoed and no core member requests changes then it will be approved and merged into the project.
|
||||||
|
|
||||||
### Differences between "new features" and "improvements"
|
### 5.3 Differences between "new features" and "improvements"
|
||||||
The difference between a new feature and improvement can be quite fuzzy and the project owner reserves all rights to decide under which category your PR falls.
|
The difference between a new feature and improvement can be quite fuzzy and the project owner reserves all rights to decide under which category your PR falls.
|
||||||
|
|
||||||
At a very basic level a PR is a new feature if it changes the intended way any part of the Red project currently works or if it modifies the user experience (UX) in any significant way. Otherwise, it is likely to be considered an improvement.
|
At a very basic level a PR is a new feature if it changes the intended way any part of the Red project currently works or if it modifies the user experience (UX) in any significant way. Otherwise, it is likely to be considered an improvement.
|
||||||
|
|
||||||
# Community
|
# 6. Community
|
||||||
You can chat with the core team and other community members about issues or pull requests in the #coding channel of the Red support server located [here](https://discord.gg/red).
|
You can chat with the core team and other community members about issues or pull requests in the #coding channel of the Red support server located [here](https://discord.gg/red).
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ python:
|
|||||||
version: 3.6
|
version: 3.6
|
||||||
pip_install: true
|
pip_install: true
|
||||||
extra_requirements:
|
extra_requirements:
|
||||||
- docs
|
|
||||||
- mongo
|
- mongo
|
||||||
32
.travis.yml
32
.travis.yml
@@ -5,21 +5,29 @@ notifications:
|
|||||||
email: false
|
email: false
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 3.5.3
|
- 3.6.5
|
||||||
- 3.6.1
|
env:
|
||||||
|
global:
|
||||||
|
PIPENV_IGNORE_VIRTUALENVS=1
|
||||||
|
matrix:
|
||||||
|
- TOXENV=py
|
||||||
|
- TOXENV=docs
|
||||||
|
- TOXENV=style
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]" >> requirements.txt
|
- pip install --upgrade pip pipenv
|
||||||
- pip install -r requirements.txt
|
- pipenv install --dev
|
||||||
- pip install .[test]
|
|
||||||
script:
|
script:
|
||||||
- python -m compileall ./redbot/cogs
|
- pipenv run tox
|
||||||
- python -m pytest
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
|
|
||||||
|
# These jobs only occur on tag creation for V3/develop if the prior ones succeed
|
||||||
- stage: PyPi Deployment
|
- stage: PyPi Deployment
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
python: 3.5.3
|
python: 3.6.5
|
||||||
env:
|
env:
|
||||||
- DEPLOYING=true
|
- DEPLOYING=true
|
||||||
deploy:
|
deploy:
|
||||||
@@ -31,14 +39,14 @@ jobs:
|
|||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
branch: V3/develop
|
||||||
python: 3.5.3
|
python: 3.6.5
|
||||||
tags: true
|
tags: true
|
||||||
- stage: Crowdin Deployment
|
- stage: Crowdin Deployment
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
python: 3.5.3
|
python: 3.6.5
|
||||||
env:
|
env:
|
||||||
- DEPLOYING=true
|
- DEPLOYING=true
|
||||||
before_deployment:
|
before_deploy:
|
||||||
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||||
- sudo apt-get update -qq
|
- sudo apt-get update -qq
|
||||||
@@ -50,5 +58,5 @@ jobs:
|
|||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
branch: V3/develop
|
||||||
python: 3.5.3
|
python: 3.6.5
|
||||||
tags: true
|
tags: true
|
||||||
|
|||||||
4
Makefile
Normal file
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
reformat:
|
||||||
|
black -l 99 `git ls-files "*.py"`
|
||||||
|
stylecheck:
|
||||||
|
black --check -l 99 `git ls-files "*.py"`
|
||||||
20
Pipfile
Normal file
20
Pipfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true}
|
||||||
|
"e1839a8" = {path = ".", editable = true}
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
tox = "*"
|
||||||
|
pytest = "*"
|
||||||
|
pytest-asyncio = "*"
|
||||||
|
sphinx = ">1.7"
|
||||||
|
sphinxcontrib-asyncio = "*"
|
||||||
|
sphinx-rtd-theme = "*"
|
||||||
|
black = {version = "*", python_version = ">= '3.6'"}
|
||||||
|
|
||||||
|
[pipenv]
|
||||||
|
allow_prereleases = true
|
||||||
467
Pipfile.lock
generated
Normal file
467
Pipfile.lock
generated
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "d340e4a19777736703970e45766d05d67b973db38b87382b6ef8696cb53abb60"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:129d83dd067760cec3cfd4456b5c6d7ac29f2c639d856884568fd539bed5a51f",
|
||||||
|
"sha256:33c62afd115c456b0cf1e890fe6753055effe0f31a28321efd4f787378d6f4ab",
|
||||||
|
"sha256:666756e1d4cf161ed1486b82f65fdd386ac07dd20fb10f025abf4be54be12746",
|
||||||
|
"sha256:9705ded5a0faa25c8f14c6afb7044002d66c9120ed7eadb4aa9ca4aad32bd00c",
|
||||||
|
"sha256:af5bfdd164256118a0a306b3f7046e63207d1f8cba73a67dcc0bd858dcfcd3bc",
|
||||||
|
"sha256:b80f44b99fa3c9b4530fcfa324a99b84843043c35b084e0b653566049974435d",
|
||||||
|
"sha256:c67e105ec74b85c8cb666b6877569dee6f55b9548f982983b9bee80b3d47e6f3",
|
||||||
|
"sha256:d15c6658de5b7783c2538407278fa062b079a46d5f814a133ae0f09bbb2cfbc4",
|
||||||
|
"sha256:d611ebd1ef48498210b65486306e065fde031040a1f3c455ca1b6baa7bf32ad3",
|
||||||
|
"sha256:dcc7e4dcec6b0012537b9f8a0726f8b111188894ab0f924b680d40b13d3298a0",
|
||||||
|
"sha256:de8ef106e130b94ca143fdfc6f27cda1d8ba439462542377738af4d99d9f5dd2",
|
||||||
|
"sha256:eb6f1405b607fff7e44168e3ceb5d3c8a8c5a2d3effe0a27f843b16ec047a6d7",
|
||||||
|
"sha256:f0e2ac69cb709367400008cebccd5d48161dd146096a009a632a132babe5714c"
|
||||||
|
],
|
||||||
|
"version": "==2.2.5"
|
||||||
|
},
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
|
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||||
|
],
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"async-timeout": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00cff4d2dce744607335cba84e9929c3165632da2d27970dbc55802a0c7873d0",
|
||||||
|
"sha256:9093db5b8ddbe4b8f6885d1a6e0ad84ae3155464cbf6877c387605244c285f3c"
|
||||||
|
],
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"colorama": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||||
|
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||||
|
],
|
||||||
|
"version": "==0.3.9"
|
||||||
|
},
|
||||||
|
"discord.py": {
|
||||||
|
"editable": true,
|
||||||
|
"git": "git://github.com/Rapptz/discord.py",
|
||||||
|
"ref": "rewrite"
|
||||||
|
},
|
||||||
|
"distro": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||||
|
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||||
|
],
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"e1839a8": {
|
||||||
|
"editable": true,
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
"funcsigs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
||||||
|
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"fuzzywuzzy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
||||||
|
"sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f"
|
||||||
|
],
|
||||||
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||||
|
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||||
|
],
|
||||||
|
"version": "==2.6"
|
||||||
|
},
|
||||||
|
"jsonrpcserver": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
|
||||||
|
],
|
||||||
|
"version": "==3.5.4"
|
||||||
|
},
|
||||||
|
"jsonschema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
|
||||||
|
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||||
|
"sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
|
||||||
|
"sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
|
||||||
|
"sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
|
||||||
|
"sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
|
||||||
|
"sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
|
||||||
|
"sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
|
||||||
|
"sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
|
||||||
|
"sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
|
||||||
|
"sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
|
||||||
|
"sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
|
||||||
|
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
|
||||||
|
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
|
||||||
|
],
|
||||||
|
"version": "==4.3.1"
|
||||||
|
},
|
||||||
|
"python-levenshtein": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||||
|
],
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
||||||
|
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
||||||
|
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
||||||
|
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
||||||
|
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
||||||
|
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
||||||
|
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
||||||
|
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
||||||
|
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
||||||
|
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
||||||
|
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
||||||
|
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
||||||
|
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
||||||
|
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
||||||
|
],
|
||||||
|
"version": "==3.12"
|
||||||
|
},
|
||||||
|
"raven": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0adae40e004dfe2181d1f2883aa3d4ca1cf16dbe449ae4b445b011c6eb220a90",
|
||||||
|
"sha256:84da75114739191bdf2388f296ffd6177e83567a7fbaf2701e034ad6026e4f3b"
|
||||||
|
],
|
||||||
|
"version": "==6.5.0"
|
||||||
|
},
|
||||||
|
"red-trivia": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:39413b9fb3f9b9362d6de1dcf69a4bf635b0f3518243f7178299b96d26cbb6a7"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||||
|
"sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a",
|
||||||
|
"sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8",
|
||||||
|
"sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345",
|
||||||
|
"sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5",
|
||||||
|
"sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937",
|
||||||
|
"sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5",
|
||||||
|
"sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584",
|
||||||
|
"sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af",
|
||||||
|
"sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27",
|
||||||
|
"sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab",
|
||||||
|
"sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac",
|
||||||
|
"sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa",
|
||||||
|
"sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc",
|
||||||
|
"sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7",
|
||||||
|
"sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4",
|
||||||
|
"sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a",
|
||||||
|
"sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941",
|
||||||
|
"sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2",
|
||||||
|
"sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da"
|
||||||
|
],
|
||||||
|
"version": "==3.4"
|
||||||
|
},
|
||||||
|
"yarl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:605480ee43eead69ec8e8c52cdfefc79cef6379cc0e87d908cf290408c1e49af",
|
||||||
|
"sha256:7fad2530cb4ddf2b74c1e4f6f9f0e28eac482094c6542f98fd71ecf67fb4fded",
|
||||||
|
"sha256:837d866a70f1ea03005914a740bddea89a253afabd6589db981b91738768bd25",
|
||||||
|
"sha256:885e40812ff9fc80e6f28ef04ad6396e3ae583ab504b1a76301fdcec7fc9f30f",
|
||||||
|
"sha256:a5457e075eab1170141774a8c69906c223ea0088eaebd6ef91b04b33527fa905",
|
||||||
|
"sha256:baa0d3f7982fa0c03a55433109c405e79a597141f2e2d6ee7e16c03eabd74886",
|
||||||
|
"sha256:beeefbe0edd47fc8b657bf7bf44791f7a6e5b14f3de1846daf999687cb68c156",
|
||||||
|
"sha256:cf6a3d6fd3e79d3457d520c12d5d18b030d5ca5d0b205ca6481857804d8d944d",
|
||||||
|
"sha256:d07d3dc6849345b7437dc58ea49ad2a1960017386d86288550728ca38e482ddc",
|
||||||
|
"sha256:d81e45bedefccb97e4e8f7d32cfae0af1d9eadd1ae795fc420c8319c3dab2a28",
|
||||||
|
"sha256:e1da2853a92fbc7e2d0248bbfa931cd621121e70ce6dda7c1eeef3516d51b46c",
|
||||||
|
"sha256:f1201de3e93fb1efc3111c8928d9366875edefd65d77c0f6b847fe299e8e1122",
|
||||||
|
"sha256:fe0390a29b5c7e90975feefe863e3d3a851be546bd797b23f338d24a15efa920"
|
||||||
|
],
|
||||||
|
"version": "==0.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
|
||||||
|
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
|
||||||
|
],
|
||||||
|
"version": "==0.7.10"
|
||||||
|
},
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
|
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||||
|
],
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"atomicwrites": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
|
||||||
|
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
|
||||||
|
],
|
||||||
|
"version": "==1.1.5"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
|
||||||
|
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
|
||||||
|
],
|
||||||
|
"version": "==18.1.0"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14",
|
||||||
|
"sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80"
|
||||||
|
],
|
||||||
|
"version": "==2.5.3"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4fec2566f9fbbd4a58de50a168cbe3ab952713530410d227e82e4c65d1fad946",
|
||||||
|
"sha256:5fec0f25486046b9edb97961c946412ced96021247dd1a60ecd9f0567b68b030"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==18.5b0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
||||||
|
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
||||||
|
],
|
||||||
|
"version": "==2018.4.16"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||||
|
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||||
|
],
|
||||||
|
"version": "==6.7"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||||
|
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||||
|
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||||
|
],
|
||||||
|
"version": "==0.14"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||||
|
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||||
|
],
|
||||||
|
"version": "==2.6"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
|
||||||
|
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
|
||||||
|
],
|
||||||
|
"version": "==1.0.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||||
|
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||||
|
],
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||||
|
],
|
||||||
|
"version": "==1.0"
|
||||||
|
},
|
||||||
|
"more-itertools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
|
||||||
|
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
|
||||||
|
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
|
||||||
|
],
|
||||||
|
"version": "==4.2.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
|
||||||
|
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
|
||||||
|
],
|
||||||
|
"version": "==17.1"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
||||||
|
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
||||||
|
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
||||||
|
],
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
||||||
|
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
||||||
|
],
|
||||||
|
"version": "==1.5.3"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||||
|
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||||
|
],
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
|
||||||
|
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
|
||||||
|
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
|
||||||
|
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
|
||||||
|
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
|
||||||
|
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
|
||||||
|
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
|
||||||
|
],
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d",
|
||||||
|
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.6.0"
|
||||||
|
},
|
||||||
|
"pytest-asyncio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd",
|
||||||
|
"sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.8.0"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
||||||
|
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
||||||
|
],
|
||||||
|
"version": "==2018.4"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
|
||||||
|
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
|
||||||
|
],
|
||||||
|
"version": "==2.18.4"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||||
|
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b",
|
||||||
|
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.7.4"
|
||||||
|
},
|
||||||
|
"sphinx-rtd-theme": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33",
|
||||||
|
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-asyncio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-websupport": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9",
|
||||||
|
"sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"tox": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:96efa09710a3daeeb845561ebbe1497641d9cef2ee0aea30db6969058b2bda2f",
|
||||||
|
"sha256:9ee7de958a43806402a38c0d2aa07fa8553f4d2c20a15b140e9f771c2afeade0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
||||||
|
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
||||||
|
],
|
||||||
|
"version": "==1.22"
|
||||||
|
},
|
||||||
|
"virtualenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
|
||||||
|
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
|
||||||
|
],
|
||||||
|
"version": "==16.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
README.rst
128
README.rst
@@ -1,42 +1,112 @@
|
|||||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
.. raw:: html
|
||||||
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
|
||||||
:alt: Documentation Status
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
<h1 align="center">
|
||||||
:target: http://makeapullrequest.com
|
<br>
|
||||||
:alt: PRs Welcome
|
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red Discord Bot"></a>
|
||||||
|
<br>
|
||||||
|
Red Discord Bot
|
||||||
|
<br>
|
||||||
|
</h1>
|
||||||
|
|
||||||
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
|
.. raw:: html
|
||||||
:target: https://crowdin.com/project/red-discordbot
|
|
||||||
:alt: Crowdin
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and fully customizable.</h4>
|
||||||
:target: https://www.patreon.com/Red_Devs
|
|
||||||
:alt: Patreon
|
|
||||||
|
|
||||||
********************
|
.. raw:: html
|
||||||
Red - Discord Bot v3
|
|
||||||
********************
|
|
||||||
|
|
||||||
**This is in beta and very much a work in progress. Regular use is not recommended.
|
<p align="center">
|
||||||
There will not be any effort made to prevent the breaking of current installations.**
|
<a href="https://discord.gg/red">
|
||||||
|
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.patreon.com/Red_Devs">
|
||||||
|
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/Made%20With-Python%203.6-blue.svg?style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
<a href="https://crowdin.com/project/red-discordbot">
|
||||||
|
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
|
||||||
|
<img src="https://img.shields.io/badge/discord-py-blue.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
How to install
|
.. raw:: html
|
||||||
^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Using python3 pip::
|
<p align="center">
|
||||||
|
<a href="#overview">Overview</a> •
|
||||||
|
<a href="#installation">Installation</a> •
|
||||||
|
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
|
||||||
|
<a href="#plugins"></a> •
|
||||||
|
<a href="#join-the-community">Community</a> •
|
||||||
|
<a href="#license">License</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot
|
==========
|
||||||
redbot-setup
|
Overview
|
||||||
redbot <name>
|
==========
|
||||||
|
|
||||||
To install requirements for voice::
|
Red is a fully modular bot – meaning all features and commands can be enabled/disabled to your liking, making it completely customizable.
|
||||||
|
This is also a *self-hosted bot* – meaning you will need 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!
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot[voice]
|
`Installation <#installation>`_ is easy, and you do **NOT** need to know anything about coding! Aside from installation and updating, every part of the bot can be controlled from within Discord.
|
||||||
|
|
||||||
To install all requirements for docs and tests::
|
**The default set of modules includes and is not limited to:**
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot[test,docs]
|
- Moderation features (kick/ban/softban/hackban, mod-log, filter, chat cleanup)
|
||||||
|
- Trivia (lists are included and can be easily added)
|
||||||
|
- Music features (YouTube, SoundCloud, local files, playlists, queues)
|
||||||
|
- Stream alerts (Twitch, Youtube, Mixer, Hitbox, Picarto)
|
||||||
|
- Slot machine
|
||||||
|
- Custom commands
|
||||||
|
- Imgur/gif search
|
||||||
|
|
||||||
For the latest git build, replace ``Red-DiscordBot`` in the above commands with
|
|
||||||
``git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop``.
|
**Additionally, other plugins (cogs) can be easily found and added from our growing community of cog repositories.**
|
||||||
|
|
||||||
|
- Cleverbot integration (talk to Red and she talks back)
|
||||||
|
- Ban sync
|
||||||
|
- Welcome messages
|
||||||
|
- Casino
|
||||||
|
- Reaction roles
|
||||||
|
- Slow Mode
|
||||||
|
- Anilist
|
||||||
|
- And much, much more!
|
||||||
|
|
||||||
|
Feel free to take a `peek <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_!
|
||||||
|
|
||||||
|
==============
|
||||||
|
Installation
|
||||||
|
==============
|
||||||
|
|
||||||
|
**The following platforms are officially supported:**
|
||||||
|
|
||||||
|
- `Windows <https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html>`_
|
||||||
|
- `MacOS <https://red-discordbot.readthedocs.io/en/v3-develop/install_mac.html>`_
|
||||||
|
- `Ubuntu <https://red-discordbot.readthedocs.io/en/v3-develop/install_ubuntu.html>`_
|
||||||
|
- `Debian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_debian.html>`_
|
||||||
|
- `CentOS 7 <https://red-discordbot.readthedocs.io/en/v3-develop/install_centos.html>`_
|
||||||
|
- `Arch Linux <https://red-discordbot.readthedocs.io/en/v3-develop/install_arch.html>`_
|
||||||
|
- `Raspbian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_raspbian.html>`_
|
||||||
|
|
||||||
|
Already using **Red** V2? Take a look at the `Data Converter <https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html>`_ to import your data to V3.
|
||||||
|
|
||||||
|
If `after reading the guides <https://red-discordbot.readthedocs.io/en/v3-develop/>`_ you are still experiencing issues, feel free to join the `Official Server <https://discord.gg/red>`_ and ask in the **#support** channel for help.
|
||||||
|
|
||||||
|
=====================
|
||||||
|
Join the community!
|
||||||
|
=====================
|
||||||
|
|
||||||
|
**Red** is in continuous development, and it’s supported by an active community which produces new content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t `find <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_ what you’re looking for, consult our `guide <https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html>`_ on building your own cogs!
|
||||||
|
|
||||||
|
Join us on our `Official Discord Server <https://discord.gg/red>`_!
|
||||||
|
|
||||||
|
=========
|
||||||
|
License
|
||||||
|
=========
|
||||||
|
|
||||||
|
Released under the `GNU GPL v3 <#License>`_.
|
||||||
|
|
||||||
|
Red is named after the main character of "Transistor", a videogame by `Super Giant Games <https://www.supergiantgames.com/games/transistor/>`_
|
||||||
|
|
||||||
|
Artwork created by `Sinlaire <https://sinlaire.deviantart.com/>`_ on Deviant Art for the Red Bot Project.
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ help:
|
|||||||
|
|
||||||
.PHONY: help Makefile
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
init:
|
||||||
|
cd .. && pipenv lock -r --dev > docs/requirements.txt && echo 'git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0' >> docs/requirements.txt
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
%: Makefile
|
%: Makefile
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
.. systemd service guide
|
.. systemd service guide
|
||||||
|
|
||||||
==========================
|
==============================================
|
||||||
Setting up auto-restart using systemd on Linux
|
Setting up auto-restart using systemd on Linux
|
||||||
==========================
|
==============================================
|
||||||
|
|
||||||
---------------------------
|
-------------------------
|
||||||
Creating the service file
|
Creating the service file
|
||||||
---------------------------
|
-------------------------
|
||||||
|
|
||||||
Create the new service file:
|
Create the new service file:
|
||||||
|
|
||||||
@@ -27,15 +27,16 @@ Paste the following and replace all instances of :code:`username` with the usern
|
|||||||
Type=idle
|
Type=idle
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=15
|
RestartSec=15
|
||||||
|
RestartPreventExitStatus=0
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
Save and exit :code:`ctrl + O; enter; ctrl + x`
|
Save and exit :code:`ctrl + O; enter; ctrl + x`
|
||||||
|
|
||||||
---------------------------
|
---------------------------------
|
||||||
Starting and enabling the service
|
Starting and enabling the service
|
||||||
---------------------------
|
---------------------------------
|
||||||
|
|
||||||
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
||||||
|
|
||||||
|
|||||||
75
docs/cog_permissions.rst
Normal file
75
docs/cog_permissions.rst
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
.. Permissions Cog Reference
|
||||||
|
|
||||||
|
=========================
|
||||||
|
Permissions Cog Reference
|
||||||
|
=========================
|
||||||
|
|
||||||
|
------------
|
||||||
|
How it works
|
||||||
|
------------
|
||||||
|
|
||||||
|
When loaded, the permissions cog will allow you
|
||||||
|
to define extra custom rules for who can use a command
|
||||||
|
|
||||||
|
If no applicable rules are found, the command will behave as if
|
||||||
|
the cog was not loaded.
|
||||||
|
|
||||||
|
-------------
|
||||||
|
Rule priority
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Rules set will be checked in the following order
|
||||||
|
|
||||||
|
|
||||||
|
1. Owner level command specific settings
|
||||||
|
2. Owner level cog specific settings
|
||||||
|
3. Server level command specific settings
|
||||||
|
4. Server level cog specific settings
|
||||||
|
|
||||||
|
For each of those, settings have varying priorities (listed below, highest to lowest priority)
|
||||||
|
|
||||||
|
1. User whitelist
|
||||||
|
2. User blacklist
|
||||||
|
3. Voice Channel whitelist
|
||||||
|
4. Voice Channel blacklist
|
||||||
|
5. Text Channel whitelist
|
||||||
|
6. Text Channel blacklist
|
||||||
|
7. Role settings (see below)
|
||||||
|
8. Server whitelist
|
||||||
|
9. Server blacklist
|
||||||
|
|
||||||
|
For the role whitelist and blacklist settings,
|
||||||
|
roles will be checked individually in order from highest to lowest role the user has
|
||||||
|
Each role will be checked for whitelist, then blacklist. The first role with a setting
|
||||||
|
found will be the one used.
|
||||||
|
|
||||||
|
-------------------------
|
||||||
|
Setting Rules from a file
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The permissions cog can set rules from a yaml file:
|
||||||
|
All entries are based on ID.
|
||||||
|
An example of the expected format is shown below.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
cogs:
|
||||||
|
Admin:
|
||||||
|
allow:
|
||||||
|
- 78631113035100160
|
||||||
|
deny:
|
||||||
|
- 96733288462286848
|
||||||
|
Audio:
|
||||||
|
allow:
|
||||||
|
- 133049272517001216
|
||||||
|
default: deny
|
||||||
|
commands:
|
||||||
|
cleanup bot:
|
||||||
|
allow:
|
||||||
|
- 78631113035100160
|
||||||
|
default: deny
|
||||||
|
ping:
|
||||||
|
deny:
|
||||||
|
- 96733288462286848
|
||||||
|
default: allow
|
||||||
|
|
||||||
91
docs/conf.py
91
docs/conf.py
@@ -19,9 +19,10 @@
|
|||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
|
||||||
|
|
||||||
os.environ['BUILDING_DOCS'] = "1"
|
sys.path.insert(0, os.path.abspath(".."))
|
||||||
|
|
||||||
|
os.environ["BUILDING_DOCS"] = "1"
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
@@ -34,35 +35,36 @@ os.environ['BUILDING_DOCS'] = "1"
|
|||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.intersphinx',
|
"sphinx.ext.intersphinx",
|
||||||
'sphinx.ext.viewcode',
|
"sphinx.ext.viewcode",
|
||||||
'sphinx.ext.napoleon',
|
"sphinx.ext.napoleon",
|
||||||
'sphinxcontrib.asyncio'
|
"sphinxcontrib.asyncio",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
# The suffix(es) of source filenames.
|
||||||
# You can specify multiple suffix as a list of string:
|
# You can specify multiple suffix as a list of string:
|
||||||
#
|
#
|
||||||
# source_suffix = ['.rst', '.md']
|
# source_suffix = ['.rst', '.md']
|
||||||
source_suffix = '.rst'
|
source_suffix = ".rst"
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Red - Discord Bot'
|
project = "Red - Discord Bot"
|
||||||
copyright = '2018, Cog Creators'
|
copyright = "2018, Cog Creators"
|
||||||
author = 'Cog Creators'
|
author = "Cog Creators"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
from redbot.core import __version__
|
from redbot.core import __version__
|
||||||
|
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = __version__
|
version = __version__
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
@@ -78,10 +80,10 @@ language = None
|
|||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This patterns also effect to html_static_path and html_extra_path
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
todo_include_todos = False
|
todo_include_todos = False
|
||||||
@@ -95,7 +97,7 @@ default_role = "any"
|
|||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
@@ -105,16 +107,16 @@ html_theme = 'sphinx_rtd_theme'
|
|||||||
|
|
||||||
html_context = {
|
html_context = {
|
||||||
# Enable the "Edit in GitHub link within the header of each page.
|
# Enable the "Edit in GitHub link within the header of each page.
|
||||||
'display_github': True,
|
"display_github": True,
|
||||||
'github_user': 'Cog-Creators',
|
"github_user": "Cog-Creators",
|
||||||
'github_repo': 'Red-DiscordBot',
|
"github_repo": "Red-DiscordBot",
|
||||||
'github_version': 'V3/develop/docs/'
|
"github_version": "V3/develop/docs/",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
# html_static_path = ['_static']
|
||||||
|
|
||||||
# Custom sidebar templates, must be a dictionary that maps document names
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
# to template names.
|
# to template names.
|
||||||
@@ -122,12 +124,12 @@ html_static_path = ['_static']
|
|||||||
# This is required for the alabaster theme
|
# This is required for the alabaster theme
|
||||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||||
html_sidebars = {
|
html_sidebars = {
|
||||||
'**': [
|
"**": [
|
||||||
'about.html',
|
"about.html",
|
||||||
'navigation.html',
|
"navigation.html",
|
||||||
'relations.html', # needs 'show_related': True theme option to display
|
"relations.html", # needs 'show_related': True theme option to display
|
||||||
'searchbox.html',
|
"searchbox.html",
|
||||||
'donate.html',
|
"donate.html",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +137,7 @@ html_sidebars = {
|
|||||||
# -- Options for HTMLHelp output ------------------------------------------
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'Red-DiscordBotdoc'
|
htmlhelp_basename = "Red-DiscordBotdoc"
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
@@ -144,15 +146,12 @@ latex_elements = {
|
|||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
#
|
#
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
#
|
#
|
||||||
# 'pointsize': '10pt',
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
# Additional stuff for the LaTeX preamble.
|
||||||
#
|
#
|
||||||
# 'preamble': '',
|
# 'preamble': '',
|
||||||
|
|
||||||
# Latex figure (float) alignment
|
# Latex figure (float) alignment
|
||||||
#
|
#
|
||||||
# 'figure_align': 'htbp',
|
# 'figure_align': 'htbp',
|
||||||
@@ -162,8 +161,7 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'Red-DiscordBot.tex', 'Red - Discord Bot Documentation',
|
(master_doc, "Red-DiscordBot.tex", "Red - Discord Bot Documentation", "Cog Creators", "manual")
|
||||||
'Cog Creators', 'manual'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -171,10 +169,7 @@ latex_documents = [
|
|||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [(master_doc, "red-discordbot", "Red - Discord Bot Documentation", [author], 1)]
|
||||||
(master_doc, 'red-discordbot', 'Red - Discord Bot Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
@@ -183,15 +178,21 @@ man_pages = [
|
|||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'Red-DiscordBot', 'Red - Discord Bot Documentation',
|
(
|
||||||
author, 'Red-DiscordBot', 'One line description of project.',
|
master_doc,
|
||||||
'Miscellaneous'),
|
"Red-DiscordBot",
|
||||||
|
"Red - Discord Bot Documentation",
|
||||||
|
author,
|
||||||
|
"Red-DiscordBot",
|
||||||
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'python': ('https://docs.python.org/3.5', None),
|
intersphinx_mapping = {
|
||||||
'dpy': ('https://discordpy.readthedocs.io/en/rewrite/', None),
|
"python": ("https://docs.python.org/3.6", None),
|
||||||
'motor': ('https://motor.readthedocs.io/en/stable/', None)}
|
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
||||||
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ RedBase
|
|||||||
|
|
||||||
.. autoclass:: RedBase
|
.. autoclass:: RedBase
|
||||||
:members:
|
:members:
|
||||||
|
:exclude-members: get_context
|
||||||
|
|
||||||
Red
|
Red
|
||||||
^^^
|
^^^
|
||||||
|
|||||||
@@ -6,22 +6,31 @@ Downloader Framework
|
|||||||
Info.json
|
Info.json
|
||||||
*********
|
*********
|
||||||
|
|
||||||
The info.json file may exist inside every package folder in the repo,
|
The optional info.json file may exist inside every package folder in the repo,
|
||||||
it is optional however. This string describes the valid keys within
|
as well as in the root of the repo. The following sections describe the valid
|
||||||
an info file (and maybe how the Downloader cog uses them).
|
keys within an info file (and maybe how the Downloader cog uses them).
|
||||||
|
|
||||||
KEYS (case sensitive):
|
Keys common to both repo and cog info.json (case sensitive)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
- ``author`` (list of strings) - list of names of authors of the cog
|
- ``author`` (list of strings) - list of names of authors of the cog or repo.
|
||||||
|
|
||||||
|
- ``description`` (string) - A long description of the cog or repo. For cogs, this
|
||||||
|
is displayed when a user executes ``!cog info``.
|
||||||
|
|
||||||
|
- ``install_msg`` (string) - The message that gets displayed when a cog
|
||||||
|
is installed or a repo is added
|
||||||
|
|
||||||
|
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||||
|
is displayed when a user executes ``!cog list``
|
||||||
|
|
||||||
|
Keys specific to the cog info.json (case sensitive)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
||||||
|
|
||||||
- ``description`` (string) - A long description of the cog that appears when a user executes ```!cog info``.
|
|
||||||
|
|
||||||
- ``hidden`` (bool) - Determines if a cog is available for install.
|
- ``hidden`` (bool) - Determines if a cog is available for install.
|
||||||
|
|
||||||
- ``install_msg`` (string) - The message that gets displayed when a cog is installed
|
|
||||||
|
|
||||||
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
|
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
|
||||||
Downloader will not deal with this functionality but it may be useful for other cogs.
|
Downloader will not deal with this functionality but it may be useful for other cogs.
|
||||||
|
|
||||||
@@ -29,9 +38,6 @@ KEYS (case sensitive):
|
|||||||
passed to pip on cog install. ``SHARED_LIBRARIES`` do NOT go in this
|
passed to pip on cog install. ``SHARED_LIBRARIES`` do NOT go in this
|
||||||
list.
|
list.
|
||||||
|
|
||||||
- ``short`` (string) - A short description of the cog that appears when
|
|
||||||
a user executes `!cog list`
|
|
||||||
|
|
||||||
- ``tags`` (list of strings) - A list of strings that are related to the
|
- ``tags`` (list of strings) - A list of strings that are related to the
|
||||||
functionality of the cog. Used to aid in searching.
|
functionality of the cog. Used to aid in searching.
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,36 @@
|
|||||||
RPC
|
RPC
|
||||||
===
|
===
|
||||||
|
|
||||||
.. automodule:: redbot.core.rpc
|
.. currentmodule:: redbot.core.rpc
|
||||||
|
|
||||||
|
V3 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.
|
||||||
|
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
||||||
|
|
||||||
|
To begin, register all methods using individual calls to the :func:`Methods.add` method.
|
||||||
|
|
||||||
|
********
|
||||||
|
Examples
|
||||||
|
********
|
||||||
|
|
||||||
|
Coming soon to a docs page near you!
|
||||||
|
|
||||||
|
*************
|
||||||
|
API Reference
|
||||||
|
*************
|
||||||
|
|
||||||
|
.. py:attribute:: redbot.core.rpc.methods
|
||||||
|
|
||||||
|
An instance of the :class:`Methods` class.
|
||||||
|
All attempts to register new RPC methods **MUST** use this object.
|
||||||
|
You should never create a new instance of the :class:`Methods` class!
|
||||||
|
|
||||||
|
RPC
|
||||||
|
^^^
|
||||||
|
.. autoclass:: redbot.core.rpc.RPC
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Methods
|
||||||
|
^^^^^^^
|
||||||
|
.. autoclass:: redbot.core.rpc.Methods
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
|
|
||||||
install_windows
|
install_windows
|
||||||
install_mac
|
install_mac
|
||||||
install_ubuntu
|
install_ubuntu_xenial
|
||||||
|
install_ubuntu_bionic
|
||||||
install_debian
|
install_debian
|
||||||
install_centos
|
install_centos
|
||||||
install_arch
|
install_arch
|
||||||
@@ -25,6 +26,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
:caption: Cog Reference:
|
:caption: Cog Reference:
|
||||||
|
|
||||||
cog_downloader
|
cog_downloader
|
||||||
|
cog_permissions
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ Installing pre-requirements
|
|||||||
|
|
||||||
yum -y groupinstall development
|
yum -y groupinstall development
|
||||||
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
||||||
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
||||||
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
|
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
Installing Red
|
Installing Red
|
||||||
|
|||||||
@@ -12,9 +12,24 @@ Installing pre-requirements
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
echo "deb http://httpredir.debian.org/debian stretch-backports main contrib non-free" >> /etc/apt/sources.list
|
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||||
apt-get update
|
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||||
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
|
||||||
|
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||||
|
|
||||||
|
Then run the following command:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||||
|
|
||||||
|
This may take a long time to complete.
|
||||||
|
|
||||||
|
After that is finished, run:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pyenv global 3.6.5
|
||||||
|
|
||||||
------------------
|
------------------
|
||||||
Installing the bot
|
Installing the bot
|
||||||
|
|||||||
@@ -12,8 +12,24 @@ Installing pre-requirements
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||||
|
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||||
|
|
||||||
|
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||||
|
|
||||||
|
Then run the following command:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||||
|
|
||||||
|
This may take a long time to complete.
|
||||||
|
|
||||||
|
After that is finished, run:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pyenv global 3.6.5
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
Installing Red
|
Installing Red
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. ubuntu install guide
|
.. ubuntu bionic install guide
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
Installing Red on Ubuntu 16.04
|
Installing Red on Ubuntu 18.04
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||||
@@ -12,7 +12,7 @@ Installing the pre-requirements
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||||
|
|
||||||
|
|
||||||
------------------
|
------------------
|
||||||
59
docs/install_ubuntu_xenial.rst
Normal file
59
docs/install_ubuntu_xenial.rst
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.. ubuntu xenial install guide
|
||||||
|
|
||||||
|
==============================
|
||||||
|
Installing Red on Ubuntu 16.04
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
Installing the pre-requirements
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
sudo apt install software-properties-common
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
|
||||||
|
wget https://bootstrap.pypa.io/get-pip.py
|
||||||
|
sudo python3.6 get-pip.py
|
||||||
|
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Installing the bot
|
||||||
|
------------------
|
||||||
|
|
||||||
|
To install without audio:
|
||||||
|
|
||||||
|
:code:`pip3.6 install -U --process-dependency-links red-discordbot --user`
|
||||||
|
|
||||||
|
To install with audio:
|
||||||
|
|
||||||
|
:code:`pip3.6 install -U --process-dependency-links red-discordbot[voice] --user`
|
||||||
|
|
||||||
|
To install the development version (without audio):
|
||||||
|
|
||||||
|
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
|
||||||
|
|
||||||
|
To install the development version (with audio):
|
||||||
|
|
||||||
|
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
Setting up your instance
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
|
||||||
|
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
|
||||||
|
for confirmation of that selection. Next, it will ask you to choose your storage backend
|
||||||
|
(the default here is JSON). It will then ask for a name for your instance. This can be
|
||||||
|
anything as long as it does not contain spaces; however, keep in mind that this is the
|
||||||
|
name you will use to run your bot, and so it should be something you can remember.
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Running Red
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
|
||||||
|
your token and a prefix.
|
||||||
@@ -8,7 +8,7 @@ Installing Red on Windows
|
|||||||
Needed Software
|
Needed Software
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5
|
* `Python <https://python.org/downloads/>`_ - Red needs Python 3.6
|
||||||
|
|
||||||
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
||||||
you may run into issues when trying to run Red
|
you may run into issues when trying to run Red
|
||||||
|
|||||||
@@ -1,4 +1,29 @@
|
|||||||
sphinx==1.6.5
|
-i https://pypi.org/simple
|
||||||
sphinxcontrib-asyncio
|
alabaster==0.7.10
|
||||||
sphinx_rtd_theme
|
attrs==18.1.0
|
||||||
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]
|
babel==2.5.3
|
||||||
|
certifi==2018.4.16
|
||||||
|
chardet==3.0.4
|
||||||
|
docutils==0.14
|
||||||
|
idna==2.6
|
||||||
|
imagesize==1.0.0
|
||||||
|
jinja2==2.10
|
||||||
|
markupsafe==1.0
|
||||||
|
more-itertools==4.1.0
|
||||||
|
packaging==17.1
|
||||||
|
pluggy==0.6.0
|
||||||
|
py==1.5.3
|
||||||
|
pygments==2.2.0
|
||||||
|
pyparsing==2.2.0
|
||||||
|
pytest-asyncio==0.8.0
|
||||||
|
pytest==3.5.1
|
||||||
|
pytz==2018.4
|
||||||
|
requests==2.18.4
|
||||||
|
six==1.11.0
|
||||||
|
snowballstemmer==1.2.1
|
||||||
|
sphinx-rtd-theme==0.3.1
|
||||||
|
sphinx==1.7.4
|
||||||
|
sphinxcontrib-asyncio==0.2.0
|
||||||
|
sphinxcontrib-websupport==1.0.1
|
||||||
|
urllib3==1.22
|
||||||
|
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ def main():
|
|||||||
if "locales" in os.listdir(os.path.join("redbot/cogs", d)):
|
if "locales" in os.listdir(os.path.join("redbot/cogs", d)):
|
||||||
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
||||||
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
||||||
print("Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(d))
|
print(
|
||||||
|
"Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(
|
||||||
|
d
|
||||||
|
)
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
print("Running 'regen_messages.py' for {}".format(d))
|
print("Running 'regen_messages.py' for {}".format(d))
|
||||||
|
|||||||
30
make.bat
Normal file
30
make.bat
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
if "%1"=="" goto help
|
||||||
|
|
||||||
|
REM This allows us to expand variables at execution
|
||||||
|
setlocal ENABLEDELAYEDEXPANSION
|
||||||
|
|
||||||
|
REM This will set PYFILES as a list of tracked .py files
|
||||||
|
set PYFILES=
|
||||||
|
for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
||||||
|
set PYFILES=!PYFILES! %%A
|
||||||
|
)
|
||||||
|
|
||||||
|
goto %1
|
||||||
|
|
||||||
|
:reformat
|
||||||
|
black -l 99 !PYFILES!
|
||||||
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
|
:stylecheck
|
||||||
|
black -l 99 --check !PYFILES!
|
||||||
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
|
:help
|
||||||
|
echo Usage:
|
||||||
|
echo make ^<command^>
|
||||||
|
echo.
|
||||||
|
echo Commands:
|
||||||
|
echo reformat Reformat all .py files being tracked by git.
|
||||||
|
echo stylecheck Check which tracked .py files need reformatting.
|
||||||
@@ -1,11 +1,23 @@
|
|||||||
import sys
|
import sys
|
||||||
import typing
|
|
||||||
import discord
|
import discord
|
||||||
|
from colorama import init, Back
|
||||||
|
|
||||||
|
init()
|
||||||
# Let's do all the dumb version checking in one place.
|
# Let's do all the dumb version checking in one place.
|
||||||
|
|
||||||
if discord.version_info.major < 1:
|
if discord.version_info.major < 1:
|
||||||
print("You are not running the rewritten version of discord.py.\n\n"
|
print(
|
||||||
|
"You are not running the rewritten version of discord.py.\n\n"
|
||||||
"In order to use Red v3 you MUST be running d.py version"
|
"In order to use Red v3 you MUST be running d.py version"
|
||||||
" >= 1.0.0.")
|
" >= 1.0.0."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
if sys.version_info < (3, 6, 0):
|
||||||
|
print(Back.RED + "[DEPRECATION WARNING]")
|
||||||
|
print(
|
||||||
|
Back.RED + "You are currently running Python 3.5."
|
||||||
|
" Support for Python 3.5 will end with the release of beta 16."
|
||||||
|
" Please update your environment to Python 3.6 as soon as possible to avoid"
|
||||||
|
" any interruptions after the beta 16 release."
|
||||||
|
)
|
||||||
|
|||||||
@@ -40,24 +40,25 @@ def init_loggers(cli_flags):
|
|||||||
logger = logging.getLogger("red")
|
logger = logging.getLogger("red")
|
||||||
|
|
||||||
red_format = logging.Formatter(
|
red_format = logging.Formatter(
|
||||||
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: '
|
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: " "%(message)s",
|
||||||
'%(message)s',
|
datefmt="[%d/%m/%Y %H:%M]",
|
||||||
datefmt="[%d/%m/%Y %H:%M]")
|
)
|
||||||
|
|
||||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||||
stdout_handler.setFormatter(red_format)
|
stdout_handler.setFormatter(red_format)
|
||||||
|
|
||||||
if cli_flags.debug:
|
if cli_flags.debug:
|
||||||
os.environ['PYTHONASYNCIODEBUG'] = '1'
|
os.environ["PYTHONASYNCIODEBUG"] = "1"
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
from redbot.core.data_manager import core_data_path
|
from redbot.core.data_manager import core_data_path
|
||||||
logfile_path = core_data_path() / 'red.log'
|
|
||||||
|
logfile_path = core_data_path() / "red.log"
|
||||||
fhandler = logging.handlers.RotatingFileHandler(
|
fhandler = logging.handlers.RotatingFileHandler(
|
||||||
filename=str(logfile_path), encoding='utf-8', mode='a',
|
filename=str(logfile_path), encoding="utf-8", mode="a", maxBytes=10 ** 7, backupCount=5
|
||||||
maxBytes=10**7, backupCount=5)
|
)
|
||||||
fhandler.setFormatter(red_format)
|
fhandler.setFormatter(red_format)
|
||||||
|
|
||||||
logger.addHandler(fhandler)
|
logger.addHandler(fhandler)
|
||||||
@@ -76,15 +77,17 @@ async def _get_prefix_and_token(red, indict):
|
|||||||
:param indict:
|
:param indict:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
indict['token'] = await red.db.token()
|
indict["token"] = await red.db.token()
|
||||||
indict['prefix'] = await red.db.prefix()
|
indict["prefix"] = await red.db.prefix()
|
||||||
indict['enable_sentry'] = await red.db.enable_sentry()
|
indict["enable_sentry"] = await red.db.enable_sentry()
|
||||||
|
|
||||||
|
|
||||||
def list_instances():
|
def list_instances():
|
||||||
if not config_file.exists():
|
if not config_file.exists():
|
||||||
print("No instances have been configured! Configure one "
|
print(
|
||||||
"using `redbot-setup` before trying to run the bot!")
|
"No instances have been configured! Configure one "
|
||||||
|
"using `redbot-setup` before trying to run the bot!"
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
data = JsonIO(config_file)._load_json()
|
data = JsonIO(config_file)._load_json()
|
||||||
@@ -118,29 +121,34 @@ def main():
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
tmp_data = {}
|
tmp_data = {}
|
||||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||||
token = os.environ.get("RED_TOKEN", tmp_data['token'])
|
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||||
prefix = cli_flags.prefix or tmp_data['prefix']
|
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||||
if token is None or not prefix:
|
if token is None or not prefix:
|
||||||
if cli_flags.no_prompt is False:
|
if cli_flags.no_prompt is False:
|
||||||
new_token = interactive_config(red, token_set=bool(token),
|
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
|
||||||
prefix_set=bool(prefix))
|
|
||||||
if new_token:
|
if new_token:
|
||||||
token = new_token
|
token = new_token
|
||||||
else:
|
else:
|
||||||
log.critical("Token and prefix must be set in order to login.")
|
log.critical("Token and prefix must be set in order to login.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||||
if tmp_data['enable_sentry']:
|
|
||||||
|
if cli_flags.dry_run:
|
||||||
|
loop.run_until_complete(red.http.close())
|
||||||
|
sys.exit(0)
|
||||||
|
if tmp_data["enable_sentry"]:
|
||||||
red.enable_sentry()
|
red.enable_sentry()
|
||||||
cleanup_tasks = True
|
cleanup_tasks = True
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
cleanup_tasks = False # No login happened, no need for this
|
cleanup_tasks = False # No login happened, no need for this
|
||||||
log.critical("This token doesn't seem to be valid. If it belongs to "
|
log.critical(
|
||||||
|
"This token doesn't seem to be valid. If it belongs to "
|
||||||
"a user account, remember that the --not-bot flag "
|
"a user account, remember that the --not-bot flag "
|
||||||
"must be used. For self-bot functionalities instead, "
|
"must be used. For self-bot functionalities instead, "
|
||||||
"--self-bot")
|
"--self-bot"
|
||||||
|
)
|
||||||
db_token = red.db.token()
|
db_token = red.db.token()
|
||||||
if db_token and not cli_flags.no_prompt:
|
if db_token and not cli_flags.no_prompt:
|
||||||
print("\nDo you want to reset the token? (y/n)")
|
print("\nDo you want to reset the token? (y/n)")
|
||||||
@@ -156,15 +164,13 @@ def main():
|
|||||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||||
loop.run_until_complete(red.logout())
|
loop.run_until_complete(red.logout())
|
||||||
finally:
|
finally:
|
||||||
rpc.clean_up()
|
|
||||||
if cleanup_tasks:
|
if cleanup_tasks:
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
gathered = asyncio.gather(
|
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||||
*pending, loop=red.loop, return_exceptions=True)
|
|
||||||
gathered.cancel()
|
gathered.cancel()
|
||||||
|
|
||||||
sys.exit(red._shutdown_mode.value)
|
sys.exit(red._shutdown_mode.value)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ GENERIC_FORBIDDEN = (
|
|||||||
|
|
||||||
HIERARCHY_ISSUE = (
|
HIERARCHY_ISSUE = (
|
||||||
"I tried to add {role.name} to {member.display_name} but that role"
|
"I tried to add {role.name} to {member.display_name} but that role"
|
||||||
" is higher than my highest role in the Discord heirarchy so I was"
|
" is higher than my highest role in the Discord hierarchy so I was"
|
||||||
" unable to successfully add it. Please give me a higher role and "
|
" unable to successfully add it. Please give me a higher role and "
|
||||||
"try again."
|
"try again."
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_HIERARCHY_ISSUE = (
|
USER_HIERARCHY_ISSUE = (
|
||||||
"I tried to add {role.name} to {member.display_name} but that role"
|
"I tried to add {role.name} to {member.display_name} but that role"
|
||||||
" is higher than your highest role in the Discord heirarchy so I was"
|
" is higher than your highest role in the Discord hierarchy so I was"
|
||||||
" unable to successfully add it. Please get a higher role and "
|
" unable to successfully add it. Please get a higher role and "
|
||||||
"try again."
|
"try again."
|
||||||
)
|
)
|
||||||
@@ -40,18 +40,16 @@ RUNNING_ANNOUNCEMENT = (
|
|||||||
|
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
def __init__(self, config=Config):
|
|
||||||
self.conf = config.get_conf(self, 8237492837454039,
|
|
||||||
force_registration=True)
|
|
||||||
|
|
||||||
self.conf.register_global(
|
def __init__(self, config=Config):
|
||||||
serverlocked=False
|
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||||
)
|
|
||||||
|
self.conf.register_global(serverlocked=False)
|
||||||
|
|
||||||
self.conf.register_guild(
|
self.conf.register_guild(
|
||||||
announce_ignore=False,
|
announce_ignore=False,
|
||||||
announce_channel=None, # Integer ID
|
announce_channel=None, # Integer ID
|
||||||
selfroles=[] # List of integer ID's
|
selfroles=[], # List of integer ID's
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__current_announcer = None
|
self.__current_announcer = None
|
||||||
@@ -63,8 +61,7 @@ class Admin:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def complain(ctx: commands.Context, message: str,
|
async def complain(ctx: commands.Context, message: str, **kwargs):
|
||||||
**kwargs):
|
|
||||||
await ctx.send(message.format(**kwargs))
|
await ctx.send(message.format(**kwargs))
|
||||||
|
|
||||||
def is_announcing(self) -> bool:
|
def is_announcing(self) -> bool:
|
||||||
@@ -78,8 +75,7 @@ class Admin:
|
|||||||
return self.__current_announcer.active or False
|
return self.__current_announcer.active or False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pass_heirarchy_check(ctx: commands.Context,
|
def pass_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
|
||||||
role: discord.Role) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Determines if the bot has a higher role than the given one.
|
Determines if the bot has a higher role than the given one.
|
||||||
:param ctx:
|
:param ctx:
|
||||||
@@ -89,8 +85,7 @@ class Admin:
|
|||||||
return ctx.guild.me.top_role > role
|
return ctx.guild.me.top_role > role
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pass_user_heirarchy_check(ctx: commands.Context,
|
def pass_user_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
|
||||||
role: discord.Role) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Determines if a user is allowed to add/remove/edit the given role.
|
Determines if a user is allowed to add/remove/edit the given role.
|
||||||
:param ctx:
|
:param ctx:
|
||||||
@@ -99,50 +94,47 @@ class Admin:
|
|||||||
"""
|
"""
|
||||||
return ctx.author.top_role > role
|
return ctx.author.top_role > role
|
||||||
|
|
||||||
async def _addrole(self, ctx: commands.Context, member: discord.Member,
|
async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||||
role: discord.Role):
|
|
||||||
try:
|
try:
|
||||||
await member.add_roles(role)
|
await member.add_roles(role)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
if not self.pass_heirarchy_check(ctx, role):
|
if not self.pass_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
|
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||||
member=member)
|
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||||
else:
|
else:
|
||||||
await ctx.send("I successfully added {role.name} to"
|
await ctx.send(
|
||||||
" {member.display_name}".format(
|
"I successfully added {role.name} to"
|
||||||
role=role, member=member
|
" {member.display_name}".format(role=role, member=member)
|
||||||
))
|
)
|
||||||
|
|
||||||
async def _removerole(self, ctx: commands.Context, member: discord.Member,
|
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||||
role: discord.Role):
|
|
||||||
try:
|
try:
|
||||||
await member.remove_roles(role)
|
await member.remove_roles(role)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
if not self.pass_heirarchy_check(ctx, role):
|
if not self.pass_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
|
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||||
member=member)
|
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||||
else:
|
else:
|
||||||
await ctx.send("I successfully removed {role.name} from"
|
await ctx.send(
|
||||||
" {member.display_name}".format(
|
"I successfully removed {role.name} from"
|
||||||
role=role, member=member
|
" {member.display_name}".format(role=role, member=member)
|
||||||
))
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def addrole(self, ctx: commands.Context, rolename: discord.Role, *,
|
async def addrole(
|
||||||
user: MemberDefaultAuthor=None):
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Adds a role to a user. If user is left blank it defaults to the
|
Adds a role to a user. If user is left blank it defaults to the
|
||||||
author of the command.
|
author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
if self.pass_user_heirarchy_check(ctx, rolename):
|
if self.pass_user_hierarchy_check(ctx, rolename):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, user, rolename)
|
await self._addrole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
@@ -151,15 +143,16 @@ class Admin:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def removerole(self, ctx: commands.Context, rolename: discord.Role, *,
|
async def removerole(
|
||||||
user: MemberDefaultAuthor=None):
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Removes a role from a user. If user is left blank it defaults to the
|
Removes a role from a user. If user is left blank it defaults to the
|
||||||
author of the command.
|
author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
if self.pass_user_heirarchy_check(ctx, rolename):
|
if self.pass_user_hierarchy_check(ctx, rolename):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, user, rolename)
|
await self._removerole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
@@ -173,9 +166,10 @@ class Admin:
|
|||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@editrole.command(name="colour", aliases=["color", ])
|
@editrole.command(name="colour", aliases=["color"])
|
||||||
async def editrole_colour(self, ctx: commands.Context, role: discord.Role,
|
async def editrole_colour(
|
||||||
value: discord.Colour):
|
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
|
||||||
|
):
|
||||||
"""Edits a role's colour
|
"""Edits a role's colour
|
||||||
|
|
||||||
Use double quotes if the role contains spaces.
|
Use double quotes if the role contains spaces.
|
||||||
@@ -185,10 +179,9 @@ class Admin:
|
|||||||
!editrole colour \"The Transistor\" #ff0000
|
!editrole colour \"The Transistor\" #ff0000
|
||||||
!editrole colour Test #ff9900"""
|
!editrole colour Test #ff9900"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
reason = "{}({}) changed the colour of role '{}'".format(
|
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
|
||||||
author.name, author.id, role.name)
|
|
||||||
|
|
||||||
if not self.pass_user_heirarchy_check(ctx, role):
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -211,9 +204,10 @@ class Admin:
|
|||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
old_name = role.name
|
old_name = role.name
|
||||||
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
||||||
author.name, author.id, old_name, name)
|
author.name, author.id, old_name, name
|
||||||
|
)
|
||||||
|
|
||||||
if not self.pass_user_heirarchy_check(ctx, role):
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -240,8 +234,7 @@ class Admin:
|
|||||||
await ctx.send("The announcement has begun.")
|
await ctx.send("The announcement has begun.")
|
||||||
else:
|
else:
|
||||||
prefix = ctx.prefix
|
prefix = ctx.prefix
|
||||||
await self.complain(ctx, RUNNING_ANNOUNCEMENT,
|
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
|
||||||
prefix=prefix)
|
|
||||||
|
|
||||||
@announce.command(name="cancel")
|
@announce.command(name="cancel")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -259,7 +252,7 @@ class Admin:
|
|||||||
@announce.command(name="channel")
|
@announce.command(name="channel")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel=None):
|
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
|
||||||
"""
|
"""
|
||||||
Changes the channel on which the bot makes announcements.
|
Changes the channel on which the bot makes announcements.
|
||||||
"""
|
"""
|
||||||
@@ -267,14 +260,12 @@ class Admin:
|
|||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
||||||
|
|
||||||
await ctx.send("The announcement channel has been set to {}".format(
|
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
|
||||||
channel.mention
|
|
||||||
))
|
|
||||||
|
|
||||||
@announce.command(name="ignore")
|
@announce.command(name="ignore")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def announce_ignore(self, ctx, *, guild: discord.Guild=None):
|
async def announce_ignore(self, ctx, *, guild: discord.Guild = None):
|
||||||
"""
|
"""
|
||||||
Toggles whether the announcements will ignore the given server.
|
Toggles whether the announcements will ignore the given server.
|
||||||
Defaults to the current server if none is provided.
|
Defaults to the current server if none is provided.
|
||||||
@@ -287,9 +278,7 @@ class Admin:
|
|||||||
|
|
||||||
verb = "will" if ignored else "will not"
|
verb = "will" if ignored else "will not"
|
||||||
|
|
||||||
await ctx.send("The server {} {} receive announcements.".format(
|
await ctx.send("The server {} {} receive announcements.".format(guild.name, verb))
|
||||||
guild.name, verb
|
|
||||||
))
|
|
||||||
|
|
||||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||||
"""
|
"""
|
||||||
@@ -384,8 +373,10 @@ class Admin:
|
|||||||
|
|
||||||
await ctx.send("The bot {} serverlocked.".format(verb))
|
await ctx.send("The bot {} serverlocked.".format(verb))
|
||||||
|
|
||||||
# region Event Handlers
|
# region Event Handlers
|
||||||
async def on_guild_join(self, guild: discord.Guild):
|
async def on_guild_join(self, guild: discord.Guild):
|
||||||
if await self._serverlock_check(guild):
|
if await self._serverlock_check(guild):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from discord.ext import commands
|
|||||||
|
|
||||||
|
|
||||||
class Announcer:
|
class Announcer:
|
||||||
def __init__(self, ctx: commands.Context,
|
|
||||||
message: str,
|
def __init__(self, ctx: commands.Context, message: str, config=None):
|
||||||
config=None):
|
|
||||||
"""
|
"""
|
||||||
:param ctx:
|
:param ctx:
|
||||||
:param message:
|
:param message:
|
||||||
@@ -65,10 +64,7 @@ class Announcer:
|
|||||||
try:
|
try:
|
||||||
await channel.send(self.message)
|
await channel.send(self.message)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await bot_owner.send("I could not announce to server: {}".format(
|
await bot_owner.send("I could not announce to server: {}".format(g.id))
|
||||||
g.id
|
|
||||||
))
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from discord.ext import commands
|
|||||||
|
|
||||||
|
|
||||||
class MemberDefaultAuthor(commands.Converter):
|
class MemberDefaultAuthor(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
|
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
|
||||||
member_converter = commands.MemberConverter()
|
member_converter = commands.MemberConverter()
|
||||||
try:
|
try:
|
||||||
@@ -16,6 +17,7 @@ class MemberDefaultAuthor(commands.Converter):
|
|||||||
|
|
||||||
|
|
||||||
class SelfRole(commands.Converter):
|
class SelfRole(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
||||||
admin = ctx.command.instance
|
admin = ctx.command.instance
|
||||||
if admin is None:
|
if admin is None:
|
||||||
@@ -28,6 +30,5 @@ class SelfRole(commands.Converter):
|
|||||||
role = await role_converter.convert(ctx, arg)
|
role = await role_converter.convert(ctx, arg)
|
||||||
|
|
||||||
if role.id not in selfroles:
|
if role.id not in selfroles:
|
||||||
raise commands.BadArgument("The provided role is not a valid"
|
raise commands.BadArgument("The provided role is not a valid" " selfrole.")
|
||||||
" selfrole.")
|
|
||||||
return role
|
return role
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../admin.py"]
|
||||||
'../admin.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from re import search
|
|||||||
from typing import Generator, Tuple, Iterable
|
from typing import Generator, Tuple, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, commands
|
from redbot.core import Config, commands, checks
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
|
||||||
@@ -26,14 +26,9 @@ class Alias:
|
|||||||
and append them to the stored alias
|
and append them to the stored alias
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_global_settings = {
|
default_global_settings = {"entries": []}
|
||||||
"entries": []
|
|
||||||
}
|
|
||||||
|
|
||||||
default_guild_settings = {
|
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
||||||
"enabled": False,
|
|
||||||
"entries": [] # Going to be a list of dicts
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -49,14 +44,17 @@ class Alias:
|
|||||||
return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
|
return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
|
||||||
|
|
||||||
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
|
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
|
||||||
return (AliasEntry.from_json(d, bot=self.bot)
|
return (
|
||||||
for d in (await self._aliases.guild(guild).entries()))
|
AliasEntry.from_json(d, bot=self.bot)
|
||||||
|
for d in (await self._aliases.guild(guild).entries())
|
||||||
|
)
|
||||||
|
|
||||||
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
|
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
|
||||||
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
||||||
|
|
||||||
async def is_alias(self, guild: discord.Guild, alias_name: str,
|
async def is_alias(
|
||||||
server_aliases: Iterable[AliasEntry]=()) -> (bool, AliasEntry):
|
self, guild: discord.Guild, alias_name: str, server_aliases: Iterable[AliasEntry] = ()
|
||||||
|
) -> (bool, AliasEntry):
|
||||||
|
|
||||||
if not server_aliases:
|
if not server_aliases:
|
||||||
server_aliases = await self.unloaded_aliases(guild)
|
server_aliases = await self.unloaded_aliases(guild)
|
||||||
@@ -76,10 +74,11 @@ class Alias:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_valid_alias_name(alias_name: str) -> bool:
|
def is_valid_alias_name(alias_name: str) -> bool:
|
||||||
return not bool(search(r'\s', alias_name)) and alias_name.isprintable()
|
return not bool(search(r"\s", alias_name)) and alias_name.isprintable()
|
||||||
|
|
||||||
async def add_alias(self, ctx: commands.Context, alias_name: str,
|
async def add_alias(
|
||||||
command: Tuple[str], global_: bool=False) -> AliasEntry:
|
self, ctx: commands.Context, alias_name: str, command: Tuple[str], global_: bool = False
|
||||||
|
) -> AliasEntry:
|
||||||
alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
|
alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
|
||||||
|
|
||||||
if global_:
|
if global_:
|
||||||
@@ -93,8 +92,9 @@ class Alias:
|
|||||||
|
|
||||||
return alias
|
return alias
|
||||||
|
|
||||||
async def delete_alias(self, ctx: commands.Context, alias_name: str,
|
async def delete_alias(
|
||||||
global_: bool=False) -> bool:
|
self, ctx: commands.Context, alias_name: str, global_: bool = False
|
||||||
|
) -> bool:
|
||||||
if global_:
|
if global_:
|
||||||
settings = self._aliases
|
settings = self._aliases
|
||||||
else:
|
else:
|
||||||
@@ -120,16 +120,15 @@ class Alias:
|
|||||||
"""
|
"""
|
||||||
content = message.content
|
content = message.content
|
||||||
prefix_list = await self.bot.command_prefix(self.bot, message)
|
prefix_list = await self.bot.command_prefix(self.bot, message)
|
||||||
prefixes = sorted(prefix_list,
|
prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
|
||||||
key=lambda pfx: len(pfx),
|
|
||||||
reverse=True)
|
|
||||||
for p in prefixes:
|
for p in prefixes:
|
||||||
if content.startswith(p):
|
if content.startswith(p):
|
||||||
return p
|
return p
|
||||||
raise ValueError(_("No prefix found."))
|
raise ValueError(_("No prefix found."))
|
||||||
|
|
||||||
def get_extra_args_from_alias(self, message: discord.Message, prefix: str,
|
def get_extra_args_from_alias(
|
||||||
alias: AliasEntry) -> str:
|
self, message: discord.Message, prefix: str, alias: AliasEntry
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
When an alias is executed by a user in chat this function tries
|
When an alias is executed by a user in chat this function tries
|
||||||
to get any extra arguments passed in with the call.
|
to get any extra arguments passed in with the call.
|
||||||
@@ -143,25 +142,27 @@ class Alias:
|
|||||||
extra = message.content[known_content_length:].strip()
|
extra = message.content[known_content_length:].strip()
|
||||||
return extra
|
return extra
|
||||||
|
|
||||||
async def maybe_call_alias(self, message: discord.Message,
|
async def maybe_call_alias(
|
||||||
aliases: Iterable[AliasEntry]=None):
|
self, message: discord.Message, aliases: Iterable[AliasEntry] = None
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
prefix = await self.get_prefix(message)
|
prefix = await self.get_prefix(message)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
potential_alias = message.content[len(prefix):].split(" ")[0]
|
potential_alias = message.content[len(prefix) :].split(" ")[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
is_alias, alias = await self.is_alias(message.guild, potential_alias, server_aliases=aliases)
|
is_alias, alias = await self.is_alias(
|
||||||
|
message.guild, potential_alias, server_aliases=aliases
|
||||||
|
)
|
||||||
|
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await self.call_alias(message, prefix, alias)
|
await self.call_alias(message, prefix, alias)
|
||||||
|
|
||||||
async def call_alias(self, message: discord.Message, prefix: str,
|
async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry):
|
||||||
alias: AliasEntry):
|
|
||||||
new_message = copy(message)
|
new_message = copy(message)
|
||||||
args = self.get_extra_args_from_alias(message, prefix, alias)
|
args = self.get_extra_args_from_alias(message, prefix, alias)
|
||||||
|
|
||||||
@@ -181,83 +182,108 @@ class Alias:
|
|||||||
"""
|
"""
|
||||||
Manage global aliases.
|
Manage global aliases.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None or \
|
if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
|
||||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="add")
|
@alias.command(name="add")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _add_alias(self, ctx: commands.Context,
|
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||||
alias_name: str, *, command):
|
|
||||||
"""
|
"""
|
||||||
Add an alias for a command.
|
Add an alias for a command.
|
||||||
"""
|
"""
|
||||||
# region Alias Add Validity Checking
|
# region Alias Add Validity Checking
|
||||||
is_command = self.is_command(alias_name)
|
is_command = self.is_command(alias_name)
|
||||||
if is_command:
|
if is_command:
|
||||||
await ctx.send(_("You attempted to create a new alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" name is already a command on this bot.").format(alias_name))
|
" name is already a command on this bot."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await ctx.send(_("You attempted to create a new alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" alias already exists on this server.").format(alias_name))
|
" alias already exists on this server."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||||
if not is_valid_name:
|
if not is_valid_name:
|
||||||
await ctx.send(_("You attempted to create a new alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" name is an invalid alias name. Alias"
|
" name is an invalid alias name. Alias"
|
||||||
" names may not contain spaces.").format(alias_name))
|
" names may not contain spaces."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# At this point we know we need to make a new alias
|
# At this point we know we need to make a new alias
|
||||||
# and that the alias name is valid.
|
# and that the alias name is valid.
|
||||||
|
|
||||||
await self.add_alias(ctx, alias_name, command)
|
await self.add_alias(ctx, alias_name, command)
|
||||||
|
|
||||||
await ctx.send(_("A new alias with the trigger `{}`"
|
await ctx.send(
|
||||||
" has been created.").format(alias_name))
|
_("A new alias with the trigger `{}`" " has been created.").format(alias_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
@global_.command(name="add")
|
@global_.command(name="add")
|
||||||
async def _add_global_alias(self, ctx: commands.Context,
|
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||||
alias_name: str, *, command):
|
|
||||||
"""
|
"""
|
||||||
Add a global alias for a command.
|
Add a global alias for a command.
|
||||||
"""
|
"""
|
||||||
# region Alias Add Validity Checking
|
# region Alias Add Validity Checking
|
||||||
is_command = self.is_command(alias_name)
|
is_command = self.is_command(alias_name)
|
||||||
if is_command:
|
if is_command:
|
||||||
await ctx.send(_("You attempted to create a new global alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" name is already a command on this bot.").format(alias_name))
|
" name is already a command on this bot."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await ctx.send(_("You attempted to create a new global alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" alias already exists on this server.").format(alias_name))
|
" alias already exists on this server."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||||
if not is_valid_name:
|
if not is_valid_name:
|
||||||
await ctx.send(_("You attempted to create a new global alias"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {} but that"
|
||||||
" name is an invalid alias name. Alias"
|
" name is an invalid alias name. Alias"
|
||||||
" names may not contain spaces.").format(alias_name))
|
" names may not contain spaces."
|
||||||
|
).format(alias_name)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||||
|
|
||||||
await ctx.send(_("A new global alias with the trigger `{}`"
|
await ctx.send(
|
||||||
" has been created.").format(alias_name))
|
_("A new global alias with the trigger `{}`" " has been created.").format(alias_name)
|
||||||
|
)
|
||||||
|
|
||||||
@alias.command(name="help")
|
@alias.command(name="help")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -280,11 +306,15 @@ class Alias:
|
|||||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
||||||
|
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await ctx.send(_("The `{}` alias will execute the"
|
await ctx.send(
|
||||||
" command `{}`").format(alias_name, alias.command))
|
_("The `{}` alias will execute the" " command `{}`").format(
|
||||||
|
alias_name, alias.command
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
||||||
|
|
||||||
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="del")
|
@alias.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
@@ -299,11 +329,13 @@ class Alias:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name):
|
if await self.delete_alias(ctx, alias_name):
|
||||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
await ctx.send(
|
||||||
" deleted.").format(alias_name))
|
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
@global_.command(name="del")
|
@global_.command(name="del")
|
||||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""
|
"""
|
||||||
@@ -317,8 +349,9 @@ class Alias:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
await ctx.send(
|
||||||
" deleted.").format(alias_name))
|
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||||
|
|
||||||
@@ -328,7 +361,9 @@ class Alias:
|
|||||||
"""
|
"""
|
||||||
Lists the available aliases on this server.
|
Lists the available aliases on this server.
|
||||||
"""
|
"""
|
||||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))])
|
names = [_("Aliases:")] + sorted(
|
||||||
|
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
|
||||||
|
)
|
||||||
if len(names) == 0:
|
if len(names) == 0:
|
||||||
await ctx.send(_("There are no aliases on this server."))
|
await ctx.send(_("There are no aliases on this server."))
|
||||||
else:
|
else:
|
||||||
@@ -339,7 +374,9 @@ class Alias:
|
|||||||
"""
|
"""
|
||||||
Lists the available global aliases on this bot.
|
Lists the available global aliases on this bot.
|
||||||
"""
|
"""
|
||||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in await self.unloaded_global_aliases()])
|
names = [_("Aliases:")] + sorted(
|
||||||
|
["+ " + a.name for a in await self.unloaded_global_aliases()]
|
||||||
|
)
|
||||||
if len(names) == 0:
|
if len(names) == 0:
|
||||||
await ctx.send(_("There are no aliases on this server."))
|
await ctx.send(_("There are no aliases on this server."))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ from redbot.core import commands
|
|||||||
|
|
||||||
|
|
||||||
class AliasEntry:
|
class AliasEntry:
|
||||||
def __init__(self, name: str, command: Tuple[str],
|
|
||||||
creator: discord.Member, global_: bool=False):
|
def __init__(
|
||||||
|
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.has_real_data = False
|
self.has_real_data = False
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -43,13 +45,12 @@ class AliasEntry:
|
|||||||
"creator": creator,
|
"creator": creator,
|
||||||
"guild": guild,
|
"guild": guild,
|
||||||
"global": self.global_,
|
"global": self.global_,
|
||||||
"uses": self.uses
|
"uses": self.uses,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data: dict, bot: commands.Bot=None):
|
def from_json(cls, data: dict, bot: commands.Bot = None):
|
||||||
ret = cls(data["name"], data["command"],
|
ret = cls(data["name"], data["command"], data["creator"], global_=data["global"])
|
||||||
data["creator"], global_=data["global"])
|
|
||||||
|
|
||||||
if bot:
|
if bot:
|
||||||
ret.has_real_data = True
|
ret.has_real_data = True
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../alias.py"]
|
||||||
'../alias.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from redbot.core.data_manager import cog_data_path
|
|||||||
import redbot.core
|
import redbot.core
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_URL = (
|
LAVALINK_DOWNLOAD_URL = (
|
||||||
"https://github.com/Cog-Creators/Red-DiscordBot/"
|
"https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar"
|
||||||
"releases/download/{}/Lavalink.jar"
|
|
||||||
).format(redbot.core.__version__)
|
).format(redbot.core.__version__)
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
||||||
@@ -21,7 +20,7 @@ BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
|
|||||||
|
|
||||||
|
|
||||||
async def download_lavalink(session):
|
async def download_lavalink(session):
|
||||||
with LAVALINK_JAR_FILE.open(mode='wb') as f:
|
with LAVALINK_JAR_FILE.open(mode="wb") as f:
|
||||||
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
|
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
|
||||||
while True:
|
while True:
|
||||||
chunk = await resp.content.read(512)
|
chunk = await resp.content.read(512)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../audio.py"]
|
||||||
'../audio.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from subprocess import Popen, DEVNULL, PIPE
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger('red.audio.manager')
|
log = logging.getLogger("red.audio.manager")
|
||||||
|
|
||||||
proc = None
|
proc = None
|
||||||
SHUTDOWN = asyncio.Event()
|
SHUTDOWN = asyncio.Event()
|
||||||
@@ -13,7 +13,8 @@ SHUTDOWN = asyncio.Event()
|
|||||||
|
|
||||||
def has_java_error(pid):
|
def has_java_error(pid):
|
||||||
from . import LAVALINK_DOWNLOAD_DIR
|
from . import LAVALINK_DOWNLOAD_DIR
|
||||||
poss_error_file = LAVALINK_DOWNLOAD_DIR / 'hs_err_pid{}.log'.format(pid)
|
|
||||||
|
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(pid)
|
||||||
return poss_error_file.exists()
|
return poss_error_file.exists()
|
||||||
|
|
||||||
|
|
||||||
@@ -29,14 +30,14 @@ async def monitor_lavalink_server(loop):
|
|||||||
log.info("Restarting Lavalink jar.")
|
log.info("Restarting Lavalink jar.")
|
||||||
await start_lavalink_server(loop)
|
await start_lavalink_server(loop)
|
||||||
else:
|
else:
|
||||||
log.error("Your Java is borked. Please find the hs_err_pid{}.log file"
|
log.error(
|
||||||
" in the Audio data folder and report this issue.".format(
|
"Your Java is borked. Please find the hs_err_pid{}.log file"
|
||||||
proc.pid
|
" in the Audio data folder and report this issue.".format(proc.pid)
|
||||||
))
|
)
|
||||||
|
|
||||||
|
|
||||||
async def has_java(loop):
|
async def has_java(loop):
|
||||||
java_available = shutil.which('java') is not None
|
java_available = shutil.which("java") is not None
|
||||||
if not java_available:
|
if not java_available:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -48,20 +49,18 @@ async def get_java_version(loop):
|
|||||||
"""
|
"""
|
||||||
This assumes we've already checked that java exists.
|
This assumes we've already checked that java exists.
|
||||||
"""
|
"""
|
||||||
proc = Popen(
|
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
|
||||||
shlex.split("java -version", posix=os.name == 'posix'),
|
|
||||||
stdout=PIPE, stderr=PIPE
|
|
||||||
)
|
|
||||||
_, err = proc.communicate()
|
_, err = proc.communicate()
|
||||||
|
|
||||||
version_info = str(err, encoding='utf-8')
|
version_info = str(err, encoding="utf-8")
|
||||||
|
|
||||||
version_line = version_info.split('\n')[0]
|
version_line = version_info.split("\n")[0]
|
||||||
version_start = version_line.find('"')
|
version_start = version_line.find('"')
|
||||||
version_string = version_line[version_start + 1:-1]
|
version_string = version_line[version_start + 1 : -1]
|
||||||
major, minor = version_string.split('.')[:2]
|
major, minor = version_string.split(".")[:2]
|
||||||
return int(major), int(minor)
|
return int(major), int(minor)
|
||||||
|
|
||||||
|
|
||||||
async def start_lavalink_server(loop):
|
async def start_lavalink_server(loop):
|
||||||
java_available, java_version = await has_java(loop)
|
java_available, java_version = await has_java(loop)
|
||||||
if not java_available:
|
if not java_available:
|
||||||
@@ -72,13 +71,15 @@ async def start_lavalink_server(loop):
|
|||||||
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
|
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
|
||||||
|
|
||||||
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
|
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
|
||||||
|
|
||||||
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
|
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
|
||||||
|
|
||||||
global proc
|
global proc
|
||||||
proc = Popen(
|
proc = Popen(
|
||||||
shlex.split(start_cmd, posix=os.name == 'posix'),
|
shlex.split(start_cmd, posix=os.name == "posix"),
|
||||||
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
||||||
stdout=DEVNULL, stderr=DEVNULL
|
stdout=DEVNULL,
|
||||||
|
stderr=DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info("Lavalink jar started. PID: {}".format(proc.pid))
|
log.info("Lavalink jar started. PID: {}".format(proc.pid))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from redbot.core.i18n import Translator, cog_i18n
|
|||||||
|
|
||||||
from redbot.core.bot import Red # Only used for type hints
|
from redbot.core.bot import Red # Only used for type hints
|
||||||
|
|
||||||
_ = Translator('Bank', __file__)
|
_ = Translator("Bank", __file__)
|
||||||
|
|
||||||
|
|
||||||
def check_global_setting_guildowner():
|
def check_global_setting_guildowner():
|
||||||
@@ -14,6 +14,7 @@ def check_global_setting_guildowner():
|
|||||||
Command decorator. If the bank is not global, it checks if the author is
|
Command decorator. If the bank is not global, it checks if the author is
|
||||||
either the guildowner or has the administrator permission.
|
either the guildowner or has the administrator permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
if await ctx.bot.is_owner(author):
|
||||||
@@ -32,6 +33,7 @@ def check_global_setting_admin():
|
|||||||
Command decorator. If the bank is not global, it checks if the author is
|
Command decorator. If the bank is not global, it checks if the author is
|
||||||
either a bot admin or has the manage_guild permission.
|
either a bot admin or has the manage_guild permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
if await ctx.bot.is_owner(author):
|
||||||
@@ -73,19 +75,15 @@ class Bank:
|
|||||||
currency_name = await bank._conf.guild(ctx.guild).currency()
|
currency_name = await bank._conf.guild(ctx.guild).currency()
|
||||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||||
|
|
||||||
settings = (_(
|
settings = _(
|
||||||
"Bank settings:\n\n"
|
"Bank settings:\n\n" "Bank name: {}\n" "Currency: {}\n" "Default balance: {}" ""
|
||||||
"Bank name: {}\n"
|
).format(bank_name, currency_name, default_balance)
|
||||||
"Currency: {}\n"
|
|
||||||
"Default balance: {}"
|
|
||||||
"").format(bank_name, currency_name, default_balance)
|
|
||||||
)
|
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@bankset.command(name="toggleglobal")
|
@bankset.command(name="toggleglobal")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool=False):
|
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
|
||||||
"""Toggles whether the bank is global or not
|
"""Toggles whether the bank is global or not
|
||||||
If the bank is global, it will become per-server
|
If the bank is global, it will become per-server
|
||||||
If the bank is per-server, it will become global"""
|
If the bank is per-server, it will become global"""
|
||||||
@@ -94,10 +92,10 @@ class Bank:
|
|||||||
word = _("per-server") if cur_setting else _("global")
|
word = _("per-server") if cur_setting else _("global")
|
||||||
if confirm is False:
|
if confirm is False:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This will toggle the bank to be {}, deleting all accounts "
|
_(
|
||||||
"in the process! If you're sure, type `{}`").format(
|
"This will toggle the bank to be {}, deleting all accounts "
|
||||||
word, "{}bankset toggleglobal yes".format(ctx.prefix)
|
"in the process! If you're sure, type `{}`"
|
||||||
)
|
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.set_global(not cur_setting)
|
await bank.set_global(not cur_setting)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class BankError(Exception):
|
class BankError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BankNotGlobal(BankError):
|
class BankNotGlobal(BankError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../bank.py"]
|
||||||
'../bank.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,13 +27,16 @@ class Cleanup:
|
|||||||
|
|
||||||
Tries its best to cleanup after itself if the response is positive.
|
Tries its best to cleanup after itself if the response is positive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def author_check(message):
|
def author_check(message):
|
||||||
return message.author == ctx.author
|
return message.author == ctx.author
|
||||||
|
|
||||||
prompt = await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
|
prompt = await ctx.send(
|
||||||
response = await ctx.bot.wait_for('message', check=author_check)
|
_("Are you sure you want to delete {} messages? (y/n)").format(number)
|
||||||
|
)
|
||||||
|
response = await ctx.bot.wait_for("message", check=author_check)
|
||||||
|
|
||||||
if response.content.lower().startswith('y'):
|
if response.content.lower().startswith("y"):
|
||||||
await prompt.delete()
|
await prompt.delete()
|
||||||
try:
|
try:
|
||||||
await response.delete()
|
await response.delete()
|
||||||
@@ -41,14 +44,19 @@ class Cleanup:
|
|||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
await ctx.send(_('Cancelled.'))
|
await ctx.send(_("Cancelled."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_messages_for_deletion(
|
async def get_messages_for_deletion(
|
||||||
ctx: commands.Context, channel: discord.TextChannel, number,
|
ctx: commands.Context,
|
||||||
check=lambda x: True, limit=100, before=None, after=None,
|
channel: discord.TextChannel,
|
||||||
delete_pinned=False
|
number,
|
||||||
|
check=lambda x: True,
|
||||||
|
limit=100,
|
||||||
|
before=None,
|
||||||
|
after=None,
|
||||||
|
delete_pinned=False,
|
||||||
) -> list:
|
) -> list:
|
||||||
"""
|
"""
|
||||||
Gets a list of messages meeting the requirements to be deleted.
|
Gets a list of messages meeting the requirements to be deleted.
|
||||||
@@ -65,9 +73,7 @@ class Cleanup:
|
|||||||
|
|
||||||
while not too_old and len(to_delete) - 1 < number:
|
while not too_old and len(to_delete) - 1 < number:
|
||||||
message = None
|
message = None
|
||||||
async for message in channel.history(limit=limit,
|
async for message in channel.history(limit=limit, before=before, after=after):
|
||||||
before=before,
|
|
||||||
after=after):
|
|
||||||
if (
|
if (
|
||||||
(not number or len(to_delete) - 1 < number)
|
(not number or len(to_delete) - 1 < number)
|
||||||
and check(message)
|
and check(message)
|
||||||
@@ -96,7 +102,9 @@ class Cleanup:
|
|||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def text(self, ctx: commands.Context, text: str, number: int, delete_pinned: bool=False):
|
async def text(
|
||||||
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
"""Deletes last X messages matching the specified text.
|
"""Deletes last X messages matching the specified text.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -122,12 +130,18 @@ class Cleanup:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
ctx,
|
||||||
delete_pinned=delete_pinned)
|
channel,
|
||||||
|
number,
|
||||||
|
check=check,
|
||||||
|
limit=1000,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
|
)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages "\
|
reason = "{}({}) deleted {} messages " " containing '{}' in channel {}.".format(
|
||||||
" containing '{}' in channel {}.".format(author.name,
|
author.name, author.id, len(to_delete), text, channel.id
|
||||||
author.id, len(to_delete), text, channel.id)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
if is_bot:
|
if is_bot:
|
||||||
@@ -138,7 +152,9 @@ class Cleanup:
|
|||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def user(self, ctx: commands.Context, user: str, number: int, delete_pinned: bool=False):
|
async def user(
|
||||||
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
"""Deletes last X messages from specified user.
|
"""Deletes last X messages from specified user.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -174,13 +190,19 @@ class Cleanup:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
ctx,
|
||||||
delete_pinned=delete_pinned
|
channel,
|
||||||
|
number,
|
||||||
|
check=check,
|
||||||
|
limit=1000,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
|
)
|
||||||
|
reason = (
|
||||||
|
"{}({}) deleted {} messages "
|
||||||
|
" made by {}({}) in channel {}."
|
||||||
|
"".format(author.name, author.id, len(to_delete), member or "???", _id, channel.name)
|
||||||
)
|
)
|
||||||
reason = "{}({}) deleted {} messages "\
|
|
||||||
" made by {}({}) in channel {}."\
|
|
||||||
"".format(author.name, author.id, len(to_delete),
|
|
||||||
member or '???', _id, channel.name)
|
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
if is_bot:
|
if is_bot:
|
||||||
@@ -192,7 +214,7 @@ class Cleanup:
|
|||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool=False):
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||||
"""Deletes all messages after specified message.
|
"""Deletes all messages after specified message.
|
||||||
|
|
||||||
To get a message id, enable developer mode in Discord's
|
To get a message id, enable developer mode in Discord's
|
||||||
@@ -207,8 +229,7 @@ class Cleanup:
|
|||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
if not is_bot:
|
if not is_bot:
|
||||||
await ctx.send(_("This command can only be used on bots with "
|
await ctx.send(_("This command can only be used on bots with " "bot accounts."))
|
||||||
"bot accounts."))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
after = await channel.get_message(message_id)
|
after = await channel.get_message(message_id)
|
||||||
@@ -221,9 +242,9 @@ class Cleanup:
|
|||||||
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
||||||
)
|
)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}."\
|
reason = "{}({}) deleted {} messages in channel {}." "".format(
|
||||||
"".format(author.name, author.id,
|
author.name, author.id, len(to_delete), channel.name
|
||||||
len(to_delete), channel.name)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
await mass_purge(to_delete, channel)
|
await mass_purge(to_delete, channel)
|
||||||
@@ -231,7 +252,7 @@ class Cleanup:
|
|||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool=False):
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Deletes last X messages.
|
"""Deletes last X messages.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -248,14 +269,13 @@ class Cleanup:
|
|||||||
return
|
return
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
ctx, channel, number, limit=1000, before=ctx.message,
|
ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned
|
||||||
delete_pinned=delete_pinned
|
|
||||||
)
|
)
|
||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}."\
|
reason = "{}({}) deleted {} messages in channel {}." "".format(
|
||||||
"".format(author.name, author.id,
|
author.name, author.id, number, channel.name
|
||||||
number, channel.name)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
if is_bot:
|
if is_bot:
|
||||||
@@ -263,10 +283,10 @@ class Cleanup:
|
|||||||
else:
|
else:
|
||||||
await slow_deletion(to_delete)
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
@cleanup.command(name='bot')
|
@cleanup.command(name="bot")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool=False):
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Cleans up command messages and messages from the bot."""
|
"""Cleans up command messages and messages from the bot."""
|
||||||
|
|
||||||
channel = ctx.message.channel
|
channel = ctx.message.channel
|
||||||
@@ -283,8 +303,8 @@ class Cleanup:
|
|||||||
prefixes = [prefixes]
|
prefixes = [prefixes]
|
||||||
|
|
||||||
# In case some idiot sets a null prefix
|
# In case some idiot sets a null prefix
|
||||||
if '' in prefixes:
|
if "" in prefixes:
|
||||||
prefixes.remove('')
|
prefixes.remove("")
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
if m.author.id == self.bot.user.id:
|
if m.author.id == self.bot.user.id:
|
||||||
@@ -293,20 +313,26 @@ class Cleanup:
|
|||||||
return True
|
return True
|
||||||
p = discord.utils.find(m.content.startswith, prefixes)
|
p = discord.utils.find(m.content.startswith, prefixes)
|
||||||
if p and len(p) > 0:
|
if p and len(p) > 0:
|
||||||
cmd_name = m.content[len(p):].split(' ')[0]
|
cmd_name = m.content[len(p) :].split(" ")[0]
|
||||||
return bool(self.bot.get_command(cmd_name))
|
return bool(self.bot.get_command(cmd_name))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
ctx,
|
||||||
delete_pinned=delete_pinned
|
channel,
|
||||||
|
number,
|
||||||
|
check=check,
|
||||||
|
limit=1000,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} "\
|
reason = (
|
||||||
" command messages in channel {}."\
|
"{}({}) deleted {} "
|
||||||
"".format(author.name, author.id, len(to_delete),
|
" command messages in channel {}."
|
||||||
channel.name)
|
"".format(author.name, author.id, len(to_delete), channel.name)
|
||||||
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
if is_bot:
|
if is_bot:
|
||||||
@@ -314,10 +340,14 @@ class Cleanup:
|
|||||||
else:
|
else:
|
||||||
await slow_deletion(to_delete)
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
@cleanup.command(name='self')
|
@cleanup.command(name="self")
|
||||||
async def cleanup_self(
|
async def cleanup_self(
|
||||||
self, ctx: commands.Context, number: int,
|
self,
|
||||||
match_pattern: str = None, delete_pinned: bool=False):
|
ctx: commands.Context,
|
||||||
|
number: int,
|
||||||
|
match_pattern: str = None,
|
||||||
|
delete_pinned: bool = False,
|
||||||
|
):
|
||||||
"""Cleans up messages owned by the bot.
|
"""Cleans up messages owned by the bot.
|
||||||
|
|
||||||
By default, all messages are cleaned. If a third argument is specified,
|
By default, all messages are cleaned. If a third argument is specified,
|
||||||
@@ -343,8 +373,7 @@ class Cleanup:
|
|||||||
me = ctx.guild.me
|
me = ctx.guild.me
|
||||||
can_mass_purge = channel.permissions_for(me).manage_messages
|
can_mass_purge = channel.permissions_for(me).manage_messages
|
||||||
|
|
||||||
use_re = (match_pattern and match_pattern.startswith('r(') and
|
use_re = match_pattern and match_pattern.startswith("r(") and match_pattern.endswith(")")
|
||||||
match_pattern.endswith(')'))
|
|
||||||
|
|
||||||
if use_re:
|
if use_re:
|
||||||
match_pattern = match_pattern[1:] # strip 'r'
|
match_pattern = match_pattern[1:] # strip 'r'
|
||||||
@@ -352,10 +381,14 @@ class Cleanup:
|
|||||||
|
|
||||||
def content_match(c):
|
def content_match(c):
|
||||||
return bool(match_re.match(c))
|
return bool(match_re.match(c))
|
||||||
|
|
||||||
elif match_pattern:
|
elif match_pattern:
|
||||||
|
|
||||||
def content_match(c):
|
def content_match(c):
|
||||||
return match_pattern in c
|
return match_pattern in c
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def content_match(_):
|
def content_match(_):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -367,8 +400,13 @@ class Cleanup:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
ctx,
|
||||||
delete_pinned=delete_pinned
|
channel,
|
||||||
|
number,
|
||||||
|
check=check,
|
||||||
|
limit=1000,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Selfbot convenience, delete trigger message
|
# Selfbot convenience, delete trigger message
|
||||||
@@ -376,14 +414,15 @@ class Cleanup:
|
|||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
if channel.name:
|
if channel.name:
|
||||||
channel_name = 'channel ' + channel.name
|
channel_name = "channel " + channel.name
|
||||||
else:
|
else:
|
||||||
channel_name = str(channel)
|
channel_name = str(channel)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages "\
|
reason = (
|
||||||
"sent by the bot in {}."\
|
"{}({}) deleted {} messages "
|
||||||
"".format(author.name, author.id, len(to_delete),
|
"sent by the bot in {}."
|
||||||
channel_name)
|
"".format(author.name, author.id, len(to_delete), channel_name)
|
||||||
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
if is_bot and can_mass_purge:
|
if is_bot and can_mass_purge:
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../cleanup.py"]
|
||||||
'../cleanup.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class AlreadyExists(CCError):
|
|||||||
class CommandObj:
|
class CommandObj:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
config = kwargs.get('config')
|
config = kwargs.get("config")
|
||||||
self.bot = kwargs.get('bot')
|
self.bot = kwargs.get("bot")
|
||||||
self.db = config.guild
|
self.db = config.guild
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -40,22 +40,23 @@ class CommandObj:
|
|||||||
return customcommands
|
return customcommands
|
||||||
|
|
||||||
async def get_responses(self, ctx):
|
async def get_responses(self, ctx):
|
||||||
intro = (_("Welcome to the interactive random {} maker!\n"
|
intro = _(
|
||||||
|
"Welcome to the interactive random {} maker!\n"
|
||||||
"Every message you send will be added as one of the random "
|
"Every message you send will be added as one of the random "
|
||||||
"response to choose from once this {} is "
|
"response to choose from once this {} is "
|
||||||
"triggered. To exit this interactive menu, type `{}`").format(
|
"triggered. To exit this interactive menu, type `{}`"
|
||||||
"customcommand", "customcommand", "exit()"
|
).format("customcommand", "customcommand", "exit()")
|
||||||
))
|
|
||||||
await ctx.send(intro)
|
await ctx.send(intro)
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
while True:
|
while True:
|
||||||
await ctx.send(_("Add a random response:"))
|
await ctx.send(_("Add a random response:"))
|
||||||
msg = await self.bot.wait_for('message', check=check)
|
msg = await self.bot.wait_for("message", check=check)
|
||||||
|
|
||||||
if msg.content.lower() == 'exit()':
|
if msg.content.lower() == "exit()":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
responses.append(msg.content)
|
responses.append(msg.content)
|
||||||
@@ -64,44 +65,31 @@ class CommandObj:
|
|||||||
def get_now(self) -> str:
|
def get_now(self) -> str:
|
||||||
# Get current time as a string, for 'created_at' and 'edited_at' fields
|
# Get current time as a string, for 'created_at' and 'edited_at' fields
|
||||||
# in the ccinfo dict
|
# in the ccinfo dict
|
||||||
return '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow())
|
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||||
|
|
||||||
async def get(self,
|
async def get(self, message: discord.Message, command: str) -> str:
|
||||||
message: discord.Message,
|
|
||||||
command: str) -> str:
|
|
||||||
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
||||||
if not ccinfo:
|
if not ccinfo:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
else:
|
else:
|
||||||
return ccinfo['response']
|
return ccinfo["response"]
|
||||||
|
|
||||||
async def create(self,
|
async def create(self, ctx: commands.Context, command: str, response):
|
||||||
ctx: commands.Context,
|
|
||||||
command: str,
|
|
||||||
response):
|
|
||||||
"""Create a customcommand"""
|
"""Create a customcommand"""
|
||||||
# Check if this command is already registered as a customcommand
|
# Check if this command is already registered as a customcommand
|
||||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||||
raise AlreadyExists()
|
raise AlreadyExists()
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
ccinfo = {
|
ccinfo = {
|
||||||
'author': {
|
"author": {"id": author.id, "name": author.name},
|
||||||
'id': author.id,
|
"command": command,
|
||||||
'name': author.name
|
"created_at": self.get_now(),
|
||||||
},
|
"editors": [],
|
||||||
'command': command,
|
"response": response,
|
||||||
'created_at': self.get_now(),
|
|
||||||
'editors': [],
|
|
||||||
'response': response
|
|
||||||
|
|
||||||
}
|
}
|
||||||
await self.db(ctx.guild).commands.set_raw(
|
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||||
command, value=ccinfo)
|
|
||||||
|
|
||||||
async def edit(self,
|
async def edit(self, ctx: commands.Context, command: str, response: None):
|
||||||
ctx: commands.Context,
|
|
||||||
command: str,
|
|
||||||
response: None):
|
|
||||||
"""Edit an already existing custom command"""
|
"""Edit an already existing custom command"""
|
||||||
# Check if this command is registered
|
# Check if this command is registered
|
||||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||||
@@ -114,41 +102,31 @@ class CommandObj:
|
|||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
await ctx.send(
|
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
|
||||||
_("Do you want to create a 'randomized' cc? {}").format("y/n")
|
|
||||||
)
|
|
||||||
|
|
||||||
msg = await self.bot.wait_for('message', check=check)
|
msg = await self.bot.wait_for("message", check=check)
|
||||||
if msg.content.lower() == 'y':
|
if msg.content.lower() == "y":
|
||||||
response = await self.get_responses(ctx=ctx)
|
response = await self.get_responses(ctx=ctx)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("What response do you want?"))
|
await ctx.send(_("What response do you want?"))
|
||||||
response = (await self.bot.wait_for(
|
response = (await self.bot.wait_for("message", check=check)).content
|
||||||
'message', check=check)
|
|
||||||
).content
|
|
||||||
|
|
||||||
ccinfo['response'] = response
|
ccinfo["response"] = response
|
||||||
ccinfo['edited_at'] = self.get_now()
|
ccinfo["edited_at"] = self.get_now()
|
||||||
|
|
||||||
if author.id not in ccinfo['editors']:
|
if author.id not in ccinfo["editors"]:
|
||||||
# Add the person who invoked the `edit` coroutine to the list of
|
# Add the person who invoked the `edit` coroutine to the list of
|
||||||
# editors, if the person is not yet in there
|
# editors, if the person is not yet in there
|
||||||
ccinfo['editors'].append(
|
ccinfo["editors"].append(author.id)
|
||||||
author.id
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.db(ctx.guild).commands.set_raw(
|
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||||
command, value=ccinfo)
|
|
||||||
|
|
||||||
async def delete(self,
|
async def delete(self, ctx: commands.Context, command: str):
|
||||||
ctx: commands.Context,
|
|
||||||
command: str):
|
|
||||||
"""Delete an already exisiting custom command"""
|
"""Delete an already exisiting custom command"""
|
||||||
# Check if this command is registered
|
# Check if this command is registered
|
||||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
await self.db(ctx.guild).commands.set_raw(
|
await self.db(ctx.guild).commands.set_raw(command, value=None)
|
||||||
command, value=None)
|
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@@ -159,24 +137,20 @@ class CustomCommands:
|
|||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.key = 414589031223512
|
self.key = 414589031223512
|
||||||
self.config = Config.get_conf(self,
|
self.config = Config.get_conf(self, self.key)
|
||||||
self.key)
|
|
||||||
self.config.register_guild(commands={})
|
self.config.register_guild(commands={})
|
||||||
self.commandobj = CommandObj(config=self.config,
|
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||||
bot=self.bot)
|
|
||||||
|
|
||||||
@commands.group(aliases=["cc"], no_pm=True)
|
@commands.group(aliases=["cc"], no_pm=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def customcom(self,
|
async def customcom(self, ctx: commands.Context):
|
||||||
ctx: commands.Context):
|
|
||||||
"""Custom commands management"""
|
"""Custom commands management"""
|
||||||
if not ctx.invoked_subcommand:
|
if not ctx.invoked_subcommand:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@customcom.group(name="add")
|
@customcom.group(name="add")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add(self,
|
async def cc_add(self, ctx: commands.Context):
|
||||||
ctx: commands.Context):
|
|
||||||
"""
|
"""
|
||||||
CCs can be enhanced with arguments:
|
CCs can be enhanced with arguments:
|
||||||
|
|
||||||
@@ -192,15 +166,12 @@ class CustomCommands:
|
|||||||
|
|
||||||
{server} message.guild
|
{server} message.guild
|
||||||
"""
|
"""
|
||||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
|
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group):
|
||||||
commands.Group):
|
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@cc_add.command(name='random')
|
@cc_add.command(name="random")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_random(self,
|
async def cc_add_random(self, ctx: commands.Context, command: str):
|
||||||
ctx: commands.Context,
|
|
||||||
command: str):
|
|
||||||
"""
|
"""
|
||||||
Create a CC where it will randomly choose a response!
|
Create a CC where it will randomly choose a response!
|
||||||
Note: This is interactive
|
Note: This is interactive
|
||||||
@@ -210,26 +181,20 @@ class CustomCommands:
|
|||||||
|
|
||||||
responses = await self.commandobj.get_responses(ctx=ctx)
|
responses = await self.commandobj.get_responses(ctx=ctx)
|
||||||
try:
|
try:
|
||||||
await self.commandobj.create(ctx=ctx,
|
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
||||||
command=command,
|
|
||||||
response=responses)
|
|
||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
"This command already exists. Use "
|
_("This command already exists. Use " "`{}` to edit it.").format(
|
||||||
"`{}` to edit it.").format(
|
|
||||||
"{}customcom edit".format(ctx.prefix)
|
"{}customcom edit".format(ctx.prefix)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# await ctx.send(str(responses))
|
# await ctx.send(str(responses))
|
||||||
|
|
||||||
@cc_add.command(name="simple")
|
@cc_add.command(name="simple")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_simple(self,
|
async def cc_add_simple(self, ctx, command: str, *, text):
|
||||||
ctx,
|
|
||||||
command: str,
|
|
||||||
*,
|
|
||||||
text):
|
|
||||||
"""Adds a simple custom command
|
"""Adds a simple custom command
|
||||||
Example:
|
Example:
|
||||||
[p]customcom add simple yourcommand Text you want
|
[p]customcom add simple yourcommand Text you want
|
||||||
@@ -240,24 +205,18 @@ class CustomCommands:
|
|||||||
await ctx.send(_("That command is already a standard command."))
|
await ctx.send(_("That command is already a standard command."))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await self.commandobj.create(ctx=ctx,
|
await self.commandobj.create(ctx=ctx, command=command, response=text)
|
||||||
command=command,
|
|
||||||
response=text)
|
|
||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
"This command already exists. Use "
|
_("This command already exists. Use " "`{}` to edit it.").format(
|
||||||
"`{}` to edit it.").format(
|
|
||||||
"{}customcom edit".format(ctx.prefix)
|
"{}customcom edit".format(ctx.prefix)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@customcom.command(name="edit")
|
@customcom.command(name="edit")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_edit(self,
|
async def cc_edit(self, ctx, command: str, *, text=None):
|
||||||
ctx,
|
|
||||||
command: str,
|
|
||||||
*,
|
|
||||||
text=None):
|
|
||||||
"""Edits a custom command
|
"""Edits a custom command
|
||||||
Example:
|
Example:
|
||||||
[p]customcom edit yourcommand Text you want
|
[p]customcom edit yourcommand Text you want
|
||||||
@@ -266,61 +225,55 @@ class CustomCommands:
|
|||||||
command = command.lower()
|
command = command.lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.commandobj.edit(ctx=ctx,
|
await self.commandobj.edit(ctx=ctx, command=command, response=text)
|
||||||
command=command,
|
|
||||||
response=text)
|
|
||||||
await ctx.send(_("Custom command successfully edited."))
|
await ctx.send(_("Custom command successfully edited."))
|
||||||
except NotFound:
|
except NotFound:
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
"That command doesn't exist. Use "
|
_("That command doesn't exist. Use " "`{}` to add it.").format(
|
||||||
"`{}` to add it.").format(
|
|
||||||
"{}customcom add".format(ctx.prefix)
|
"{}customcom add".format(ctx.prefix)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@customcom.command(name="delete")
|
@customcom.command(name="delete")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_delete(self,
|
async def cc_delete(self, ctx, command: str):
|
||||||
ctx,
|
|
||||||
command: str):
|
|
||||||
"""Deletes a custom command
|
"""Deletes a custom command
|
||||||
Example:
|
Example:
|
||||||
[p]customcom delete yourcommand"""
|
[p]customcom delete yourcommand"""
|
||||||
guild = ctx.message.guild
|
guild = ctx.message.guild
|
||||||
command = command.lower()
|
command = command.lower()
|
||||||
try:
|
try:
|
||||||
await self.commandobj.delete(ctx=ctx,
|
await self.commandobj.delete(ctx=ctx, command=command)
|
||||||
command=command)
|
|
||||||
await ctx.send(_("Custom command successfully deleted."))
|
await ctx.send(_("Custom command successfully deleted."))
|
||||||
except NotFound:
|
except NotFound:
|
||||||
await ctx.send(_("That command doesn't exist."))
|
await ctx.send(_("That command doesn't exist."))
|
||||||
|
|
||||||
@customcom.command(name="list")
|
@customcom.command(name="list")
|
||||||
async def cc_list(self,
|
async def cc_list(self, ctx):
|
||||||
ctx):
|
|
||||||
"""Shows custom commands list"""
|
"""Shows custom commands list"""
|
||||||
|
|
||||||
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
|
_(
|
||||||
"There are no custom commands in this server."
|
"There are no custom commands in this server."
|
||||||
" Use `{}` to start adding some.").format(
|
" Use `{}` to start adding some."
|
||||||
"{}customcom add".format(ctx.prefix)
|
).format("{}customcom add".format(ctx.prefix))
|
||||||
))
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for command, body in response.items():
|
for command, body in response.items():
|
||||||
responses = body['response']
|
responses = body["response"]
|
||||||
if isinstance(responses, list):
|
if isinstance(responses, list):
|
||||||
result = ", ".join(responses)
|
result = ", ".join(responses)
|
||||||
elif isinstance(responses, str):
|
elif isinstance(responses, str):
|
||||||
result = responses
|
result = responses
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
results.append("{command:<15} : {result}".format(command=command,
|
results.append("{command:<15} : {result}".format(command=command, result=result))
|
||||||
result=result))
|
|
||||||
|
|
||||||
commands = "\n".join(results)
|
commands = "\n".join(results)
|
||||||
|
|
||||||
@@ -330,14 +283,13 @@ class CustomCommands:
|
|||||||
for page in pagify(commands, delims=[" ", "\n"]):
|
for page in pagify(commands, delims=[" ", "\n"]):
|
||||||
await ctx.author.send(box(page))
|
await ctx.author.send(box(page))
|
||||||
|
|
||||||
async def on_message(self,
|
async def on_message(self, message):
|
||||||
message):
|
|
||||||
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
|
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
|
||||||
if len(message.content) < 2 or is_private:
|
if len(message.content) < 2 or is_private:
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = message.guild
|
guild = message.guild
|
||||||
prefixes = await self.bot.db.guild(guild).get_raw('prefix', default=[])
|
prefixes = await self.bot.db.guild(guild).get_raw("prefix", default=[])
|
||||||
|
|
||||||
if len(prefixes) < 1:
|
if len(prefixes) < 1:
|
||||||
def_prefixes = await self.bot.get_prefix(message)
|
def_prefixes = await self.bot.get_prefix(message)
|
||||||
@@ -356,10 +308,9 @@ class CustomCommands:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if user_allowed:
|
if user_allowed:
|
||||||
cmd = message.content[len(prefix):]
|
cmd = message.content[len(prefix) :]
|
||||||
try:
|
try:
|
||||||
c = await self.commandobj.get(message=message,
|
c = await self.commandobj.get(message=message, command=cmd)
|
||||||
command=cmd)
|
|
||||||
if isinstance(c, list):
|
if isinstance(c, list):
|
||||||
command = random.choice(c)
|
command = random.choice(c)
|
||||||
elif isinstance(c, str):
|
elif isinstance(c, str):
|
||||||
@@ -371,18 +322,14 @@ class CustomCommands:
|
|||||||
response = self.format_cc(command, message)
|
response = self.format_cc(command, message)
|
||||||
await message.channel.send(response)
|
await message.channel.send(response)
|
||||||
|
|
||||||
def format_cc(self,
|
def format_cc(self, command, message) -> str:
|
||||||
command,
|
|
||||||
message) -> str:
|
|
||||||
results = re.findall("\{([^}]+)\}", command)
|
results = re.findall("\{([^}]+)\}", command)
|
||||||
for result in results:
|
for result in results:
|
||||||
param = self.transform_parameter(result, message)
|
param = self.transform_parameter(result, message)
|
||||||
command = command.replace("{" + result + "}", param)
|
command = command.replace("{" + result + "}", param)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
def transform_parameter(self,
|
def transform_parameter(self, result, message) -> str:
|
||||||
result,
|
|
||||||
message) -> str:
|
|
||||||
"""
|
"""
|
||||||
For security reasons only specific objects are allowed
|
For security reasons only specific objects are allowed
|
||||||
Internals are ignored
|
Internals are ignored
|
||||||
@@ -393,7 +340,7 @@ class CustomCommands:
|
|||||||
"author": message.author,
|
"author": message.author,
|
||||||
"channel": message.channel,
|
"channel": message.channel,
|
||||||
"guild": message.guild,
|
"guild": message.guild,
|
||||||
"server": message.guild
|
"server": message.guild,
|
||||||
}
|
}
|
||||||
if result in objects:
|
if result in objects:
|
||||||
return str(objects[result])
|
return str(objects[result])
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../customcom.py"]
|
||||||
'../customcom.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -16,49 +16,49 @@ class SpecResolver(object):
|
|||||||
self.v2path = path
|
self.v2path = path
|
||||||
self.resolved = set()
|
self.resolved = set()
|
||||||
self.available_core_conversions = {
|
self.available_core_conversions = {
|
||||||
'Bank Accounts': {
|
"Bank Accounts": {
|
||||||
'cfg': ('Bank', None, 384734293238749),
|
"cfg": ("Bank", None, 384734293238749),
|
||||||
'file': self.v2path / 'data' / 'economy' / 'bank.json',
|
"file": self.v2path / "data" / "economy" / "bank.json",
|
||||||
'converter': self.bank_accounts_conv_spec
|
"converter": self.bank_accounts_conv_spec,
|
||||||
},
|
},
|
||||||
'Economy Settings': {
|
"Economy Settings": {
|
||||||
'cfg': ('Economy', 'config', 1256844281),
|
"cfg": ("Economy", "config", 1256844281),
|
||||||
'file': self.v2path / 'data' / 'economy' / 'settings.json',
|
"file": self.v2path / "data" / "economy" / "settings.json",
|
||||||
'converter': self.economy_conv_spec
|
"converter": self.economy_conv_spec,
|
||||||
},
|
},
|
||||||
'Mod Log Cases': {
|
"Mod Log Cases": {
|
||||||
'cfg': ('ModLog', None, 1354799444),
|
"cfg": ("ModLog", None, 1354799444),
|
||||||
'file': self.v2path / 'data' / 'mod' / 'modlog.json',
|
"file": self.v2path / "data" / "mod" / "modlog.json",
|
||||||
'converter': None # prevents from showing as available
|
"converter": None, # prevents from showing as available
|
||||||
},
|
},
|
||||||
'Filter': {
|
"Filter": {
|
||||||
'cfg': ('Filter', 'settings', 4766951341),
|
"cfg": ("Filter", "settings", 4766951341),
|
||||||
'file': self.v2path / 'data' / 'mod' / 'filter.json',
|
"file": self.v2path / "data" / "mod" / "filter.json",
|
||||||
'converter': self.filter_conv_spec
|
"converter": self.filter_conv_spec,
|
||||||
},
|
},
|
||||||
'Past Names': {
|
"Past Names": {
|
||||||
'cfg': ('Mod', 'settings', 4961522000),
|
"cfg": ("Mod", "settings", 4961522000),
|
||||||
'file': self.v2path / 'data' / 'mod' / 'past_names.json',
|
"file": self.v2path / "data" / "mod" / "past_names.json",
|
||||||
'converter': self.past_names_conv_spec
|
"converter": self.past_names_conv_spec,
|
||||||
},
|
},
|
||||||
'Past Nicknames': {
|
"Past Nicknames": {
|
||||||
'cfg': ('Mod', 'settings', 4961522000),
|
"cfg": ("Mod", "settings", 4961522000),
|
||||||
'file': self.v2path / 'data' / 'mod' / 'past_nicknames.json',
|
"file": self.v2path / "data" / "mod" / "past_nicknames.json",
|
||||||
'converter': self.past_nicknames_conv_spec
|
"converter": self.past_nicknames_conv_spec,
|
||||||
|
},
|
||||||
|
"Custom Commands": {
|
||||||
|
"cfg": ("CustomCommands", "config", 414589031223512),
|
||||||
|
"file": self.v2path / "data" / "customcom" / "commands.json",
|
||||||
|
"converter": self.customcom_conv_spec,
|
||||||
},
|
},
|
||||||
'Custom Commands': {
|
|
||||||
'cfg': ('CustomCommands', 'config', 414589031223512),
|
|
||||||
'file': self.v2path / 'data' / 'customcom' / 'commands.json',
|
|
||||||
'converter': self.customcom_conv_spec
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
return sorted(
|
return sorted(
|
||||||
k for k, v in self.available_core_conversions.items()
|
k
|
||||||
if v['file'].is_file() and v['converter'] is not None
|
for k, v in self.available_core_conversions.items()
|
||||||
and k not in self.resolved
|
if v["file"].is_file() and v["converter"] is not None and k not in self.resolved
|
||||||
)
|
)
|
||||||
|
|
||||||
def unpack(self, parent_key, parent_value):
|
def unpack(self, parent_key, parent_value):
|
||||||
@@ -75,15 +75,8 @@ class SpecResolver(object):
|
|||||||
"""Flatten a nested dictionary structure"""
|
"""Flatten a nested dictionary structure"""
|
||||||
dictionary = {(key,): value for key, value in dictionary.items()}
|
dictionary = {(key,): value for key, value in dictionary.items()}
|
||||||
while True:
|
while True:
|
||||||
dictionary = dict(
|
dictionary = dict(chain.from_iterable(starmap(self.unpack, dictionary.items())))
|
||||||
chain.from_iterable(
|
if not any(isinstance(value, dict) for value in dictionary.values()):
|
||||||
starmap(self.unpack, dictionary.items())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not any(
|
|
||||||
isinstance(value, dict)
|
|
||||||
for value in dictionary.values()
|
|
||||||
):
|
|
||||||
break
|
break
|
||||||
return dictionary
|
return dictionary
|
||||||
|
|
||||||
@@ -97,11 +90,8 @@ class SpecResolver(object):
|
|||||||
outerkey, innerkey = tuple(k[:-1]), (k[-1],)
|
outerkey, innerkey = tuple(k[:-1]), (k[-1],)
|
||||||
if outerkey not in ret:
|
if outerkey not in ret:
|
||||||
ret[outerkey] = {}
|
ret[outerkey] = {}
|
||||||
if innerkey[0] == 'created_at':
|
if innerkey[0] == "created_at":
|
||||||
x = int(
|
x = int(datetime.strptime(v, "%Y-%m-%d %H:%M:%S").timestamp())
|
||||||
datetime.strptime(
|
|
||||||
v, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
||||||
)
|
|
||||||
ret[outerkey].update({innerkey: x})
|
ret[outerkey].update({innerkey: x})
|
||||||
else:
|
else:
|
||||||
ret[outerkey].update({innerkey: v})
|
ret[outerkey].update({innerkey: v})
|
||||||
@@ -121,16 +111,10 @@ class SpecResolver(object):
|
|||||||
raise NotImplementedError("This one isn't ready yet")
|
raise NotImplementedError("This one isn't ready yet")
|
||||||
|
|
||||||
def filter_conv_spec(self, data: dict):
|
def filter_conv_spec(self, data: dict):
|
||||||
return {
|
return {(Config.GUILD, k): {("filter",): v} for k, v in data.items()}
|
||||||
(Config.GUILD, k): {('filter',): v}
|
|
||||||
for k, v in data.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def past_names_conv_spec(self, data: dict):
|
def past_names_conv_spec(self, data: dict):
|
||||||
return {
|
return {(Config.USER, k): {("past_names",): v} for k, v in data.items()}
|
||||||
(Config.USER, k): {('past_names',): v}
|
|
||||||
for k, v in data.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def past_nicknames_conv_spec(self, data: dict):
|
def past_nicknames_conv_spec(self, data: dict):
|
||||||
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
||||||
@@ -146,19 +130,16 @@ class SpecResolver(object):
|
|||||||
flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data))
|
flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data))
|
||||||
ret = {}
|
ret = {}
|
||||||
for k, v in flatscoped.items():
|
for k, v in flatscoped.items():
|
||||||
outerkey, innerkey = (*k[:-1],), ('commands', k[-1])
|
outerkey, innerkey = (*k[:-1],), ("commands", k[-1])
|
||||||
if outerkey not in ret:
|
if outerkey not in ret:
|
||||||
ret[outerkey] = {}
|
ret[outerkey] = {}
|
||||||
|
|
||||||
ccinfo = {
|
ccinfo = {
|
||||||
'author': {
|
"author": {"id": 42, "name": "Converted from a v2 instance"},
|
||||||
'id': 42,
|
"command": k[-1],
|
||||||
'name': 'Converted from a v2 instance'
|
"created_at": "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()),
|
||||||
},
|
"editors": [],
|
||||||
'command': k[-1],
|
"response": v,
|
||||||
'created_at': '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow()),
|
|
||||||
'editors': [],
|
|
||||||
'response': v
|
|
||||||
}
|
}
|
||||||
ret[outerkey].update({innerkey: ccinfo})
|
ret[outerkey].update({innerkey: ccinfo})
|
||||||
return ret
|
return ret
|
||||||
@@ -168,8 +149,8 @@ class SpecResolver(object):
|
|||||||
raise NotImplementedError("No Conversion Specs for this")
|
raise NotImplementedError("No Conversion Specs for this")
|
||||||
|
|
||||||
info = self.available_core_conversions[prettyname]
|
info = self.available_core_conversions[prettyname]
|
||||||
filepath, converter = info['file'], info['converter']
|
filepath, converter = info["file"], info["converter"]
|
||||||
(cogname, attr, _id) = info['cfg']
|
(cogname, attr, _id) = info["cfg"]
|
||||||
try:
|
try:
|
||||||
config = getattr(bot.get_cog(cogname), attr)
|
config = getattr(bot.get_cog(cogname), attr)
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from redbot.core.i18n import Translator, cog_i18n
|
|||||||
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
|
||||||
_ = Translator('DataConverter', __file__)
|
_ = Translator("DataConverter", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@@ -34,13 +34,14 @@ class DataConverter:
|
|||||||
|
|
||||||
if not resolver.available:
|
if not resolver.available:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
_("There don't seem to be any data files I know how to "
|
_(
|
||||||
|
"There don't seem to be any data files I know how to "
|
||||||
"handle here. Are you sure you gave me the base "
|
"handle here. Are you sure you gave me the base "
|
||||||
"installation path?")
|
"installation path?"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
while resolver.available:
|
while resolver.available:
|
||||||
menu = _("Please select a set of data to import by number"
|
menu = _("Please select a set of data to import by number" ", or 'exit' to exit")
|
||||||
", or 'exit' to exit")
|
|
||||||
for index, entry in enumerate(resolver.available, 1):
|
for index, entry in enumerate(resolver.available, 1):
|
||||||
menu += "\n{}. {}".format(index, entry)
|
menu += "\n{}. {}".format(index, entry)
|
||||||
|
|
||||||
@@ -50,24 +51,17 @@ class DataConverter:
|
|||||||
return m.channel == ctx.channel and m.author == ctx.author
|
return m.channel == ctx.channel and m.author == ctx.author
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for(
|
message = await self.bot.wait_for("message", check=pred, timeout=60)
|
||||||
'message', check=pred, timeout=60
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return await ctx.send(
|
return await ctx.send(_("Try this again when you are more ready"))
|
||||||
_('Try this again when you are more ready'))
|
|
||||||
else:
|
else:
|
||||||
if message.content.strip().lower() in [
|
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
|
||||||
'quit', 'exit', '-1', 'q', 'cancel'
|
|
||||||
]:
|
|
||||||
return await ctx.tick()
|
return await ctx.tick()
|
||||||
try:
|
try:
|
||||||
message = int(message.content.strip())
|
message = int(message.content.strip())
|
||||||
to_conv = resolver.available[message - 1]
|
to_conv = resolver.available[message - 1]
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
await ctx.send(
|
await ctx.send(_("That wasn't a valid choice."))
|
||||||
_("That wasn't a valid choice.")
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
@@ -76,6 +70,8 @@ class DataConverter:
|
|||||||
await menu_message.delete()
|
await menu_message.delete()
|
||||||
else:
|
else:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
_("There isn't anything else I know how to convert here."
|
_(
|
||||||
"\nThere might be more things I can convert in the future.")
|
"There isn't anything else I know how to convert here."
|
||||||
|
"\nThere might be more things I can convert in the future."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../dataconverter.py"]
|
||||||
'../dataconverter.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import asyncio
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
__all__ = ["install_agreement", ]
|
__all__ = ["install_agreement"]
|
||||||
|
|
||||||
REPO_INSTALL_MSG = (
|
REPO_INSTALL_MSG = (
|
||||||
"You're about to add a 3rd party repository. The creator of Red"
|
"You're about to add a 3rd party repository. The creator of Red"
|
||||||
@@ -17,29 +17,32 @@ REPO_INSTALL_MSG = (
|
|||||||
|
|
||||||
|
|
||||||
def install_agreement():
|
def install_agreement():
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
downloader = ctx.command.instance
|
downloader = ctx.command.instance
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
return True
|
return True
|
||||||
elif downloader.already_agreed:
|
elif downloader.already_agreed:
|
||||||
return True
|
return True
|
||||||
elif ctx.invoked_subcommand is None or \
|
elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
|
||||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def does_agree(msg: discord.Message):
|
def does_agree(msg: discord.Message):
|
||||||
return ctx.author == msg.author and \
|
return (
|
||||||
ctx.channel == msg.channel and \
|
ctx.author == msg.author
|
||||||
msg.content == "I agree"
|
and ctx.channel == msg.channel
|
||||||
|
and msg.content == "I agree"
|
||||||
|
)
|
||||||
|
|
||||||
await ctx.send(REPO_INSTALL_MSG)
|
await ctx.send(REPO_INSTALL_MSG)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await ctx.bot.wait_for('message', check=does_agree, timeout=30)
|
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("Your response has timed out, please try again.")
|
await ctx.send("Your response has timed out, please try again.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
downloader.already_agreed = True
|
downloader.already_agreed = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .installable import Installable
|
|||||||
|
|
||||||
|
|
||||||
class InstalledCog(commands.Converter):
|
class InstalledCog(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
downloader = ctx.bot.get_cog("Downloader")
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
@@ -12,8 +13,6 @@ class InstalledCog(commands.Converter):
|
|||||||
|
|
||||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
||||||
if cog is None:
|
if cog is None:
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument("That cog is not installed")
|
||||||
"That cog is not installed"
|
|
||||||
)
|
|
||||||
|
|
||||||
return cog
|
return cog
|
||||||
|
|||||||
@@ -22,20 +22,18 @@ from .installable import Installable
|
|||||||
from .log import log
|
from .log import log
|
||||||
from .repo_manager import RepoManager, Repo
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
_ = Translator('Downloader', __file__)
|
_ = Translator("Downloader", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Downloader:
|
class Downloader:
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
self.conf = Config.get_conf(self, identifier=998240343,
|
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
||||||
force_registration=True)
|
|
||||||
|
|
||||||
self.conf.register_global(
|
self.conf.register_global(installed=[])
|
||||||
installed=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.already_agreed = False
|
self.already_agreed = False
|
||||||
|
|
||||||
@@ -46,13 +44,13 @@ class Downloader:
|
|||||||
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
|
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
if not self.SHAREDLIB_INIT.exists():
|
if not self.SHAREDLIB_INIT.exists():
|
||||||
with self.SHAREDLIB_INIT.open(mode='w', encoding='utf-8') as _:
|
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if str(self.LIB_PATH) not in syspath:
|
if str(self.LIB_PATH) not in syspath:
|
||||||
syspath.insert(1, str(self.LIB_PATH))
|
syspath.insert(1, str(self.LIB_PATH))
|
||||||
|
|
||||||
self._repo_manager = RepoManager(self.conf)
|
self._repo_manager = RepoManager()
|
||||||
|
|
||||||
async def cog_install_path(self):
|
async def cog_install_path(self):
|
||||||
"""Get the current cog install path.
|
"""Get the current cog install path.
|
||||||
@@ -170,7 +168,7 @@ class Downloader:
|
|||||||
for repo, reqs in has_reqs:
|
for repo, reqs in has_reqs:
|
||||||
for req in reqs:
|
for req in reqs:
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
ret = ret and await repo.install_raw_requirements([req, ], self.LIB_PATH)
|
ret = ret and await repo.install_raw_requirements([req], self.LIB_PATH)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -200,8 +198,12 @@ class Downloader:
|
|||||||
if success:
|
if success:
|
||||||
await ctx.send(_("Libraries installed."))
|
await ctx.send(_("Libraries installed."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Some libraries failed to install. Please check"
|
await ctx.send(
|
||||||
" your logs for a complete list."))
|
_(
|
||||||
|
"Some libraries failed to install. Please check"
|
||||||
|
" your logs for a complete list."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -214,7 +216,7 @@ class Downloader:
|
|||||||
|
|
||||||
@repo.command(name="add")
|
@repo.command(name="add")
|
||||||
@install_agreement()
|
@install_agreement()
|
||||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str=None):
|
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||||
"""
|
"""
|
||||||
Add a new repo to Downloader.
|
Add a new repo to Downloader.
|
||||||
|
|
||||||
@@ -223,11 +225,7 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
repo = await self._repo_manager.add_repo(
|
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||||
name=name,
|
|
||||||
url=repo_url,
|
|
||||||
branch=branch
|
|
||||||
)
|
|
||||||
except ExistingGitRepo:
|
except ExistingGitRepo:
|
||||||
await ctx.send(_("That git repo has already been added under another name."))
|
await ctx.send(_("That git repo has already been added under another name."))
|
||||||
except CloningError:
|
except CloningError:
|
||||||
@@ -254,11 +252,26 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
repos = self._repo_manager.get_all_repo_names()
|
repos = self._repo_manager.get_all_repo_names()
|
||||||
repos = sorted(repos, key=str.lower)
|
repos = sorted(repos, key=str.lower)
|
||||||
joined = _("Installed Repos:\n") + "\n".join(["+ " + r for r in repos])
|
joined = _("Installed Repos:\n\n")
|
||||||
|
for repo_name in repos:
|
||||||
|
repo = self._repo_manager.get_repo(repo_name)
|
||||||
|
joined += "+ {}: {}\n".format(repo.name, repo.short or "")
|
||||||
|
|
||||||
for page in pagify(joined, ["\n"], shorten_by=16):
|
for page in pagify(joined, ["\n"], shorten_by=16):
|
||||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
|
@repo.command(name="info")
|
||||||
|
async def _repo_info(self, ctx, repo_name: Repo):
|
||||||
|
"""
|
||||||
|
Lists information about a single repo
|
||||||
|
"""
|
||||||
|
if repo_name is None:
|
||||||
|
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
|
||||||
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def cog(self, ctx):
|
async def cog(self, ctx):
|
||||||
@@ -275,20 +288,28 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
||||||
if cog is None:
|
if cog is None:
|
||||||
await ctx.send(_("Error, there is no cog by the name of"
|
await ctx.send(
|
||||||
" `{}` in the `{}` repo.").format(cog_name, repo_name.name))
|
_("Error, there is no cog by the name of" " `{}` in the `{}` repo.").format(
|
||||||
|
cog_name, repo_name.name
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
elif cog.min_python_version > sys.version_info:
|
elif cog.min_python_version > sys.version_info:
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
|
_(
|
||||||
"This cog requires at least python version {}, aborting install.".format(
|
"This cog requires at least python version {}, aborting install.".format(
|
||||||
'.'.join([str(n) for n in cog.min_python_version])
|
".".join([str(n) for n in cog.min_python_version])
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
||||||
await ctx.send(_("Failed to install the required libraries for"
|
await ctx.send(
|
||||||
" `{}`: `{}`").format(cog.name, cog.requirements))
|
_("Failed to install the required libraries for" " `{}`: `{}`").format(
|
||||||
|
cog.name, cog.requirements
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await repo_name.install_cog(cog, await self.cog_install_path())
|
await repo_name.install_cog(cog, await self.cog_install_path())
|
||||||
@@ -317,12 +338,16 @@ class Downloader:
|
|||||||
await self._remove_from_installed(cog_name)
|
await self._remove_from_installed(cog_name)
|
||||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That cog was installed but can no longer"
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"That cog was installed but can no longer"
|
||||||
" be located. You may need to remove it's"
|
" be located. You may need to remove it's"
|
||||||
" files manually if it is still usable."))
|
" files manually if it is still usable."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@cog.command(name="update")
|
@cog.command(name="update")
|
||||||
async def _cog_update(self, ctx, cog_name: InstalledCog=None):
|
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
|
||||||
"""
|
"""
|
||||||
Updates all cogs or one of your choosing.
|
Updates all cogs or one of your choosing.
|
||||||
"""
|
"""
|
||||||
@@ -358,9 +383,11 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
cogs = repo_name.available_cogs
|
cogs = repo_name.available_cogs
|
||||||
cogs = _("Available Cogs:\n") + "\n".join(
|
cogs = _("Available Cogs:\n") + "\n".join(
|
||||||
["+ {}: {}".format(c.name, c.short or "") for c in cogs])
|
["+ {}: {}".format(c.name, c.short or "") for c in cogs]
|
||||||
|
)
|
||||||
|
|
||||||
await ctx.send(box(cogs, lang="diff"))
|
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||||
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
@cog.command(name="info")
|
@cog.command(name="info")
|
||||||
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
||||||
@@ -369,15 +396,17 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
||||||
if cog is None:
|
if cog is None:
|
||||||
await ctx.send(_("There is no cog `{}` in the repo `{}`").format(
|
await ctx.send(
|
||||||
cog_name, repo_name.name
|
_("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
|
||||||
))
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
|
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]):
|
async def is_installed(
|
||||||
|
self, cog_name: str
|
||||||
|
) -> Union[Tuple[bool, Installable], Tuple[bool, None]]:
|
||||||
"""Check to see if a cog has been installed through Downloader.
|
"""Check to see if a cog has been installed through Downloader.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -397,8 +426,9 @@ class Downloader:
|
|||||||
return True, installable
|
return True, installable
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
def format_findcog_info(self, command_name: str,
|
def format_findcog_info(
|
||||||
cog_installable: Union[Installable, object]=None) -> str:
|
self, command_name: str, cog_installable: Union[Installable, object] = None
|
||||||
|
) -> str:
|
||||||
"""Format a cog's info for output to discord.
|
"""Format a cog's info for output to discord.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -444,7 +474,7 @@ class Downloader:
|
|||||||
The name of the cog according to Downloader..
|
The name of the cog according to Downloader..
|
||||||
|
|
||||||
"""
|
"""
|
||||||
splitted = instance.__module__.split('.')
|
splitted = instance.__module__.split(".")
|
||||||
return splitted[-2]
|
return splitted[-2]
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo",
|
__all__ = [
|
||||||
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError",
|
"DownloaderException",
|
||||||
"UpdateError", "GitDiffError", "PipError"]
|
"GitException",
|
||||||
|
"InvalidRepoName",
|
||||||
|
"ExistingGitRepo",
|
||||||
|
"MissingGitRepo",
|
||||||
|
"CloningError",
|
||||||
|
"CurrentHashError",
|
||||||
|
"HardResetError",
|
||||||
|
"UpdateError",
|
||||||
|
"GitDiffError",
|
||||||
|
"PipError",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DownloaderException(Exception):
|
class DownloaderException(Exception):
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class Installable(RepoJSONMixin):
|
|||||||
:class:`InstallationType`.
|
:class:`InstallationType`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, location: Path):
|
def __init__(self, location: Path):
|
||||||
"""Base installable initializer.
|
"""Base installable initializer.
|
||||||
|
|
||||||
@@ -114,13 +115,9 @@ class Installable(RepoJSONMixin):
|
|||||||
|
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
copy_func(
|
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||||
src=str(self._location),
|
|
||||||
dst=str(target_dir / self._location.stem)
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
log.exception("Error occurred when copying path:"
|
log.exception("Error occurred when copying path:" " {}".format(self._location))
|
||||||
" {}".format(self._location))
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -130,7 +127,7 @@ class Installable(RepoJSONMixin):
|
|||||||
if self._info_file.exists():
|
if self._info_file.exists():
|
||||||
self._process_info_file()
|
self._process_info_file()
|
||||||
|
|
||||||
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]:
|
def _process_info_file(self, info_file_path: Path = None) -> MutableMapping[str, Any]:
|
||||||
"""
|
"""
|
||||||
Processes an information file. Loads dependencies among other
|
Processes an information file. Loads dependencies among other
|
||||||
information into this object.
|
information into this object.
|
||||||
@@ -144,13 +141,14 @@ class Installable(RepoJSONMixin):
|
|||||||
raise ValueError("No valid information file path was found.")
|
raise ValueError("No valid information file path was found.")
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
with info_file_path.open(encoding='utf-8') as f:
|
with info_file_path.open(encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
info = {}
|
info = {}
|
||||||
log.exception("Invalid JSON information file at path:"
|
log.exception(
|
||||||
" {}".format(info_file_path))
|
"Invalid JSON information file at path:" " {}".format(info_file_path)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._info = info
|
self._info = info
|
||||||
|
|
||||||
@@ -167,7 +165,7 @@ class Installable(RepoJSONMixin):
|
|||||||
self.bot_version = bot_version
|
self.bot_version = bot_version
|
||||||
|
|
||||||
try:
|
try:
|
||||||
min_python_version = tuple(info.get('min_python_version', [3, 5, 1]))
|
min_python_version = tuple(info.get("min_python_version", [3, 5, 1]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
min_python_version = self.min_python_version
|
min_python_version = self.min_python_version
|
||||||
self.min_python_version = min_python_version
|
self.min_python_version = min_python_version
|
||||||
@@ -200,15 +198,12 @@ class Installable(RepoJSONMixin):
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
return {
|
return {"repo_name": self.repo_name, "cog_name": self.name}
|
||||||
"repo_name": self.repo_name,
|
|
||||||
"cog_name": self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data: dict, repo_mgr: "RepoManager"):
|
def from_json(cls, data: dict, repo_mgr: "RepoManager"):
|
||||||
repo_name = data['repo_name']
|
repo_name = data["repo_name"]
|
||||||
cog_name = data['cog_name']
|
cog_name = data["cog_name"]
|
||||||
|
|
||||||
repo = repo_mgr.get_repo(repo_name)
|
repo = repo_mgr.get_repo(repo_name)
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class RepoJSONMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self._info_file.open(encoding='utf-8') as f:
|
with self._info_file.open(encoding="utf-8") as f:
|
||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../downloader.py"]
|
||||||
'../downloader.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,16 +27,23 @@ class Repo(RepoJSONMixin):
|
|||||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
GIT_PULL = "git -C {path} pull -q --ff-only"
|
||||||
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status"
|
GIT_DIFF_FILE_STATUS = (
|
||||||
" {old_hash} {new_hash}")
|
"git -C {path} diff --no-commit-id --name-status" " {old_hash} {new_hash}"
|
||||||
GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.."
|
)
|
||||||
" {relative_file_path}")
|
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.." " {relative_file_path}"
|
||||||
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
||||||
|
|
||||||
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
||||||
|
|
||||||
def __init__(self, name: str, url: str, branch: str, folder_path: Path,
|
def __init__(
|
||||||
available_modules: Tuple[Installable]=(), loop: asyncio.AbstractEventLoop=None):
|
self,
|
||||||
|
name: str,
|
||||||
|
url: str,
|
||||||
|
branch: str,
|
||||||
|
folder_path: Path,
|
||||||
|
available_modules: Tuple[Installable] = (),
|
||||||
|
loop: asyncio.AbstractEventLoop = None,
|
||||||
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.branch = branch
|
self.branch = branch
|
||||||
|
|
||||||
@@ -71,11 +78,12 @@ class Repo(RepoJSONMixin):
|
|||||||
return poss_repo
|
return poss_repo
|
||||||
|
|
||||||
def _existing_git_repo(self) -> (bool, Path):
|
def _existing_git_repo(self) -> (bool, Path):
|
||||||
git_path = self.folder_path / '.git'
|
git_path = self.folder_path / ".git"
|
||||||
return git_path.exists(), git_path
|
return git_path.exists(), git_path
|
||||||
|
|
||||||
async def _get_file_update_statuses(
|
async def _get_file_update_statuses(
|
||||||
self, old_hash: str, new_hash: str) -> MutableMapping[str, str]:
|
self, old_hash: str, new_hash: str
|
||||||
|
) -> MutableMapping[str, str]:
|
||||||
"""
|
"""
|
||||||
Gets the file update status letters for each changed file between
|
Gets the file update status letters for each changed file between
|
||||||
the two hashes.
|
the two hashes.
|
||||||
@@ -85,29 +93,25 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_DIFF_FILE_STATUS.format(
|
self.GIT_DIFF_FILE_STATUS.format(
|
||||||
path=self.folder_path,
|
path=self.folder_path, old_hash=old_hash, new_hash=new_hash
|
||||||
old_hash=old_hash,
|
|
||||||
new_hash=new_hash
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitDiffError("Git diff failed for repo at path:"
|
raise GitDiffError("Git diff failed for repo at path:" " {}".format(self.folder_path))
|
||||||
" {}".format(self.folder_path))
|
|
||||||
|
|
||||||
stdout = p.stdout.strip().decode().split('\n')
|
stdout = p.stdout.strip().decode().split("\n")
|
||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
|
|
||||||
for filename in stdout:
|
for filename in stdout:
|
||||||
# TODO: filter these filenames by ones in self.available_modules
|
# TODO: filter these filenames by ones in self.available_modules
|
||||||
status, _, filepath = filename.partition('\t')
|
status, _, filepath = filename.partition("\t")
|
||||||
ret[filepath] = status
|
ret[filepath] = status
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def _get_commit_notes(self, old_commit_hash: str,
|
async def _get_commit_notes(self, old_commit_hash: str, relative_file_path: str) -> str:
|
||||||
relative_file_path: str) -> str:
|
|
||||||
"""
|
"""
|
||||||
Gets the commit notes from git log.
|
Gets the commit notes from git log.
|
||||||
:param old_commit_hash: Point in time to start getting messages
|
:param old_commit_hash: Point in time to start getting messages
|
||||||
@@ -119,13 +123,15 @@ class Repo(RepoJSONMixin):
|
|||||||
self.GIT_LOG.format(
|
self.GIT_LOG.format(
|
||||||
path=self.folder_path,
|
path=self.folder_path,
|
||||||
old_hash=old_commit_hash,
|
old_hash=old_commit_hash,
|
||||||
relative_file_path=relative_file_path
|
relative_file_path=relative_file_path,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitException("An exception occurred while executing git log on"
|
raise GitException(
|
||||||
" this repo: {}".format(self.folder_path))
|
"An exception occurred while executing git log on"
|
||||||
|
" this repo: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@@ -146,10 +152,11 @@ class Repo(RepoJSONMixin):
|
|||||||
Installable(location=name)
|
Installable(location=name)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path), ]):
|
for file_finder, name, is_pkg in pkgutil.walk_packages(
|
||||||
curr_modules.append(
|
path=[str(self.folder_path)], onerror=lambda name: None
|
||||||
Installable(location=self.folder_path / name)
|
):
|
||||||
)
|
if is_pkg:
|
||||||
|
curr_modules.append(Installable(location=self.folder_path / name))
|
||||||
self.available_modules = curr_modules
|
self.available_modules = curr_modules
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
@@ -157,12 +164,11 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
async def _run(self, *args, **kwargs):
|
async def _run(self, *args, **kwargs):
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['GIT_TERMINAL_PROMPT'] = '0'
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
kwargs['env'] = env
|
kwargs["env"] = env
|
||||||
async with self._repo_lock:
|
async with self._repo_lock:
|
||||||
return await self._loop.run_in_executor(
|
return await self._loop.run_in_executor(
|
||||||
self._executor,
|
self._executor, functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
|
||||||
functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def clone(self) -> Tuple[str]:
|
async def clone(self) -> Tuple[str]:
|
||||||
@@ -176,24 +182,17 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, path = self._existing_git_repo()
|
exists, path = self._existing_git_repo()
|
||||||
if exists:
|
if exists:
|
||||||
raise ExistingGitRepo(
|
raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||||
"A git repo already exists at path: {}".format(path)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.branch is not None:
|
if self.branch is not None:
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_CLONE.format(
|
self.GIT_CLONE.format(
|
||||||
branch=self.branch,
|
branch=self.branch, url=self.url, folder=self.folder_path
|
||||||
url=self.url,
|
|
||||||
folder=self.folder_path
|
|
||||||
).split()
|
).split()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_CLONE_NO_BRANCH.format(
|
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
|
||||||
url=self.url,
|
|
||||||
folder=self.folder_path
|
|
||||||
).split()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
@@ -217,23 +216,18 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
|
||||||
self.GIT_CURRENT_BRANCH.format(
|
|
||||||
path=self.folder_path
|
|
||||||
).split()
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitException("Could not determine current branch"
|
raise GitException(
|
||||||
" at path: {}".format(self.folder_path))
|
"Could not determine current branch" " at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
async def current_commit(self, branch: str=None) -> str:
|
async def current_commit(self, branch: str = None) -> str:
|
||||||
"""Determine the current commit hash of the repo.
|
"""Determine the current commit hash of the repo.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -252,15 +246,10 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_LATEST_COMMIT.format(
|
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
|
||||||
path=self.folder_path,
|
|
||||||
branch=branch
|
|
||||||
).split()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
@@ -268,7 +257,7 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
async def current_url(self, folder: Path=None) -> str:
|
async def current_url(self, folder: Path = None) -> str:
|
||||||
"""
|
"""
|
||||||
Discovers the FETCH URL for a Git repo.
|
Discovers the FETCH URL for a Git repo.
|
||||||
|
|
||||||
@@ -290,18 +279,14 @@ class Repo(RepoJSONMixin):
|
|||||||
if folder is None:
|
if folder is None:
|
||||||
folder = self.folder_path
|
folder = self.folder_path
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
|
||||||
Repo.GIT_DISCOVER_REMOTE_URL.format(
|
|
||||||
path=folder
|
|
||||||
).split()
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise RuntimeError("Unable to discover a repo URL.")
|
raise RuntimeError("Unable to discover a repo URL.")
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
async def hard_reset(self, branch: str=None) -> None:
|
async def hard_reset(self, branch: str = None) -> None:
|
||||||
"""Perform a hard reset on the current repo.
|
"""Perform a hard reset on the current repo.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -315,21 +300,18 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_HARD_RESET.format(
|
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
|
||||||
path=self.folder_path,
|
|
||||||
branch=branch
|
|
||||||
).split()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise HardResetError("Some error occurred when trying to"
|
raise HardResetError(
|
||||||
|
"Some error occurred when trying to"
|
||||||
" execute a hard reset on the repo at"
|
" execute a hard reset on the repo at"
|
||||||
" the following path: {}".format(self.folder_path))
|
" the following path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
async def update(self) -> (str, str):
|
async def update(self) -> (str, str):
|
||||||
"""Update the current branch of this repo.
|
"""Update the current branch of this repo.
|
||||||
@@ -345,15 +327,13 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
await self.hard_reset(branch=curr_branch)
|
await self.hard_reset(branch=curr_branch)
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
|
||||||
self.GIT_PULL.format(
|
|
||||||
path=self.folder_path
|
|
||||||
).split()
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise UpdateError("Git pull returned a non zero exit code"
|
raise UpdateError(
|
||||||
" for the repo located at path: {}".format(self.folder_path))
|
"Git pull returned a non zero exit code"
|
||||||
|
" for the repo located at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
new_commit = await self.current_commit(branch=curr_branch)
|
new_commit = await self.current_commit(branch=curr_branch)
|
||||||
|
|
||||||
@@ -389,7 +369,9 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
return await cog.copy_to(target_dir=target_dir)
|
return await cog.copy_to(target_dir=target_dir)
|
||||||
|
|
||||||
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool:
|
async def install_libraries(
|
||||||
|
self, target_dir: Path, libraries: Tuple[Installable] = ()
|
||||||
|
) -> bool:
|
||||||
"""Install shared libraries to the target directory.
|
"""Install shared libraries to the target directory.
|
||||||
|
|
||||||
If :code:`libraries` is not specified, all shared libraries in the repo
|
If :code:`libraries` is not specified, all shared libraries in the repo
|
||||||
@@ -469,16 +451,16 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.PIP_INSTALL.format(
|
self.PIP_INSTALL.format(
|
||||||
python=executable,
|
python=executable, target_dir=target_dir, reqs=" ".join(requirements)
|
||||||
target_dir=target_dir,
|
|
||||||
reqs=" ".join(requirements)
|
|
||||||
).split()
|
).split()
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
log.error("Something went wrong when installing"
|
log.error(
|
||||||
|
"Something went wrong when installing"
|
||||||
" the following requirements:"
|
" the following requirements:"
|
||||||
" {}".format(", ".join(requirements)))
|
" {}".format(", ".join(requirements))
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -490,8 +472,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(
|
return tuple(
|
||||||
[m for m in self.available_modules
|
[m for m in self.available_modules if m.type == InstallableType.COG and not m.hidden]
|
||||||
if m.type == InstallableType.COG and not m.hidden]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -501,8 +482,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(
|
return tuple(
|
||||||
[m for m in self.available_modules
|
[m for m in self.available_modules if m.type == InstallableType.SHARED_LIBRARY]
|
||||||
if m.type == InstallableType.SHARED_LIBRARY]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -515,8 +495,8 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
|
|
||||||
class RepoManager:
|
class RepoManager:
|
||||||
def __init__(self, downloader_config: Config):
|
|
||||||
self.downloader_config = downloader_config
|
def __init__(self):
|
||||||
|
|
||||||
self._repos = {}
|
self._repos = {}
|
||||||
|
|
||||||
@@ -526,7 +506,7 @@ class RepoManager:
|
|||||||
@property
|
@property
|
||||||
def repos_folder(self) -> Path:
|
def repos_folder(self) -> Path:
|
||||||
data_folder = data_manager.cog_data_path(self)
|
data_folder = data_manager.cog_data_path(self)
|
||||||
return data_folder / 'repos'
|
return data_folder / "repos"
|
||||||
|
|
||||||
def does_repo_exist(self, name: str) -> bool:
|
def does_repo_exist(self, name: str) -> bool:
|
||||||
return name in self._repos
|
return name in self._repos
|
||||||
@@ -537,7 +517,7 @@ class RepoManager:
|
|||||||
raise InvalidRepoName("Not a valid Python variable name.")
|
raise InvalidRepoName("Not a valid Python variable name.")
|
||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo:
|
async def add_repo(self, url: str, name: str, branch: str = "master") -> Repo:
|
||||||
"""Add and clone a git repository.
|
"""Add and clone a git repository.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -556,14 +536,12 @@ class RepoManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if self.does_repo_exist(name):
|
if self.does_repo_exist(name):
|
||||||
raise InvalidRepoName(
|
raise ExistingGitRepo(
|
||||||
"That repo name you provided already exists."
|
"That repo name you provided already exists. Please choose another."
|
||||||
" Please choose another."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
r = Repo(url=url, name=name, branch=branch,
|
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
|
||||||
folder_path=self.repos_folder / name)
|
|
||||||
await r.clone()
|
await r.clone()
|
||||||
|
|
||||||
self._repos[name] = r
|
self._repos[name] = r
|
||||||
|
|||||||
@@ -36,45 +36,45 @@ class SMReel(Enum):
|
|||||||
PAYOUTS = {
|
PAYOUTS = {
|
||||||
(SMReel.two, SMReel.two, SMReel.six): {
|
(SMReel.two, SMReel.two, SMReel.six): {
|
||||||
"payout": lambda x: x * 2500 + x,
|
"payout": lambda x: x * 2500 + x,
|
||||||
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!")
|
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!"),
|
||||||
},
|
},
|
||||||
(SMReel.flc, SMReel.flc, SMReel.flc): {
|
(SMReel.flc, SMReel.flc, SMReel.flc): {
|
||||||
"payout": lambda x: x + 1000,
|
"payout": lambda x: x + 1000,
|
||||||
"phrase": _("4LC! +1000!")
|
"phrase": _("4LC! +1000!"),
|
||||||
},
|
},
|
||||||
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
|
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
|
||||||
"payout": lambda x: x + 800,
|
"payout": lambda x: x + 800,
|
||||||
"phrase": _("Three cherries! +800!")
|
"phrase": _("Three cherries! +800!"),
|
||||||
},
|
},
|
||||||
(SMReel.two, SMReel.six): {
|
(SMReel.two, SMReel.six): {
|
||||||
"payout": lambda x: x * 4 + x,
|
"payout": lambda x: x * 4 + x,
|
||||||
"phrase": _("2 6! Your bid has been multiplied * 4!")
|
"phrase": _("2 6! Your bid has been multiplied * 4!"),
|
||||||
},
|
},
|
||||||
(SMReel.cherries, SMReel.cherries): {
|
(SMReel.cherries, SMReel.cherries): {
|
||||||
"payout": lambda x: x * 3 + x,
|
"payout": lambda x: x * 3 + x,
|
||||||
"phrase": _("Two cherries! Your bid has been multiplied * 3!")
|
"phrase": _("Two cherries! Your bid has been multiplied * 3!"),
|
||||||
},
|
|
||||||
"3 symbols": {
|
|
||||||
"payout": lambda x: x + 500,
|
|
||||||
"phrase": _("Three symbols! +500!")
|
|
||||||
},
|
},
|
||||||
|
"3 symbols": {"payout": lambda x: x + 500, "phrase": _("Three symbols! +500!")},
|
||||||
"2 symbols": {
|
"2 symbols": {
|
||||||
"payout": lambda x: x * 2 + x,
|
"payout": lambda x: x * 2 + x,
|
||||||
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!")
|
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
SLOT_PAYOUTS_MSG = _("Slot machine payouts:\n"
|
SLOT_PAYOUTS_MSG = _(
|
||||||
|
"Slot machine payouts:\n"
|
||||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||||
"{two.value} {six.value} Bet * 4\n"
|
"{two.value} {six.value} Bet * 4\n"
|
||||||
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
||||||
"Three symbols: +500\n"
|
"Three symbols: +500\n"
|
||||||
"Two symbols: Bet * 2").format(**SMReel.__dict__)
|
"Two symbols: Bet * 2"
|
||||||
|
).format(**SMReel.__dict__)
|
||||||
|
|
||||||
|
|
||||||
def guild_only_check():
|
def guild_only_check():
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
return True
|
return True
|
||||||
@@ -82,10 +82,12 @@ def guild_only_check():
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
class SetParser:
|
class SetParser:
|
||||||
|
|
||||||
def __init__(self, argument):
|
def __init__(self, argument):
|
||||||
allowed = ("+", "-")
|
allowed = ("+", "-")
|
||||||
self.sum = int(argument)
|
self.sum = int(argument)
|
||||||
@@ -115,19 +117,14 @@ class Economy:
|
|||||||
"SLOT_MIN": 5,
|
"SLOT_MIN": 5,
|
||||||
"SLOT_MAX": 100,
|
"SLOT_MAX": 100,
|
||||||
"SLOT_TIME": 0,
|
"SLOT_TIME": 0,
|
||||||
"REGISTER_CREDITS": 0
|
"REGISTER_CREDITS": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
default_global_settings = default_guild_settings
|
default_global_settings = default_guild_settings
|
||||||
|
|
||||||
default_member_settings = {
|
default_member_settings = {"next_payday": 0, "last_slot": 0}
|
||||||
"next_payday": 0,
|
|
||||||
"last_slot": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
default_role_settings = {
|
default_role_settings = {"PAYDAY_CREDITS": 0}
|
||||||
"PAYDAY_CREDITS": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
default_user_settings = default_member_settings
|
default_user_settings = default_member_settings
|
||||||
|
|
||||||
@@ -159,8 +156,7 @@ class Economy:
|
|||||||
bal = await bank.get_balance(user)
|
bal = await bank.get_balance(user)
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
await ctx.send(_("{}'s balance is {} {}").format(
|
await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
|
||||||
user.display_name, bal, currency))
|
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
||||||
@@ -171,11 +167,13 @@ class Economy:
|
|||||||
try:
|
try:
|
||||||
await bank.transfer_credits(from_, to, amount)
|
await bank.transfer_credits(from_, to, amount)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await ctx.send(str(e))
|
return await ctx.send(str(e))
|
||||||
|
|
||||||
await ctx.send(_("{} transferred {} {} to {}").format(
|
await ctx.send(
|
||||||
|
_("{} transferred {} {} to {}").format(
|
||||||
from_.display_name, amount, currency, to.display_name
|
from_.display_name, amount, currency, to.display_name
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@_bank.command(name="set")
|
@_bank.command(name="set")
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
@@ -193,19 +191,25 @@ class Economy:
|
|||||||
|
|
||||||
if creds.operation == "deposit":
|
if creds.operation == "deposit":
|
||||||
await bank.deposit_credits(to, creds.sum)
|
await bank.deposit_credits(to, creds.sum)
|
||||||
await ctx.send(_("{} added {} {} to {}'s account.").format(
|
await ctx.send(
|
||||||
|
_("{} added {} {} to {}'s account.").format(
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
author.display_name, creds.sum, currency, to.display_name
|
||||||
))
|
)
|
||||||
|
)
|
||||||
elif creds.operation == "withdraw":
|
elif creds.operation == "withdraw":
|
||||||
await bank.withdraw_credits(to, creds.sum)
|
await bank.withdraw_credits(to, creds.sum)
|
||||||
await ctx.send(_("{} removed {} {} from {}'s account.").format(
|
await ctx.send(
|
||||||
|
_("{} removed {} {} from {}'s account.").format(
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
author.display_name, creds.sum, currency, to.display_name
|
||||||
))
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await bank.set_balance(to, creds.sum)
|
await bank.set_balance(to, creds.sum)
|
||||||
await ctx.send(_("{} set {}'s account to {} {}.").format(
|
await ctx.send(
|
||||||
|
_("{} set {}'s account to {} {}.").format(
|
||||||
author.display_name, to.display_name, creds.sum, currency
|
author.display_name, to.display_name, creds.sum, currency
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@@ -214,16 +218,17 @@ class Economy:
|
|||||||
"""Deletes bank accounts"""
|
"""Deletes bank accounts"""
|
||||||
if confirmation is False:
|
if confirmation is False:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This will delete all bank accounts for {}.\nIf you're sure, type "
|
_(
|
||||||
"`{}bank reset yes`").format(
|
"This will delete all bank accounts for {}.\nIf you're sure, type "
|
||||||
self.bot.user.name if await bank.is_global() else "this server",
|
"`{}bank reset yes`"
|
||||||
ctx.prefix
|
).format(
|
||||||
|
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.wipe_bank()
|
await bank.wipe_bank()
|
||||||
await ctx.send(_("All bank accounts for {} have been "
|
await ctx.send(
|
||||||
"deleted.").format(
|
_("All bank accounts for {} have been " "deleted.").format(
|
||||||
self.bot.user.name if await bank.is_global() else "this server"
|
self.bot.user.name if await bank.is_global() else "this server"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -245,50 +250,65 @@ class Economy:
|
|||||||
await self.config.user(author).next_payday.set(next_payday)
|
await self.config.user(author).next_payday.set(next_payday)
|
||||||
|
|
||||||
pos = await bank.get_leaderboard_position(author)
|
pos = await bank.get_leaderboard_position(author)
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||||
"You currently have {3} {1}.\n\n"
|
"You currently have {3} {1}.\n\n"
|
||||||
"You are currently #{4} on the leaderboard!"
|
"You are currently #{4} on the leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
|
author,
|
||||||
str(await bank.get_balance(author)), pos
|
credits_name,
|
||||||
))
|
str(await self.config.PAYDAY_CREDITS()),
|
||||||
|
str(await bank.get_balance(author)),
|
||||||
|
pos,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to"
|
_("{} Too soon. For your next payday you have to" " wait {}.").format(
|
||||||
" wait {}.").format(author.mention, dtime)
|
author.mention, dtime
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
next_payday = await self.config.member(author).next_payday()
|
next_payday = await self.config.member(author).next_payday()
|
||||||
if cur_time >= next_payday:
|
if cur_time >= next_payday:
|
||||||
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||||
for role in author.roles:
|
for role in author.roles:
|
||||||
role_credits = await self.config.role(role).PAYDAY_CREDITS() # Nice variable name
|
role_credits = await self.config.role(
|
||||||
|
role
|
||||||
|
).PAYDAY_CREDITS() # Nice variable name
|
||||||
if role_credits > credit_amount:
|
if role_credits > credit_amount:
|
||||||
credit_amount = role_credits
|
credit_amount = role_credits
|
||||||
await bank.deposit_credits(author, credit_amount)
|
await bank.deposit_credits(author, credit_amount)
|
||||||
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
||||||
await self.config.member(author).next_payday.set(next_payday)
|
await self.config.member(author).next_payday.set(next_payday)
|
||||||
pos = await bank.get_leaderboard_position(author)
|
pos = await bank.get_leaderboard_position(author)
|
||||||
await ctx.send(_(
|
await ctx.send(
|
||||||
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||||
"You currently have {3} {1}.\n\n"
|
"You currently have {3} {1}.\n\n"
|
||||||
"You are currently #{4} on the leaderboard!"
|
"You are currently #{4} on the leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author, credits_name, credit_amount,
|
author,
|
||||||
str(await bank.get_balance(author)), pos
|
credits_name,
|
||||||
))
|
credit_amount,
|
||||||
|
str(await bank.get_balance(author)),
|
||||||
|
pos,
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to"
|
_("{} Too soon. For your next payday you have to" " wait {}.").format(
|
||||||
" wait {}.").format(author.mention, dtime))
|
author.mention, dtime
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool=False):
|
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
|
||||||
"""Prints out the leaderboard
|
"""Prints out the leaderboard
|
||||||
|
|
||||||
Defaults to top 10"""
|
Defaults to top 10"""
|
||||||
@@ -296,7 +316,9 @@ class Economy:
|
|||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if top < 1:
|
if top < 1:
|
||||||
top = 10
|
top = 10
|
||||||
if await bank.is_global() and show_global: # show_global is only applicable if bank is global
|
if (
|
||||||
|
await bank.is_global() and show_global
|
||||||
|
): # show_global is only applicable if bank is global
|
||||||
guild = None
|
guild = None
|
||||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||||
if len(bank_sorted) < top:
|
if len(bank_sorted) < top:
|
||||||
@@ -310,8 +332,12 @@ class Economy:
|
|||||||
balance = acc[1]["balance"]
|
balance = acc[1]["balance"]
|
||||||
balwidth = 2
|
balwidth = 2
|
||||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
||||||
pos=pos, poswidth=poswidth, name=name, namewidth=namewidth,
|
pos=pos,
|
||||||
balance=balance, balwidth=balwidth
|
poswidth=poswidth,
|
||||||
|
name=name,
|
||||||
|
namewidth=namewidth,
|
||||||
|
balance=balance,
|
||||||
|
balwidth=balwidth,
|
||||||
)
|
)
|
||||||
if highscore != "":
|
if highscore != "":
|
||||||
for page in pagify(highscore, shorten_by=12):
|
for page in pagify(highscore, shorten_by=12):
|
||||||
@@ -337,7 +363,11 @@ class Economy:
|
|||||||
slot_time = await self.config.SLOT_TIME()
|
slot_time = await self.config.SLOT_TIME()
|
||||||
last_slot = await self.config.user(author).last_slot()
|
last_slot = await self.config.user(author).last_slot()
|
||||||
else:
|
else:
|
||||||
valid_bid = await self.config.guild(guild).SLOT_MIN() <= bid <= await self.config.guild(guild).SLOT_MAX()
|
valid_bid = (
|
||||||
|
await self.config.guild(guild).SLOT_MIN()
|
||||||
|
<= bid
|
||||||
|
<= await self.config.guild(guild).SLOT_MAX()
|
||||||
|
)
|
||||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
slot_time = await self.config.guild(guild).SLOT_TIME()
|
||||||
last_slot = await self.config.member(author).last_slot()
|
last_slot = await self.config.member(author).last_slot()
|
||||||
now = calendar.timegm(ctx.message.created_at.utctimetuple())
|
now = calendar.timegm(ctx.message.created_at.utctimetuple())
|
||||||
@@ -364,9 +394,11 @@ class Economy:
|
|||||||
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
||||||
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
|
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
|
||||||
reels.append(new_reel) # for each reel
|
reels.append(new_reel) # for each reel
|
||||||
rows = ((reels[0][0], reels[1][0], reels[2][0]),
|
rows = (
|
||||||
|
(reels[0][0], reels[1][0], reels[2][0]),
|
||||||
(reels[0][1], reels[1][1], reels[2][1]),
|
(reels[0][1], reels[1][1], reels[2][1]),
|
||||||
(reels[0][2], reels[1][2], reels[2][2]))
|
(reels[0][2], reels[1][2], reels[2][2]),
|
||||||
|
)
|
||||||
|
|
||||||
slot = "~~\n~~" # Mobile friendly
|
slot = "~~\n~~" # Mobile friendly
|
||||||
for i, row in enumerate(rows): # Let's build the slot to show
|
for i, row in enumerate(rows): # Let's build the slot to show
|
||||||
@@ -378,8 +410,7 @@ class Economy:
|
|||||||
payout = PAYOUTS.get(rows[1])
|
payout = PAYOUTS.get(rows[1])
|
||||||
if not payout:
|
if not payout:
|
||||||
# Checks for two-consecutive-symbols special rewards
|
# Checks for two-consecutive-symbols special rewards
|
||||||
payout = PAYOUTS.get((rows[1][0], rows[1][1]),
|
payout = PAYOUTS.get((rows[1][0], rows[1][1]), PAYOUTS.get((rows[1][1], rows[1][2])))
|
||||||
PAYOUTS.get((rows[1][1], rows[1][2])))
|
|
||||||
if not payout:
|
if not payout:
|
||||||
# Still nothing. Let's check for 3 generic same symbols
|
# Still nothing. Let's check for 3 generic same symbols
|
||||||
# or 2 consecutive symbols
|
# or 2 consecutive symbols
|
||||||
@@ -395,15 +426,20 @@ class Economy:
|
|||||||
pay = payout["payout"](bid)
|
pay = payout["payout"](bid)
|
||||||
now = then - bid + pay
|
now = then - bid + pay
|
||||||
await bank.set_balance(author, now)
|
await bank.set_balance(author, now)
|
||||||
await channel.send(_("{}\n{} {}\n\nYour bid: {}\n{} → {}!"
|
await channel.send(
|
||||||
"").format(slot, author.mention,
|
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!" "").format(
|
||||||
payout["phrase"], bid, then, now))
|
slot, author.mention, payout["phrase"], bid, then, now
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
then = await bank.get_balance(author)
|
then = await bank.get_balance(author)
|
||||||
await bank.withdraw_credits(author, bid)
|
await bank.withdraw_credits(author, bid)
|
||||||
now = then - bid
|
now = then - bid
|
||||||
await channel.send(_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!"
|
await channel.send(
|
||||||
"").format(slot, author.mention, bid, then, now))
|
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!" "").format(
|
||||||
|
slot, author.mention, bid, then, now
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@@ -427,17 +463,18 @@ class Economy:
|
|||||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||||
register_amount = await bank.get_default_balance(guild)
|
register_amount = await bank.get_default_balance(guild)
|
||||||
msg = box(
|
msg = box(
|
||||||
_("Minimum slot bid: {}\n"
|
_(
|
||||||
|
"Minimum slot bid: {}\n"
|
||||||
"Maximum slot bid: {}\n"
|
"Maximum slot bid: {}\n"
|
||||||
"Slot cooldown: {}\n"
|
"Slot cooldown: {}\n"
|
||||||
"Payday amount: {}\n"
|
"Payday amount: {}\n"
|
||||||
"Payday cooldown: {}\n"
|
"Payday cooldown: {}\n"
|
||||||
"Amount given at account registration: {}"
|
"Amount given at account registration: {}"
|
||||||
"").format(
|
""
|
||||||
slot_min, slot_max, slot_time,
|
).format(
|
||||||
payday_amount, payday_time, register_amount
|
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
|
||||||
),
|
),
|
||||||
_("Current Economy settings:")
|
_("Current Economy settings:"),
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@@ -445,7 +482,7 @@ class Economy:
|
|||||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||||
"""Minimum slot machine bid"""
|
"""Minimum slot machine bid"""
|
||||||
if bid < 1:
|
if bid < 1:
|
||||||
await ctx.send(_('Invalid min bid amount.'))
|
await ctx.send(_("Invalid min bid amount."))
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
@@ -460,8 +497,7 @@ class Economy:
|
|||||||
"""Maximum slot machine bid"""
|
"""Maximum slot machine bid"""
|
||||||
slot_min = await self.config.SLOT_MIN()
|
slot_min = await self.config.SLOT_MIN()
|
||||||
if bid < 1 or bid < slot_min:
|
if bid < 1 or bid < slot_min:
|
||||||
await ctx.send(_('Invalid slotmax bid amount. Must be greater'
|
await ctx.send(_("Invalid slotmax bid amount. Must be greater" " than slotmin."))
|
||||||
' than slotmin.'))
|
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
@@ -489,8 +525,11 @@ class Economy:
|
|||||||
await self.config.PAYDAY_TIME.set(seconds)
|
await self.config.PAYDAY_TIME.set(seconds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||||
await ctx.send(_("Value modified. At least {} seconds must pass "
|
await ctx.send(
|
||||||
"between each payday.").format(seconds))
|
_("Value modified. At least {} seconds must pass " "between each payday.").format(
|
||||||
|
seconds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||||
@@ -504,8 +543,7 @@ class Economy:
|
|||||||
await self.config.PAYDAY_CREDITS.set(creds)
|
await self.config.PAYDAY_CREDITS.set(creds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(_("Every payday will now give {} {}."
|
await ctx.send(_("Every payday will now give {} {}." "").format(creds, credits_name))
|
||||||
"").format(creds, credits_name))
|
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||||
@@ -516,8 +554,11 @@ class Economy:
|
|||||||
await ctx.send("The bank must be per-server for per-role paydays to work.")
|
await ctx.send("The bank must be per-server for per-role paydays to work.")
|
||||||
else:
|
else:
|
||||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(_("Every payday will now give {} {} to people with the role {}."
|
await ctx.send(
|
||||||
"").format(creds, credits_name, role.name))
|
_("Every payday will now give {} {} to people with the role {}." "").format(
|
||||||
|
creds, credits_name, role.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def registeramount(self, ctx: commands.Context, creds: int):
|
async def registeramount(self, ctx: commands.Context, creds: int):
|
||||||
@@ -527,17 +568,18 @@ class Economy:
|
|||||||
creds = 0
|
creds = 0
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
await bank.set_default_balance(creds, guild)
|
await bank.set_default_balance(creds, guild)
|
||||||
await ctx.send(_("Registering an account will now give {} {}."
|
await ctx.send(
|
||||||
"").format(creds, credits_name))
|
_("Registering an account will now give {} {}." "").format(creds, credits_name)
|
||||||
|
)
|
||||||
|
|
||||||
# What would I ever do without stackoverflow?
|
# What would I ever do without stackoverflow?
|
||||||
def display_time(self, seconds, granularity=2):
|
def display_time(self, seconds, granularity=2):
|
||||||
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
||||||
(_('weeks'), 604800), # 60 * 60 * 24 * 7
|
(_("weeks"), 604800), # 60 * 60 * 24 * 7
|
||||||
(_('days'), 86400), # 60 * 60 * 24
|
(_("days"), 86400), # 60 * 60 * 24
|
||||||
(_('hours'), 3600), # 60 * 60
|
(_("hours"), 3600), # 60 * 60
|
||||||
(_('minutes'), 60),
|
(_("minutes"), 60),
|
||||||
(_('seconds'), 1),
|
(_("seconds"), 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
@@ -547,6 +589,6 @@ class Economy:
|
|||||||
if value:
|
if value:
|
||||||
seconds -= value * count
|
seconds -= value * count
|
||||||
if value == 1:
|
if value == 1:
|
||||||
name = name.rstrip('s')
|
name = name.rstrip("s")
|
||||||
result.append("{} {}".format(value, name))
|
result.append("{} {}".format(value, name))
|
||||||
return ', '.join(result[:granularity])
|
return ", ".join(result[:granularity])
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../economy.py"]
|
||||||
'../economy.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ class Filter:
|
|||||||
"filterban_count": 0,
|
"filterban_count": 0,
|
||||||
"filterban_time": 0,
|
"filterban_time": 0,
|
||||||
"filter_names": False,
|
"filter_names": False,
|
||||||
"filter_default_name": "John Doe"
|
"filter_default_name": "John Doe",
|
||||||
}
|
|
||||||
default_member_settings = {
|
|
||||||
"filter_count": 0,
|
|
||||||
"next_reset_time": 0
|
|
||||||
}
|
}
|
||||||
|
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
|
||||||
self.settings.register_guild(**default_guild_settings)
|
self.settings.register_guild(**default_guild_settings)
|
||||||
self.settings.register_member(**default_member_settings)
|
self.settings.register_member(**default_member_settings)
|
||||||
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
||||||
@@ -37,8 +34,7 @@ class Filter:
|
|||||||
async def register_filterban(self):
|
async def register_filterban(self):
|
||||||
try:
|
try:
|
||||||
await modlog.register_casetype(
|
await modlog.register_casetype(
|
||||||
"filterban", False, ":filing_cabinet: :hammer:",
|
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
|
||||||
"Filter ban", "ban"
|
|
||||||
)
|
)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
@@ -79,13 +75,12 @@ class Filter:
|
|||||||
word_list = []
|
word_list = []
|
||||||
tmp = ""
|
tmp = ""
|
||||||
for word in split_words:
|
for word in split_words:
|
||||||
if not word.startswith("\"")\
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
and not word.endswith("\"") and not tmp:
|
|
||||||
word_list.append(word)
|
word_list.append(word)
|
||||||
else:
|
else:
|
||||||
if word.startswith("\""):
|
if word.startswith('"'):
|
||||||
tmp += word[1:]
|
tmp += word[1:]
|
||||||
elif word.endswith("\""):
|
elif word.endswith('"'):
|
||||||
tmp += word[:-1]
|
tmp += word[:-1]
|
||||||
word_list.append(tmp)
|
word_list.append(tmp)
|
||||||
tmp = ""
|
tmp = ""
|
||||||
@@ -110,13 +105,12 @@ class Filter:
|
|||||||
word_list = []
|
word_list = []
|
||||||
tmp = ""
|
tmp = ""
|
||||||
for word in split_words:
|
for word in split_words:
|
||||||
if not word.startswith("\"")\
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
and not word.endswith("\"") and not tmp:
|
|
||||||
word_list.append(word)
|
word_list.append(word)
|
||||||
else:
|
else:
|
||||||
if word.startswith("\""):
|
if word.startswith('"'):
|
||||||
tmp += word[1:]
|
tmp += word[1:]
|
||||||
elif word.endswith("\""):
|
elif word.endswith('"'):
|
||||||
tmp += word[:-1]
|
tmp += word[:-1]
|
||||||
word_list.append(tmp)
|
word_list.append(tmp)
|
||||||
tmp = ""
|
tmp = ""
|
||||||
@@ -139,14 +133,10 @@ class Filter:
|
|||||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Names and nicknames will no longer be "
|
_("Names and nicknames will no longer be " "checked against the filter")
|
||||||
"checked against the filter")
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(_("Names and nicknames will now be checked against " "the filter"))
|
||||||
_("Names and nicknames will now be checked against "
|
|
||||||
"the filter")
|
|
||||||
)
|
|
||||||
|
|
||||||
@_filter.command(name="defaultname")
|
@_filter.command(name="defaultname")
|
||||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||||
@@ -160,15 +150,15 @@ class Filter:
|
|||||||
await ctx.send(_("The name to use on filtered names has been set"))
|
await ctx.send(_("The name to use on filtered names has been set"))
|
||||||
|
|
||||||
@_filter.command(name="ban")
|
@_filter.command(name="ban")
|
||||||
async def filter_ban(
|
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||||
self, ctx: commands.Context, count: int, timeframe: int):
|
|
||||||
"""
|
"""
|
||||||
Sets up an autoban if the specified number of messages are
|
Sets up an autoban if the specified number of messages are
|
||||||
filtered in the specified amount of time (in seconds)
|
filtered in the specified amount of time (in seconds)
|
||||||
"""
|
"""
|
||||||
if (count <= 0) != (timeframe <= 0):
|
if (count <= 0) != (timeframe <= 0):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Count and timeframe either both need to be 0 "
|
_(
|
||||||
|
"Count and timeframe either both need to be 0 "
|
||||||
"or both need to be greater than 0!"
|
"or both need to be greater than 0!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -213,9 +203,7 @@ class Filter:
|
|||||||
if filter_count > 0 and filter_time > 0:
|
if filter_count > 0 and filter_time > 0:
|
||||||
if message.created_at.timestamp() >= next_reset_time:
|
if message.created_at.timestamp() >= next_reset_time:
|
||||||
next_reset_time = message.created_at.timestamp() + filter_time
|
next_reset_time = message.created_at.timestamp() + filter_time
|
||||||
await self.settings.member(author).next_reset_time.set(
|
await self.settings.member(author).next_reset_time.set(next_reset_time)
|
||||||
next_reset_time
|
|
||||||
)
|
|
||||||
if user_count > 0:
|
if user_count > 0:
|
||||||
user_count = 0
|
user_count = 0
|
||||||
await self.settings.member(author).filter_count.set(user_count)
|
await self.settings.member(author).filter_count.set(user_count)
|
||||||
@@ -231,8 +219,10 @@ class Filter:
|
|||||||
if filter_count > 0 and filter_time > 0:
|
if filter_count > 0 and filter_time > 0:
|
||||||
user_count += 1
|
user_count += 1
|
||||||
await self.settings.member(author).filter_count.set(user_count)
|
await self.settings.member(author).filter_count.set(user_count)
|
||||||
if user_count >= filter_count and \
|
if (
|
||||||
message.created_at.timestamp() < next_reset_time:
|
user_count >= filter_count
|
||||||
|
and message.created_at.timestamp() < next_reset_time
|
||||||
|
):
|
||||||
reason = "Autoban (too many filtered messages)"
|
reason = "Autoban (too many filtered messages)"
|
||||||
try:
|
try:
|
||||||
await server.ban(author, reason=reason)
|
await server.ban(author, reason=reason)
|
||||||
@@ -240,8 +230,13 @@ class Filter:
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
await modlog.create_case(
|
await modlog.create_case(
|
||||||
self.bot, server, message.created_at,
|
self.bot,
|
||||||
"filterban", author, server.me, reason
|
server,
|
||||||
|
message.created_at,
|
||||||
|
"filterban",
|
||||||
|
author,
|
||||||
|
server.me,
|
||||||
|
reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
@@ -323,4 +318,3 @@ class Filter:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../filter.py"]
|
||||||
'../filter.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class RPS(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class RPSParser:
|
class RPSParser:
|
||||||
|
|
||||||
def __init__(self, argument):
|
def __init__(self, argument):
|
||||||
argument = argument.lower()
|
argument = argument.lower()
|
||||||
if argument == "rock":
|
if argument == "rock":
|
||||||
@@ -40,13 +41,26 @@ class General:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.stopwatches = {}
|
self.stopwatches = {}
|
||||||
self.ball = [
|
self.ball = [
|
||||||
_("As I see it, yes"), _("It is certain"), _("It is decidedly so"),
|
_("As I see it, yes"),
|
||||||
_("Most likely"), _("Outlook good"), _("Signs point to yes"),
|
_("It is certain"),
|
||||||
_("Without a doubt"), _("Yes"), _("Yes – definitely"), _("You may rely on it"),
|
_("It is decidedly so"),
|
||||||
_("Reply hazy, try again"), _("Ask again later"),
|
_("Most likely"),
|
||||||
_("Better not tell you now"), _("Cannot predict now"),
|
_("Outlook good"),
|
||||||
_("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"),
|
_("Signs point to yes"),
|
||||||
_("My sources say no"), _("Outlook not so good"), _("Very doubtful")
|
_("Without a doubt"),
|
||||||
|
_("Yes"),
|
||||||
|
_("Yes – definitely"),
|
||||||
|
_("You may rely on it"),
|
||||||
|
_("Reply hazy, try again"),
|
||||||
|
_("Ask again later"),
|
||||||
|
_("Better not tell you now"),
|
||||||
|
_("Cannot predict now"),
|
||||||
|
_("Concentrate and ask again"),
|
||||||
|
_("Don't count on it"),
|
||||||
|
_("My reply is no"),
|
||||||
|
_("My sources say no"),
|
||||||
|
_("Outlook not so good"),
|
||||||
|
_("Very doubtful"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@@ -57,12 +71,12 @@ class General:
|
|||||||
"""
|
"""
|
||||||
choices = [escape(c, mass_mentions=True) for c in choices]
|
choices = [escape(c, mass_mentions=True) for c in choices]
|
||||||
if len(choices) < 2:
|
if len(choices) < 2:
|
||||||
await ctx.send(_('Not enough choices to pick from.'))
|
await ctx.send(_("Not enough choices to pick from."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(choice(choices))
|
await ctx.send(choice(choices))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def roll(self, ctx, number : int = 100):
|
async def roll(self, ctx, number: int = 100):
|
||||||
"""Rolls random number (between 1 and user choice)
|
"""Rolls random number (between 1 and user choice)
|
||||||
|
|
||||||
Defaults to 100.
|
Defaults to 100.
|
||||||
@@ -70,14 +84,12 @@ class General:
|
|||||||
author = ctx.author
|
author = ctx.author
|
||||||
if number > 1:
|
if number > 1:
|
||||||
n = randint(1, number)
|
n = randint(1, number)
|
||||||
await ctx.send(
|
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
|
||||||
_("{} :game_die: {} :game_die:").format(author.mention, n)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
|
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def flip(self, ctx, user: discord.Member=None):
|
async def flip(self, ctx, user: discord.Member = None):
|
||||||
"""Flips a coin... or a user.
|
"""Flips a coin... or a user.
|
||||||
|
|
||||||
Defaults to coin.
|
Defaults to coin.
|
||||||
@@ -86,8 +98,7 @@ class General:
|
|||||||
msg = ""
|
msg = ""
|
||||||
if user.id == ctx.bot.user.id:
|
if user.id == ctx.bot.user.id:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
msg = _("Nice try. You think this is funny?\n"
|
msg = _("Nice try. You think this is funny?\n" "How about *this* instead:\n\n")
|
||||||
"How about *this* instead:\n\n")
|
|
||||||
char = "abcdefghijklmnopqrstuvwxyz"
|
char = "abcdefghijklmnopqrstuvwxyz"
|
||||||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||||||
table = str.maketrans(char, tran)
|
table = str.maketrans(char, tran)
|
||||||
@@ -98,23 +109,21 @@ class General:
|
|||||||
name = name.translate(table)
|
name = name.translate(table)
|
||||||
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
|
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")]))
|
||||||
_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def rps(self, ctx, your_choice : RPSParser):
|
async def rps(self, ctx, your_choice: RPSParser):
|
||||||
"""Play rock paper scissors"""
|
"""Play rock paper scissors"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
player_choice = your_choice.choice
|
player_choice = your_choice.choice
|
||||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||||
cond = {
|
cond = {
|
||||||
(RPS.rock, RPS.paper) : False,
|
(RPS.rock, RPS.paper): False,
|
||||||
(RPS.rock, RPS.scissors) : True,
|
(RPS.rock, RPS.scissors): True,
|
||||||
(RPS.paper, RPS.rock) : True,
|
(RPS.paper, RPS.rock): True,
|
||||||
(RPS.paper, RPS.scissors) : False,
|
(RPS.paper, RPS.scissors): False,
|
||||||
(RPS.scissors, RPS.rock) : False,
|
(RPS.scissors, RPS.rock): False,
|
||||||
(RPS.scissors, RPS.paper) : True
|
(RPS.scissors, RPS.paper): True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if red_choice == player_choice:
|
if red_choice == player_choice:
|
||||||
@@ -123,20 +132,14 @@ class General:
|
|||||||
outcome = cond[(player_choice, red_choice)]
|
outcome = cond[(player_choice, red_choice)]
|
||||||
|
|
||||||
if outcome is True:
|
if outcome is True:
|
||||||
await ctx.send(_("{} You win {}!").format(
|
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
|
||||||
red_choice.value, author.mention
|
|
||||||
))
|
|
||||||
elif outcome is False:
|
elif outcome is False:
|
||||||
await ctx.send(_("{} You lose {}!").format(
|
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
|
||||||
red_choice.value, author.mention
|
|
||||||
))
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("{} We're square {}!").format(
|
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
|
||||||
red_choice.value, author.mention
|
|
||||||
))
|
|
||||||
|
|
||||||
@commands.command(name="8", aliases=["8ball"])
|
@commands.command(name="8", aliases=["8ball"])
|
||||||
async def _8ball(self, ctx, *, question : str):
|
async def _8ball(self, ctx, *, question: str):
|
||||||
"""Ask 8 ball a question
|
"""Ask 8 ball a question
|
||||||
|
|
||||||
Question must end with a question mark.
|
Question must end with a question mark.
|
||||||
@@ -160,14 +163,14 @@ class General:
|
|||||||
self.stopwatches.pop(author.id, None)
|
self.stopwatches.pop(author.id, None)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def lmgtfy(self, ctx, *, search_terms : str):
|
async def lmgtfy(self, ctx, *, search_terms: str):
|
||||||
"""Creates a lmgtfy link"""
|
"""Creates a lmgtfy link"""
|
||||||
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
||||||
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
|
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
@commands.command(hidden=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def hug(self, ctx, user : discord.Member, intensity : int=1):
|
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
|
||||||
"""Because everyone likes hugs
|
"""Because everyone likes hugs
|
||||||
|
|
||||||
Up to 10 intensity levels."""
|
Up to 10 intensity levels."""
|
||||||
@@ -184,97 +187,30 @@ class General:
|
|||||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
async def userinfo(self, ctx, *, user: discord.Member=None):
|
|
||||||
"""Shows users's informations"""
|
|
||||||
author = ctx.author
|
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
user = author
|
|
||||||
|
|
||||||
# A special case for a special someone :^)
|
|
||||||
special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000)
|
|
||||||
is_special = (user.id == 96130341705637888 and
|
|
||||||
guild.id == 133049272517001216)
|
|
||||||
|
|
||||||
roles = sorted(user.roles)[1:]
|
|
||||||
|
|
||||||
joined_at = user.joined_at if not is_special else special_date
|
|
||||||
since_created = (ctx.message.created_at - user.created_at).days
|
|
||||||
since_joined = (ctx.message.created_at - joined_at).days
|
|
||||||
user_joined = joined_at.strftime("%d %b %Y %H:%M")
|
|
||||||
user_created = user.created_at.strftime("%d %b %Y %H:%M")
|
|
||||||
member_number = sorted(guild.members,
|
|
||||||
key=lambda m: m.joined_at).index(user) + 1
|
|
||||||
|
|
||||||
created_on = _("{}\n({} days ago)").format(user_created, since_created)
|
|
||||||
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
|
|
||||||
|
|
||||||
activity = _("Chilling in {} status").format(user.status)
|
|
||||||
if user.activity is None: # Default status
|
|
||||||
pass
|
|
||||||
elif user.activity.type == discord.ActivityType.playing:
|
|
||||||
activity = _("Playing {}").format(user.activity.name)
|
|
||||||
elif user.activity.type == discord.ActivityType.streaming:
|
|
||||||
activity = _("Streaming [{}]({})").format(user.activity.name, user.activity.url)
|
|
||||||
elif user.activity.type == discord.ActivityType.listening:
|
|
||||||
activity = _("Listening to {}").format(user.activity.name)
|
|
||||||
elif user.activity.type == discord.ActivityType.watching:
|
|
||||||
activity = _("Watching {}").format(user.activity.name)
|
|
||||||
|
|
||||||
if roles:
|
|
||||||
roles = ", ".join([x.name for x in roles])
|
|
||||||
else:
|
|
||||||
roles = _("None")
|
|
||||||
|
|
||||||
data = discord.Embed(description=activity, colour=user.colour)
|
|
||||||
data.add_field(name=_("Joined Discord on"), value=created_on)
|
|
||||||
data.add_field(name=_("Joined this server on"), value=joined_on)
|
|
||||||
data.add_field(name=_("Roles"), value=roles, inline=False)
|
|
||||||
data.set_footer(text=_("Member #{} | User ID: {}"
|
|
||||||
"").format(member_number, user.id))
|
|
||||||
|
|
||||||
name = str(user)
|
|
||||||
name = " ~ ".join((name, user.nick)) if user.nick else name
|
|
||||||
|
|
||||||
if user.avatar:
|
|
||||||
avatar = user.avatar_url
|
|
||||||
avatar = avatar.replace('webp', 'png')
|
|
||||||
data.set_author(name=name, url=avatar)
|
|
||||||
data.set_thumbnail(url=avatar)
|
|
||||||
else:
|
|
||||||
data.set_author(name=name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await ctx.send(embed=data)
|
|
||||||
except discord.HTTPException:
|
|
||||||
await ctx.send(_("I need the `Embed links` permission "
|
|
||||||
"to send this."))
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def serverinfo(self, ctx):
|
async def serverinfo(self, ctx):
|
||||||
"""Shows server's informations"""
|
"""Shows server's informations"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
online = len([m.status for m in guild.members
|
online = len(
|
||||||
if m.status == discord.Status.online or
|
[
|
||||||
m.status == discord.Status.idle])
|
m.status
|
||||||
|
for m in guild.members
|
||||||
|
if m.status == discord.Status.online or m.status == discord.Status.idle
|
||||||
|
]
|
||||||
|
)
|
||||||
total_users = len(guild.members)
|
total_users = len(guild.members)
|
||||||
text_channels = len(guild.text_channels)
|
text_channels = len(guild.text_channels)
|
||||||
voice_channels = len(guild.voice_channels)
|
voice_channels = len(guild.voice_channels)
|
||||||
passed = (ctx.message.created_at - guild.created_at).days
|
passed = (ctx.message.created_at - guild.created_at).days
|
||||||
created_at = (_("Since {}. That's over {} days ago!"
|
created_at = _("Since {}. That's over {} days ago!" "").format(
|
||||||
"").format(guild.created_at.strftime("%d %b %Y %H:%M"),
|
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
||||||
passed))
|
)
|
||||||
|
|
||||||
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
|
colour = "".join([choice("0123456789ABCDEF") for x in range(6)])
|
||||||
colour = randint(0, 0xFFFFFF)
|
colour = randint(0, 0xFFFFFF)
|
||||||
|
|
||||||
data = discord.Embed(
|
data = discord.Embed(description=created_at, colour=discord.Colour(value=colour))
|
||||||
description=created_at,
|
|
||||||
colour=discord.Colour(value=colour))
|
|
||||||
data.add_field(name=_("Region"), value=str(guild.region))
|
data.add_field(name=_("Region"), value=str(guild.region))
|
||||||
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
||||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
data.add_field(name=_("Text Channels"), value=text_channels)
|
||||||
@@ -292,16 +228,16 @@ class General:
|
|||||||
try:
|
try:
|
||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(_("I need the `Embed links` permission "
|
await ctx.send(_("I need the `Embed links` permission " "to send this."))
|
||||||
"to send this."))
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def urban(self, ctx, *, search_terms: str, definition_number: int=1):
|
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
||||||
"""Urban Dictionary search
|
"""Urban Dictionary search
|
||||||
|
|
||||||
Definition number must be between 1 and 10"""
|
Definition number must be between 1 and 10"""
|
||||||
|
|
||||||
def encode(s):
|
def encode(s):
|
||||||
return quote_plus(s, encoding='utf-8', errors='replace')
|
return quote_plus(s, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
# definition_number is just there to show up in the help
|
# definition_number is just there to show up in the help
|
||||||
# all this mess is to avoid forcing double quotes on the user
|
# all this mess is to avoid forcing double quotes on the user
|
||||||
@@ -326,18 +262,18 @@ class General:
|
|||||||
result = await r.json()
|
result = await r.json()
|
||||||
item_list = result["list"]
|
item_list = result["list"]
|
||||||
if item_list:
|
if item_list:
|
||||||
definition = item_list[pos]['definition']
|
definition = item_list[pos]["definition"]
|
||||||
example = item_list[pos]['example']
|
example = item_list[pos]["example"]
|
||||||
defs = len(item_list)
|
defs = len(item_list)
|
||||||
msg = ("**Definition #{} out of {}:\n**{}\n\n"
|
msg = "**Definition #{} out of {}:\n**{}\n\n" "**Example:\n**{}".format(
|
||||||
"**Example:\n**{}".format(pos+1, defs, definition,
|
pos + 1, defs, definition, example
|
||||||
example))
|
)
|
||||||
msg = pagify(msg, ["\n"])
|
msg = pagify(msg, ["\n"])
|
||||||
for page in msg:
|
for page in msg:
|
||||||
await ctx.send(page)
|
await ctx.send(page)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Your search terms gave no results."))
|
await ctx.send(_("Your search terms gave no results."))
|
||||||
except IndexError:
|
except IndexError:
|
||||||
await ctx.send(_("There is no definition #{}").format(pos+1))
|
await ctx.send(_("There is no definition #{}").format(pos + 1))
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("Error."))
|
await ctx.send(_("Error."))
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../general.py"]
|
||||||
'../general.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
|||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Image:
|
class Image:
|
||||||
"""Image related commands."""
|
"""Image related commands."""
|
||||||
default_global = {
|
default_global = {"imgur_client_id": None}
|
||||||
"imgur_client_id": None
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -45,7 +43,9 @@ class Image:
|
|||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}").format(
|
_("A client ID has not been set! Please set one with {}").format(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)))
|
"`{}imgurcreds`".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
||||||
async with self.session.get(url, headers=headers, params=params) as search_get:
|
async with self.session.get(url, headers=headers, params=params) as search_get:
|
||||||
@@ -66,7 +66,9 @@ class Image:
|
|||||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||||
|
|
||||||
@_imgur.command(name="subreddit")
|
@_imgur.command(name="subreddit")
|
||||||
async def imgur_subreddit(self, ctx, subreddit: str, sort_type: str="top", window: str="day"):
|
async def imgur_subreddit(
|
||||||
|
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
|
||||||
|
):
|
||||||
"""Gets images from the specified subreddit section
|
"""Gets images from the specified subreddit section
|
||||||
|
|
||||||
Sort types: new, top
|
Sort types: new, top
|
||||||
@@ -90,7 +92,9 @@ class Image:
|
|||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}").format(
|
_("A client ID has not been set! Please set one with {}").format(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)))
|
"`{}imgurcreds`".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
@@ -139,8 +143,9 @@ class Image:
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
|
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}" "".format(
|
||||||
"".format(GIPHY_API_KEY, keywords))
|
GIPHY_API_KEY, keywords
|
||||||
|
)
|
||||||
|
|
||||||
async with self.session.get(url) as r:
|
async with self.session.get(url) as r:
|
||||||
result = await r.json()
|
result = await r.json()
|
||||||
@@ -161,8 +166,9 @@ class Image:
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
|
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}" "".format(
|
||||||
"".format(GIPHY_API_KEY, keywords))
|
GIPHY_API_KEY, keywords
|
||||||
|
)
|
||||||
|
|
||||||
async with self.session.get(url) as r:
|
async with self.session.get(url) as r:
|
||||||
result = await r.json()
|
result = await r.json()
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../image.py"]
|
||||||
'../image.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import discord
|
|||||||
|
|
||||||
|
|
||||||
def mod_or_voice_permissions(**perms):
|
def mod_or_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -18,15 +19,19 @@ def mod_or_voice_permissions(**perms):
|
|||||||
|
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
resolved = vc.permissions_for(author)
|
resolved = vc.permissions_for(author)
|
||||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
good = resolved.administrator or all(
|
||||||
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
|
)
|
||||||
if not good:
|
if not good:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
def admin_or_voice_permissions(**perms):
|
def admin_or_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -37,22 +42,29 @@ def admin_or_voice_permissions(**perms):
|
|||||||
return True
|
return True
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
resolved = vc.permissions_for(author)
|
resolved = vc.permissions_for(author)
|
||||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
good = resolved.administrator or all(
|
||||||
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
|
)
|
||||||
if not good:
|
if not good:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
def bot_has_voice_permissions(**perms):
|
def bot_has_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
resolved = vc.permissions_for(guild.me)
|
resolved = vc.permissions_for(guild.me)
|
||||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
good = resolved.administrator or all(
|
||||||
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
|
)
|
||||||
if not good:
|
if not good:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../mod.py"]
|
||||||
'../mod.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../modlog.py"]
|
||||||
'../modlog.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
|
||||||
_ = Translator('ModLog', __file__)
|
_ = Translator("ModLog", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@@ -32,15 +32,12 @@ class ModLog:
|
|||||||
if channel:
|
if channel:
|
||||||
if channel.permissions_for(guild.me).send_messages:
|
if channel.permissions_for(guild.me).send_messages:
|
||||||
await modlog.set_modlog_channel(guild, channel)
|
await modlog.set_modlog_channel(guild, channel)
|
||||||
await ctx.send(
|
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
||||||
_("Mod events will be sent to {}").format(
|
|
||||||
channel.mention
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I do not have permissions to "
|
_("I do not have permissions to " "send messages in {}!").format(
|
||||||
"send messages in {}!").format(channel.mention)
|
channel.mention
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -51,7 +48,7 @@ class ModLog:
|
|||||||
await modlog.set_modlog_channel(guild, None)
|
await modlog.set_modlog_channel(guild, None)
|
||||||
await ctx.send(_("Mod log deactivated."))
|
await ctx.send(_("Mod log deactivated."))
|
||||||
|
|
||||||
@modlogset.command(name='cases')
|
@modlogset.command(name="cases")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def set_cases(self, ctx: commands.Context, action: str = None):
|
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||||
"""Enables or disables case creation for each type of mod action"""
|
"""Enables or disables case creation for each type of mod action"""
|
||||||
@@ -64,8 +61,8 @@ class ModLog:
|
|||||||
msg = ""
|
msg = ""
|
||||||
for ct in casetypes:
|
for ct in casetypes:
|
||||||
enabled = await ct.is_enabled()
|
enabled = await ct.is_enabled()
|
||||||
value = 'enabled' if enabled else 'disabled'
|
value = "enabled" if enabled else "disabled"
|
||||||
msg += '%s : %s\n' % (ct.name, value)
|
msg += "%s : %s\n" % (ct.name, value)
|
||||||
|
|
||||||
msg = title + "\n" + box(msg)
|
msg = title + "\n" + box(msg)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
@@ -78,10 +75,8 @@ class ModLog:
|
|||||||
enabled = await casetype.is_enabled()
|
enabled = await casetype.is_enabled()
|
||||||
await casetype.set_enabled(True if not enabled else False)
|
await casetype.set_enabled(True if not enabled else False)
|
||||||
|
|
||||||
msg = (
|
msg = _("Case creation for {} actions is now {}.").format(
|
||||||
_('Case creation for {} actions is now {}.').format(
|
action, "enabled" if not enabled else "disabled"
|
||||||
action, 'enabled' if not enabled else 'disabled'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@@ -133,8 +128,10 @@ class ModLog:
|
|||||||
if audit_type:
|
if audit_type:
|
||||||
audit_case = None
|
audit_case = None
|
||||||
async for entry in guild.audit_logs(action=audit_type):
|
async for entry in guild.audit_logs(action=audit_type):
|
||||||
if entry.target.id == case_before.user.id and \
|
if (
|
||||||
entry.action == audit_type:
|
entry.target.id == case_before.user.id
|
||||||
|
and entry.action == audit_type
|
||||||
|
):
|
||||||
audit_case = entry
|
audit_case = entry
|
||||||
break
|
break
|
||||||
if audit_case:
|
if audit_case:
|
||||||
@@ -145,11 +142,9 @@ class ModLog:
|
|||||||
if not (is_guild_owner or is_case_author or author_is_mod):
|
if not (is_guild_owner or is_case_author or author_is_mod):
|
||||||
await ctx.send(_("You are not authorized to modify that case!"))
|
await ctx.send(_("You are not authorized to modify that case!"))
|
||||||
return
|
return
|
||||||
to_modify = {
|
to_modify = {"reason": reason}
|
||||||
"reason": reason,
|
|
||||||
}
|
|
||||||
if case_before.moderator != author:
|
if case_before.moderator != author:
|
||||||
to_modify["amended_by"] = author
|
to_modify["amended_by"] = author
|
||||||
to_modify["modified_at"] = ctx.message.created_at.timestamp()
|
to_modify["modified_at"] = ctx.message.created_at.timestamp()
|
||||||
await case_before.edit(self.bot, to_modify)
|
await case_before.edit(to_modify)
|
||||||
await ctx.send(_("Reason has been updated."))
|
await ctx.send(_("Reason has been updated."))
|
||||||
|
|||||||
5
redbot/cogs/permissions/__init__.py
Normal file
5
redbot/cogs/permissions/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .permissions import Permissions
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Permissions(bot))
|
||||||
26
redbot/cogs/permissions/converters.py
Normal file
26
redbot/cogs/permissions/converters.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from redbot.core import commands
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class CogOrCommand(commands.Converter):
|
||||||
|
|
||||||
|
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||||
|
ret = ctx.bot.get_cog(arg)
|
||||||
|
if ret:
|
||||||
|
return "cogs", ret.__class__.__name__
|
||||||
|
ret = ctx.bot.get_command(arg)
|
||||||
|
if ret:
|
||||||
|
return "commands", ret.qualified_name
|
||||||
|
|
||||||
|
raise commands.BadArgument()
|
||||||
|
|
||||||
|
|
||||||
|
class RuleType(commands.Converter):
|
||||||
|
|
||||||
|
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||||
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
|
return "allow"
|
||||||
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
|
return "deny"
|
||||||
|
|
||||||
|
raise commands.BadArgument()
|
||||||
653
redbot/cogs/permissions/permissions.py
Normal file
653
redbot/cogs/permissions/permissions.py
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
from copy import copy
|
||||||
|
import contextlib
|
||||||
|
import asyncio
|
||||||
|
import discord
|
||||||
|
from redbot.core import commands
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core import checks
|
||||||
|
from redbot.core.config import Config
|
||||||
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
|
||||||
|
from .resolvers import val_if_check_is_valid, resolve_models
|
||||||
|
from .yaml_handler import yamlset_acl, yamlget_acl
|
||||||
|
from .converters import CogOrCommand, RuleType
|
||||||
|
|
||||||
|
_models = ["owner", "guildowner", "admin", "mod"]
|
||||||
|
|
||||||
|
_ = Translator("Permissions", __file__)
|
||||||
|
|
||||||
|
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
||||||
|
|
||||||
|
|
||||||
|
@cog_i18n(_)
|
||||||
|
class Permissions:
|
||||||
|
"""
|
||||||
|
A high level permission model
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Not sure if we will use admin or mod models in core red
|
||||||
|
# but they are explicitly supported
|
||||||
|
resolution_order = {k: _models[:i] for i, k in enumerate(_models, 1)}
|
||||||
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||||
|
self._before = []
|
||||||
|
self._after = []
|
||||||
|
self.config.register_global(owner_models={})
|
||||||
|
self.config.register_guild(owner_models={})
|
||||||
|
|
||||||
|
def add_check(self, check_obj: object, before_or_after: str):
|
||||||
|
"""
|
||||||
|
adds a check to the check ordering
|
||||||
|
|
||||||
|
checks should be a function taking 2 arguments:
|
||||||
|
ctx: commands.Context
|
||||||
|
level: str
|
||||||
|
|
||||||
|
and returning:
|
||||||
|
None: do not interfere
|
||||||
|
True: command should be allowed even if they dont
|
||||||
|
have role or perm requirements for the check
|
||||||
|
False: command should be blocked
|
||||||
|
|
||||||
|
before_or_after:
|
||||||
|
Should literally be a str equaling 'before' or 'after'
|
||||||
|
This should be based on if this should take priority
|
||||||
|
over set rules or not
|
||||||
|
|
||||||
|
3rd party cogs adding checks using this should only allow
|
||||||
|
the owner to add checks before, and ensure only the owner
|
||||||
|
can add checks recieving the level 'owner'
|
||||||
|
|
||||||
|
3rd party cogs should keep a copy of of any checks they registered
|
||||||
|
and deregister then on unload
|
||||||
|
"""
|
||||||
|
|
||||||
|
if before_or_after == "before":
|
||||||
|
self._before.append(check_obj)
|
||||||
|
elif before_or_after == "after":
|
||||||
|
self._after.append(check_obj)
|
||||||
|
else:
|
||||||
|
raise TypeError("RTFM")
|
||||||
|
|
||||||
|
def remove_check(self, check_obj: object, before_or_after: str):
|
||||||
|
"""
|
||||||
|
removes a previously registered check object
|
||||||
|
|
||||||
|
3rd party cogs should keep a copy of of any checks they registered
|
||||||
|
and deregister then on unload
|
||||||
|
"""
|
||||||
|
|
||||||
|
if before_or_after == "before":
|
||||||
|
self._before.remove(check_obj)
|
||||||
|
elif before_or_after == "after":
|
||||||
|
self._after.remove(check_obj)
|
||||||
|
else:
|
||||||
|
raise TypeError("RTFM")
|
||||||
|
|
||||||
|
async def __global_check(self, ctx):
|
||||||
|
"""
|
||||||
|
Yes, this is needed on top of hooking into checks.py
|
||||||
|
to ensure that unchecked commands can still be managed by permissions
|
||||||
|
This should return True in the case of no overrides
|
||||||
|
defering to check logic
|
||||||
|
This works since all checks must be True to run
|
||||||
|
"""
|
||||||
|
v = await self.check_overrides(ctx, "mod")
|
||||||
|
|
||||||
|
if v is False:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def check_overrides(self, ctx: commands.Context, level: str) -> bool:
|
||||||
|
"""
|
||||||
|
This checks for any overrides in the permission model
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx: `redbot.core.context.commands.Context`
|
||||||
|
The context of the command
|
||||||
|
level: `str`
|
||||||
|
One of 'owner', 'guildowner', 'admin', 'mod'
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
a trinary value using None + bool to resolve permissions for
|
||||||
|
checks.py
|
||||||
|
"""
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
return True
|
||||||
|
voice_channel = None
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
voice_channel = ctx.author.voice.voice_channel
|
||||||
|
entries = [x for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||||
|
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||||
|
entries.extend([x.id for x in roles])
|
||||||
|
|
||||||
|
for check in self._before:
|
||||||
|
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||||
|
if override is not None:
|
||||||
|
return override
|
||||||
|
|
||||||
|
for model in self.resolution_order[level]:
|
||||||
|
override_model = getattr(self, model + "_model", None)
|
||||||
|
override = await override_model(ctx) if override_model else None
|
||||||
|
if override is not None:
|
||||||
|
return override
|
||||||
|
|
||||||
|
for check in self._after:
|
||||||
|
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||||
|
if override is not None:
|
||||||
|
return override
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def owner_model(self, ctx: commands.Context) -> bool:
|
||||||
|
"""
|
||||||
|
Handles owner level overrides
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with self.config.owner_models() as models:
|
||||||
|
return resolve_models(ctx=ctx, models=models)
|
||||||
|
|
||||||
|
async def guildowner_model(self, ctx: commands.Context) -> bool:
|
||||||
|
"""
|
||||||
|
Handles guild level overrides
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
|
return resolve_models(ctx=ctx, models=models)
|
||||||
|
|
||||||
|
# Either of the below function signatures could be used
|
||||||
|
# without any other modifications required at a later date
|
||||||
|
#
|
||||||
|
# async def admin_model(self, ctx: commands.Context) -> bool:
|
||||||
|
# async def mod_model(self, ctx: commands.Context) -> bool:
|
||||||
|
|
||||||
|
@commands.group(aliases=["p"])
|
||||||
|
async def permissions(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Permission management tools
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send_help()
|
||||||
|
|
||||||
|
@permissions.command()
|
||||||
|
async def explain(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Provides a detailed explanation of how the permission model functions
|
||||||
|
"""
|
||||||
|
# Apologies in advance for the translators out there...
|
||||||
|
|
||||||
|
message = _(
|
||||||
|
"This cog extends the default permission model of the bot. "
|
||||||
|
"By default, many commands are restricted based on what "
|
||||||
|
"the command can do."
|
||||||
|
"\n"
|
||||||
|
"Any command that could impact the host machine, "
|
||||||
|
"is generally owner only."
|
||||||
|
"\n"
|
||||||
|
"Commands that take administrative or moderator "
|
||||||
|
"actions in servers generally require a mod or an admin."
|
||||||
|
"\n"
|
||||||
|
"This cog allows you to refine some of those settings. "
|
||||||
|
"You can allow wider or narrower "
|
||||||
|
"access to most commands using it."
|
||||||
|
"\n\n"
|
||||||
|
"When additional rules are set using this cog, "
|
||||||
|
"those rules will be checked prior to "
|
||||||
|
"checking for the default restrictions of the command. "
|
||||||
|
"\n"
|
||||||
|
"Rules set globally (by the owner) are checked first, "
|
||||||
|
"then rules set for guilds. If multiple global or guild "
|
||||||
|
"rules apply to the case, the order they are checked is:"
|
||||||
|
"\n"
|
||||||
|
"1. Rules about a user.\n"
|
||||||
|
"2. Rules about the voice channel a user is in.\n"
|
||||||
|
"3. Rules about the text channel a command was issued in\n"
|
||||||
|
"4. Rules about a role the user has "
|
||||||
|
"(The highest role they have with a rule will be used)\n"
|
||||||
|
"5. Rules about the guild a user is in (Owner level only)"
|
||||||
|
"\n\nFor more details, please read the official documentation."
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
|
@permissions.command(name="canrun")
|
||||||
|
async def _test_permission_model(
|
||||||
|
self, ctx: commands.Context, user: discord.Member, *, command: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This checks if someone can run a command in the current location
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not command:
|
||||||
|
return await ctx.send_help()
|
||||||
|
|
||||||
|
message = copy(ctx.message)
|
||||||
|
message.author = user
|
||||||
|
message.content = "{}{}".format(ctx.prefix, command)
|
||||||
|
|
||||||
|
com = self.bot.get_command(command)
|
||||||
|
if com is None:
|
||||||
|
out = _("No such command")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
testcontext = await self.bot.get_context(message, cls=commands.Context)
|
||||||
|
can = await com.can_run(testcontext)
|
||||||
|
except commands.CheckFailure:
|
||||||
|
can = False
|
||||||
|
|
||||||
|
out = (
|
||||||
|
_("That user can run the specified command.")
|
||||||
|
if can
|
||||||
|
else _("That user can not run the specified command.")
|
||||||
|
)
|
||||||
|
await ctx.send(out)
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="setglobalacl")
|
||||||
|
async def owner_set_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Take a YAML file upload to set permissions from
|
||||||
|
"""
|
||||||
|
if not ctx.message.attachments:
|
||||||
|
return await ctx.send(_("You must upload a file"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return await ctx.send(_("Inalid syntax"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Rules set."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="getglobalacl")
|
||||||
|
async def owner_get_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Dumps a YAML file with the current owner level permissions
|
||||||
|
"""
|
||||||
|
await yamlget_acl(ctx, config=self.config.owner_models)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="setguildacl")
|
||||||
|
async def guild_set_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Take a YAML file upload to set permissions from
|
||||||
|
"""
|
||||||
|
if not ctx.message.attachments:
|
||||||
|
return await ctx.send(_("You must upload a file"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return await ctx.send(_("Inalid syntax"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Rules set."))
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="getguildacl")
|
||||||
|
async def guild_get_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Dumps a YAML file with the current owner level permissions
|
||||||
|
"""
|
||||||
|
await yamlget_acl(ctx, config=self.config.guild(ctx.guild).owner_models)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="updateguildacl")
|
||||||
|
async def guild_update_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Take a YAML file upload to update permissions from
|
||||||
|
|
||||||
|
Use this to not lose existing rules
|
||||||
|
"""
|
||||||
|
if not ctx.message.attachments:
|
||||||
|
return await ctx.send(_("You must upload a file"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return await ctx.send(_("Inalid syntax"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Rules set."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="updateglobalacl")
|
||||||
|
async def owner_update_acl(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Take a YAML file upload to update permissions from
|
||||||
|
|
||||||
|
Use this to not lose existing rules
|
||||||
|
"""
|
||||||
|
if not ctx.message.attachments:
|
||||||
|
return await ctx.send(_("You must upload a file"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return await ctx.send(_("Inalid syntax"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Rules set."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="addglobalrule")
|
||||||
|
async def add_to_global_rule(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
allow_or_deny: RuleType,
|
||||||
|
cog_or_command: CogOrCommand,
|
||||||
|
who_or_what: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
adds something to the rules
|
||||||
|
|
||||||
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
|
cog_or_command: case sensitive cog or command name
|
||||||
|
nested commands should be space seperated, but enclosed in quotes
|
||||||
|
|
||||||
|
who_or_what: what to add to the rule list.
|
||||||
|
For best results, use an ID or mention
|
||||||
|
The bot will try to uniquely match even without,
|
||||||
|
but a failure to do so will raise an error
|
||||||
|
This can be a user, role, channel, or guild
|
||||||
|
"""
|
||||||
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
|
if not obj:
|
||||||
|
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
if allow_or_deny not in data[model_type][type_name]:
|
||||||
|
data[model_type][type_name][allow_or_deny] = []
|
||||||
|
|
||||||
|
if obj in data[model_type][type_name][allow_or_deny]:
|
||||||
|
return await ctx.send(_("That rule already exists."))
|
||||||
|
|
||||||
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Rule added."))
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="addguildrule")
|
||||||
|
async def add_to_guild_rule(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
allow_or_deny: RuleType,
|
||||||
|
cog_or_command: CogOrCommand,
|
||||||
|
who_or_what: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
adds something to the rules
|
||||||
|
|
||||||
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
|
cog_or_command: case sensitive cog or command name
|
||||||
|
nested commands should be space seperated, but enclosed in quotes
|
||||||
|
|
||||||
|
who_or_what: what to add to the rule list.
|
||||||
|
For best results, use an ID or mention
|
||||||
|
The bot will try to uniquely match even without,
|
||||||
|
but a failure to do so will raise an error
|
||||||
|
This can be a user, role, channel, or guild
|
||||||
|
"""
|
||||||
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
|
if not obj:
|
||||||
|
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
if allow_or_deny not in data[model_type][type_name]:
|
||||||
|
data[model_type][type_name][allow_or_deny] = []
|
||||||
|
|
||||||
|
if obj in data[model_type][type_name][allow_or_deny]:
|
||||||
|
return await ctx.send(_("That rule already exists."))
|
||||||
|
|
||||||
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Rule added."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="removeglobalrule")
|
||||||
|
async def rem_from_global_rule(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
allow_or_deny: RuleType,
|
||||||
|
cog_or_command: CogOrCommand,
|
||||||
|
who_or_what: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
removes something from the rules
|
||||||
|
|
||||||
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
|
cog_or_command: case sensitive cog or command name
|
||||||
|
nested commands should be space seperated, but enclosed in quotes
|
||||||
|
|
||||||
|
who_or_what: what to add to the rule list.
|
||||||
|
For best results, use an ID or mention
|
||||||
|
The bot will try to uniquely match even without,
|
||||||
|
but a failure to do so will raise an error
|
||||||
|
This can be a user, role, channel, or guild
|
||||||
|
"""
|
||||||
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
|
if not obj:
|
||||||
|
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
if allow_or_deny not in data[model_type][type_name]:
|
||||||
|
data[model_type][type_name][allow_or_deny] = []
|
||||||
|
|
||||||
|
if obj not in data[model_type][type_name][allow_or_deny]:
|
||||||
|
return await ctx.send(_("That rule doesn't exist."))
|
||||||
|
|
||||||
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Rule removed."))
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="removeguildrule")
|
||||||
|
async def rem_from_guild_rule(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
allow_or_deny: RuleType,
|
||||||
|
cog_or_command: CogOrCommand,
|
||||||
|
who_or_what: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
removes something from the rules
|
||||||
|
|
||||||
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
|
cog_or_command: case sensitive cog or command name
|
||||||
|
nested commands should be space seperated, but enclosed in quotes
|
||||||
|
|
||||||
|
who_or_what: what to add to the rule list.
|
||||||
|
For best results, use an ID or mention
|
||||||
|
The bot will try to uniquely match even without,
|
||||||
|
but a failure to do so will raise an error
|
||||||
|
This can be a user, role, channel, or guild
|
||||||
|
"""
|
||||||
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
|
if not obj:
|
||||||
|
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
if allow_or_deny not in data[model_type][type_name]:
|
||||||
|
data[model_type][type_name][allow_or_deny] = []
|
||||||
|
|
||||||
|
if obj not in data[model_type][type_name][allow_or_deny]:
|
||||||
|
return await ctx.send(_("That rule doesn't exist."))
|
||||||
|
|
||||||
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Rule removed."))
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="setdefaultguildrule")
|
||||||
|
async def set_default_guild_rule(
|
||||||
|
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
|
Use with a cog or command and no setting to clear the default and defer to
|
||||||
|
normal check logic
|
||||||
|
"""
|
||||||
|
if allow_or_deny:
|
||||||
|
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
||||||
|
else:
|
||||||
|
val_to_set = None
|
||||||
|
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
|
||||||
|
data[model_type][type_name]["default"] = val_to_set
|
||||||
|
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Defualt set."))
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="setdefaultglobalrule")
|
||||||
|
async def set_default_global_rule(
|
||||||
|
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
|
Use with a cog or command and no setting to clear the default and defer to
|
||||||
|
normal check logic
|
||||||
|
"""
|
||||||
|
|
||||||
|
if allow_or_deny:
|
||||||
|
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
||||||
|
else:
|
||||||
|
val_to_set = None
|
||||||
|
|
||||||
|
model_type, type_name = cog_or_command
|
||||||
|
async with self.config.owner_models() as models:
|
||||||
|
data = {k: v for k, v in models.items()}
|
||||||
|
if model_type not in data:
|
||||||
|
data[model_type] = {}
|
||||||
|
if type_name not in data[model_type]:
|
||||||
|
data[model_type][type_name] = {}
|
||||||
|
|
||||||
|
data[model_type][type_name]["default"] = val_to_set
|
||||||
|
|
||||||
|
models.update(data)
|
||||||
|
await ctx.send(_("Defualt set."))
|
||||||
|
|
||||||
|
@commands.bot_has_permissions(add_reactions=True)
|
||||||
|
@checks.is_owner()
|
||||||
|
@permissions.command(name="clearglobalsettings")
|
||||||
|
async def clear_globals(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Clears all global rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
m = await ctx.send("Are you sure?")
|
||||||
|
for r in REACTS.keys():
|
||||||
|
await m.add_reaction(r)
|
||||||
|
try:
|
||||||
|
reaction, user = await self.bot.wait_for(
|
||||||
|
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||||
|
|
||||||
|
if REACTS.get(str(reaction)):
|
||||||
|
await self.config.owner_models.clear()
|
||||||
|
await ctx.send(_("Global settings cleared"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Okay."))
|
||||||
|
|
||||||
|
@commands.bot_has_permissions(add_reactions=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@permissions.command(name="clearguildsettings")
|
||||||
|
async def clear_guild_settings(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Clears all guild rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
m = await ctx.send("Are you sure?")
|
||||||
|
for r in REACTS.keys():
|
||||||
|
await m.add_reaction(r)
|
||||||
|
try:
|
||||||
|
reaction, user = await self.bot.wait_for(
|
||||||
|
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||||
|
|
||||||
|
if REACTS.get(str(reaction)):
|
||||||
|
await self.config.guild(ctx.guild).owner_models.clear()
|
||||||
|
await ctx.send(_("Guild settings cleared"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Okay."))
|
||||||
|
|
||||||
|
def find_object_uniquely(self, info: str) -> int:
|
||||||
|
"""
|
||||||
|
Finds an object uniquely, returns it's id or returns None
|
||||||
|
"""
|
||||||
|
if info is None:
|
||||||
|
return None
|
||||||
|
objs = []
|
||||||
|
|
||||||
|
objs.extend(self.bot.users)
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
objs.extend(guild.roles)
|
||||||
|
objs.extend(guild.channels)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_id = int(info)
|
||||||
|
except ValueError:
|
||||||
|
_id = None
|
||||||
|
|
||||||
|
for function in (
|
||||||
|
lambda x: x.id == _id,
|
||||||
|
lambda x: x.mention == info,
|
||||||
|
lambda x: str(x) == info,
|
||||||
|
lambda x: x.name == info,
|
||||||
|
lambda x: (x.nick if hasattr(x, "nick") else None) == info,
|
||||||
|
):
|
||||||
|
canidates = list(filter(function, objs))
|
||||||
|
if len(canidates) == 1:
|
||||||
|
return canidates[0].id
|
||||||
|
|
||||||
|
return None
|
||||||
91
redbot/cogs/permissions/resolvers.py
Normal file
91
redbot/cogs/permissions/resolvers.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import types
|
||||||
|
import contextlib
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from redbot.core import commands
|
||||||
|
|
||||||
|
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
||||||
|
|
||||||
|
|
||||||
|
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns the value from a check if it is valid
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Non staticmethods should not be run without their parent
|
||||||
|
# class, even if the parent class did not deregister them
|
||||||
|
if check.__module__ is None:
|
||||||
|
pass
|
||||||
|
elif isinstance(check, types.FunctionType):
|
||||||
|
if (
|
||||||
|
next(filter(lambda x: check.__module__ == x.__module__, ctx.bot.cogs.values()), None)
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
val = None
|
||||||
|
# let's not spam the console with improperly made 3rd party checks
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutine(check) or asyncio.iscoroutinefunction(check):
|
||||||
|
val = await check(ctx, level=level)
|
||||||
|
else:
|
||||||
|
val = check(ctx, level=level)
|
||||||
|
except Exception as e:
|
||||||
|
# but still provide a way to view it (run with debug flag)
|
||||||
|
log.debug(str(e))
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Resolves models in order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cmd_name = ctx.command.qualified_name
|
||||||
|
cog_name = ctx.cog.__class__.__name__
|
||||||
|
|
||||||
|
resolved = None
|
||||||
|
|
||||||
|
to_iter = (("commands", cmd_name), ("cogs", cog_name))
|
||||||
|
|
||||||
|
for model_name, ctx_attr in to_iter:
|
||||||
|
if ctx_attr in models.get(model_name, {}):
|
||||||
|
blacklist = models[model_name][ctx_attr].get("deny", [])
|
||||||
|
whitelist = models[model_name][ctx_attr].get("allow", [])
|
||||||
|
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||||
|
if resolved is not None:
|
||||||
|
return resolved
|
||||||
|
resolved = models[model_name][ctx_attr].get("default", None)
|
||||||
|
if resolved is not None:
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
|
||||||
|
"""
|
||||||
|
resolves specific lists
|
||||||
|
"""
|
||||||
|
|
||||||
|
voice_channel = None
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
voice_channel = ctx.author.voice.voice_channel
|
||||||
|
|
||||||
|
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||||
|
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||||
|
entries.extend([x.id for x in roles])
|
||||||
|
# entries now contains the following (in order) (if applicable)
|
||||||
|
# author.id
|
||||||
|
# author.voice.voice_channel.id
|
||||||
|
# channel.id
|
||||||
|
# role.id for each role (highest to lowest)
|
||||||
|
# (implicitly) guild.id because
|
||||||
|
# the @everyone role shares an id with the guild
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if entry in whitelist:
|
||||||
|
return True
|
||||||
|
if entry in blacklist:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
19
redbot/cogs/permissions/template.yaml
Normal file
19
redbot/cogs/permissions/template.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
cogs:
|
||||||
|
Admin:
|
||||||
|
allow:
|
||||||
|
- 78631113035100160
|
||||||
|
deny:
|
||||||
|
- 96733288462286848
|
||||||
|
Audio:
|
||||||
|
allow:
|
||||||
|
- 133049272517001216
|
||||||
|
default: deny
|
||||||
|
commands:
|
||||||
|
cleanup bot:
|
||||||
|
allow:
|
||||||
|
- 78631113035100160
|
||||||
|
default: deny
|
||||||
|
ping:
|
||||||
|
deny:
|
||||||
|
- 96733288462286848
|
||||||
|
default: allow
|
||||||
67
redbot/cogs/permissions/yaml_handler.py
Normal file
67
redbot/cogs/permissions/yaml_handler.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import io
|
||||||
|
import yaml
|
||||||
|
import pathlib
|
||||||
|
import discord
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_template() -> dict:
|
||||||
|
template_fp = pathlib.Path(__file__).parent / "template.yaml"
|
||||||
|
|
||||||
|
with template_fp.open() as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
async def yamlset_acl(ctx, *, config, update):
|
||||||
|
_fp = io.BytesIO()
|
||||||
|
await ctx.message.attachments[0].save(_fp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(_fp)
|
||||||
|
except yaml.YAMLError:
|
||||||
|
_fp.close()
|
||||||
|
del _fp
|
||||||
|
raise
|
||||||
|
|
||||||
|
old_data = await config()
|
||||||
|
|
||||||
|
for outer, inner in data.items():
|
||||||
|
for ok, iv in inner.items():
|
||||||
|
for k, v in iv.items():
|
||||||
|
if k == "default":
|
||||||
|
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
|
||||||
|
|
||||||
|
if not update:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if isinstance(old_data[outer][ok][k], list):
|
||||||
|
data[outer][ok][k].extend(old_data[outer][ok][k])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await config.set(data)
|
||||||
|
|
||||||
|
|
||||||
|
async def yamlget_acl(ctx, *, config):
|
||||||
|
data = await config()
|
||||||
|
removals = []
|
||||||
|
|
||||||
|
for outer, inner in data.items():
|
||||||
|
for ok, iv in inner.items():
|
||||||
|
for k, v in iv.items():
|
||||||
|
if k != "default":
|
||||||
|
continue
|
||||||
|
if v is True:
|
||||||
|
data[outer][ok][k] = "allow"
|
||||||
|
elif v is False:
|
||||||
|
data[outer][ok][k] = "deny"
|
||||||
|
else:
|
||||||
|
removals.append((outer, ok, k))
|
||||||
|
|
||||||
|
for tup in removals:
|
||||||
|
o, i, k = tup
|
||||||
|
data[o][i].pop(k, None)
|
||||||
|
|
||||||
|
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
|
||||||
|
_fp.seek(0)
|
||||||
|
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
|
||||||
|
_fp.close()
|
||||||
@@ -22,15 +22,9 @@ log = logging.getLogger("red.reports")
|
|||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Reports:
|
class Reports:
|
||||||
|
|
||||||
default_guild_settings = {
|
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
||||||
"output_channel": None,
|
|
||||||
"active": False,
|
|
||||||
"next_ticket": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
default_report = {
|
default_report = {"report": {}}
|
||||||
'report': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# This can be made configureable later if it
|
# This can be made configureable later if it
|
||||||
# becomes an issue.
|
# becomes an issue.
|
||||||
@@ -42,15 +36,14 @@ class Reports:
|
|||||||
(timedelta(seconds=5), 1),
|
(timedelta(seconds=5), 1),
|
||||||
(timedelta(minutes=5), 3),
|
(timedelta(minutes=5), 3),
|
||||||
(timedelta(hours=1), 10),
|
(timedelta(hours=1), 10),
|
||||||
(timedelta(days=1), 24)
|
(timedelta(days=1), 24),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(
|
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
||||||
self, 78631113035100160, force_registration=True)
|
|
||||||
self.config.register_guild(**self.default_guild_settings)
|
self.config.register_guild(**self.default_guild_settings)
|
||||||
self.config.register_custom('REPORT', **self.default_report)
|
self.config.register_custom("REPORT", **self.default_report)
|
||||||
self.antispam = {}
|
self.antispam = {}
|
||||||
self.user_cache = []
|
self.user_cache = []
|
||||||
self.tunnel_store = {}
|
self.tunnel_store = {}
|
||||||
@@ -59,9 +52,7 @@ class Reports:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def tunnels(self):
|
def tunnels(self):
|
||||||
return [
|
return [x["tun"] for x in self.tunnel_store.values()]
|
||||||
x['tun'] for x in self.tunnel_store.values()
|
|
||||||
]
|
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -99,9 +90,7 @@ class Reports:
|
|||||||
admin_role = discord.utils.get(
|
admin_role = discord.utils.get(
|
||||||
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
||||||
)
|
)
|
||||||
mod_role = discord.utils.get(
|
mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
|
||||||
guild.roles, id=await self.bot.db.guild(guild).mod_role()
|
|
||||||
)
|
|
||||||
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
||||||
if perms:
|
if perms:
|
||||||
ret |= m.guild_permissions >= perms
|
ret |= m.guild_permissions >= perms
|
||||||
@@ -111,10 +100,13 @@ class Reports:
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def discover_guild(
|
async def discover_guild(
|
||||||
self, author: discord.User, *,
|
self,
|
||||||
mod: bool=False,
|
author: discord.User,
|
||||||
permissions: Union[discord.Permissions, dict]=None,
|
*,
|
||||||
prompt: str=""):
|
mod: bool = False,
|
||||||
|
permissions: Union[discord.Permissions, dict] = None,
|
||||||
|
prompt: str = ""
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
discovers which of shared guilds between the bot
|
discovers which of shared guilds between the bot
|
||||||
and provided user based on conditions (mod or permissions is an or)
|
and provided user based on conditions (mod or permissions is an or)
|
||||||
@@ -151,13 +143,9 @@ class Reports:
|
|||||||
return m.author == author and m.channel == dm.channel
|
return m.author == author and m.channel == dm.channel
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for(
|
message = await self.bot.wait_for("message", check=pred, timeout=45)
|
||||||
'message', check=pred, timeout=45
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await author.send(
|
await author.send(_("You took too long to select. Try again later."))
|
||||||
_("You took too long to select. Try again later.")
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -187,35 +175,31 @@ class Reports:
|
|||||||
if await self.bot.embed_requested(channel, author):
|
if await self.bot.embed_requested(channel, author):
|
||||||
em = discord.Embed(description=report)
|
em = discord.Embed(description=report)
|
||||||
em.set_author(
|
em.set_author(
|
||||||
name=_('Report from {0.display_name}').format(author),
|
name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url
|
||||||
icon_url=author.avatar_url
|
|
||||||
)
|
)
|
||||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||||
send_content = None
|
send_content = None
|
||||||
else:
|
else:
|
||||||
em = None
|
em = None
|
||||||
send_content = _(
|
send_content = _("Report from {author.mention} (Ticket #{number})").format(
|
||||||
'Report from {author.mention} (Ticket #{number})'
|
author=author, number=ticket_number
|
||||||
).format(author=author, number=ticket_number)
|
)
|
||||||
send_content += "\n" + report
|
send_content += "\n" + report
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await Tunnel.message_forwarder(
|
await Tunnel.message_forwarder(
|
||||||
destination=channel,
|
destination=channel, content=send_content, embed=em, files=files
|
||||||
content=send_content,
|
|
||||||
embed=em,
|
|
||||||
files=files
|
|
||||||
)
|
)
|
||||||
except (discord.Forbidden, discord.HTTPException):
|
except (discord.Forbidden, discord.HTTPException):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
|
await self.config.custom("REPORT", guild.id, ticket_number).report.set(
|
||||||
{'user_id': author.id, 'report': report}
|
{"user_id": author.id, "report": report}
|
||||||
)
|
)
|
||||||
return ticket_number
|
return ticket_number
|
||||||
|
|
||||||
@commands.group(name="report", invoke_without_command=True)
|
@commands.group(name="report", invoke_without_command=True)
|
||||||
async def report(self, ctx: commands.Context, *, _report: str=""):
|
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||||
"""
|
"""
|
||||||
Follow the prompts to make a report
|
Follow the prompts to make a report
|
||||||
|
|
||||||
@@ -226,8 +210,7 @@ class Reports:
|
|||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if guild is None:
|
if guild is None:
|
||||||
guild = await self.discover_guild(
|
guild = await self.discover_guild(
|
||||||
author,
|
author, prompt=_("Select a server to make a report in by number.")
|
||||||
prompt=_("Select a server to make a report in by number.")
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -238,24 +221,23 @@ class Reports:
|
|||||||
return
|
return
|
||||||
g_active = await self.config.guild(guild).active()
|
g_active = await self.config.guild(guild).active()
|
||||||
if not g_active:
|
if not g_active:
|
||||||
return await author.send(
|
return await author.send(_("Reporting has not been enabled for this server"))
|
||||||
_("Reporting has not been enabled for this server")
|
|
||||||
)
|
|
||||||
if guild.id not in self.antispam:
|
if guild.id not in self.antispam:
|
||||||
self.antispam[guild.id] = {}
|
self.antispam[guild.id] = {}
|
||||||
if author.id not in self.antispam[guild.id]:
|
if author.id not in self.antispam[guild.id]:
|
||||||
self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
|
self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
|
||||||
if self.antispam[guild.id][author.id].spammy:
|
if self.antispam[guild.id][author.id].spammy:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_("You've sent a few too many of these recently. "
|
_(
|
||||||
|
"You've sent a few too many of these recently. "
|
||||||
"Contact a server admin to resolve this, or try again "
|
"Contact a server admin to resolve this, or try again "
|
||||||
"later.")
|
"later."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if author.id in self.user_cache:
|
if author.id in self.user_cache:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_("Finish making your prior report "
|
_("Finish making your prior report " "before making an additional one")
|
||||||
"before making an additional one")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
@@ -273,13 +255,13 @@ class Reports:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
dm = await author.send(
|
dm = await author.send(
|
||||||
_("Please respond to this message with your Report."
|
_(
|
||||||
"\nYour report should be a single message")
|
"Please respond to this message with your Report."
|
||||||
|
"\nYour report should be a single message"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(
|
await ctx.send(_("This requires DMs enabled."))
|
||||||
_("This requires DMs enabled.")
|
|
||||||
)
|
|
||||||
self.user_cache.remove(author.id)
|
self.user_cache.remove(author.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -287,25 +269,17 @@ class Reports:
|
|||||||
return m.author == author and m.channel == dm.channel
|
return m.author == author and m.channel == dm.channel
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for(
|
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
||||||
'message', check=pred, timeout=180
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await author.send(
|
await author.send(_("You took too long. Try again later."))
|
||||||
_("You took too long. Try again later.")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
val = await self.send_report(message, guild)
|
val = await self.send_report(message, guild)
|
||||||
|
|
||||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||||
if val is None:
|
if val is None:
|
||||||
await author.send(
|
await author.send(_("There was an error sending your report."))
|
||||||
_("There was an error sending your report.")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await author.send(
|
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||||
_("Your report was submitted. (Ticket #{})").format(val)
|
|
||||||
)
|
|
||||||
self.antispam[guild.id][author.id].stamp()
|
self.antispam[guild.id][author.id].stamp()
|
||||||
|
|
||||||
self.user_cache.remove(author.id)
|
self.user_cache.remove(author.id)
|
||||||
@@ -318,18 +292,14 @@ class Reports:
|
|||||||
return
|
return
|
||||||
|
|
||||||
_id = payload.message_id
|
_id = payload.message_id
|
||||||
t = next(filter(
|
t = next(filter(lambda x: _id in x[1]["msgs"], self.tunnel_store.items()), None)
|
||||||
lambda x: _id in x[1]['msgs'],
|
|
||||||
self.tunnel_store.items()
|
|
||||||
), None)
|
|
||||||
|
|
||||||
if t is None:
|
if t is None:
|
||||||
return
|
return
|
||||||
tun = t[1]['tun']
|
tun = t[1]["tun"]
|
||||||
if payload.user_id in [x.id for x in tun.members]:
|
if payload.user_id in [x.id for x in tun.members]:
|
||||||
await tun.react_close(
|
await tun.react_close(
|
||||||
uid=payload.user_id,
|
uid=payload.user_id, message=_("{closer} has closed the correspondence")
|
||||||
message=_("{closer} has closed the correspondence")
|
|
||||||
)
|
)
|
||||||
self.tunnel_store.pop(t[0], None)
|
self.tunnel_store.pop(t[0], None)
|
||||||
|
|
||||||
@@ -337,12 +307,12 @@ class Reports:
|
|||||||
for k, v in self.tunnel_store.items():
|
for k, v in self.tunnel_store.items():
|
||||||
topic = _("Re: ticket# {1} in {0.name}").format(*k)
|
topic = _("Re: ticket# {1} in {0.name}").format(*k)
|
||||||
# Tunnels won't forward unintended messages, this is safe
|
# Tunnels won't forward unintended messages, this is safe
|
||||||
msgs = await v['tun'].communicate(message=message, topic=topic)
|
msgs = await v["tun"].communicate(message=message, topic=topic)
|
||||||
if msgs:
|
if msgs:
|
||||||
self.tunnel_store[k]['msgs'] = msgs
|
self.tunnel_store[k]["msgs"] = msgs
|
||||||
|
|
||||||
@checks.mod_or_permissions(manage_members=True)
|
@checks.mod_or_permissions(manage_members=True)
|
||||||
@report.command(name='interact')
|
@report.command(name="interact")
|
||||||
async def response(self, ctx, ticket_number: int):
|
async def response(self, ctx, ticket_number: int):
|
||||||
"""
|
"""
|
||||||
opens a message tunnel between things you say in this channel
|
opens a message tunnel between things you say in this channel
|
||||||
@@ -353,27 +323,24 @@ class Reports:
|
|||||||
|
|
||||||
# note, mod_or_permissions is an implicit guild_only
|
# note, mod_or_permissions is an implicit guild_only
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
rec = await self.config.custom(
|
rec = await self.config.custom("REPORT", guild.id, ticket_number).report()
|
||||||
'REPORT', guild.id, ticket_number).report()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = guild.get_member(rec.get('user_id'))
|
user = guild.get_member(rec.get("user_id"))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return await ctx.send(
|
return await ctx.send(_("That ticket doesn't seem to exist"))
|
||||||
_("That ticket doesn't seem to exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
return await ctx.send(
|
return await ctx.send(_("That user isn't here anymore."))
|
||||||
_("That user isn't here anymore.")
|
|
||||||
)
|
|
||||||
|
|
||||||
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
|
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
|
||||||
|
|
||||||
if tun is None:
|
if tun is None:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
_("Either you or the user you are trying to reach already "
|
_(
|
||||||
"has an open communication.")
|
"Either you or the user you are trying to reach already "
|
||||||
|
"has an open communication."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
big_topic = _(
|
big_topic = _(
|
||||||
@@ -387,18 +354,13 @@ class Reports:
|
|||||||
"\nTunnels are not persistent across bot restarts."
|
"\nTunnels are not persistent across bot restarts."
|
||||||
)
|
)
|
||||||
topic = big_topic.format(
|
topic = big_topic.format(
|
||||||
ticketnum=ticket_number,
|
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||||
who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
m = await tun.communicate(
|
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||||
message=ctx.message, topic=topic, skip_message_content=True
|
|
||||||
)
|
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("User has disabled DMs."))
|
await ctx.send(_("User has disabled DMs."))
|
||||||
tun.close()
|
tun.close()
|
||||||
else:
|
else:
|
||||||
self.tunnel_store[(guild, ticket_number)] = {'tun': tun, 'msgs': m}
|
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||||
await ctx.send(
|
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||||
big_topic.format(who=_("You have"), ticketnum=ticket_number)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../mod.py"]
|
||||||
'../mod.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,9 +3,24 @@ from redbot.core import Config, checks, commands
|
|||||||
from redbot.core.utils.chat_formatting import pagify
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream
|
from .streamtypes import (
|
||||||
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials,
|
TwitchStream,
|
||||||
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials)
|
HitboxStream,
|
||||||
|
MixerStream,
|
||||||
|
PicartoStream,
|
||||||
|
TwitchCommunity,
|
||||||
|
YoutubeStream,
|
||||||
|
)
|
||||||
|
from .errors import (
|
||||||
|
OfflineStream,
|
||||||
|
StreamNotFound,
|
||||||
|
APIError,
|
||||||
|
InvalidYoutubeCredentials,
|
||||||
|
CommunityNotFound,
|
||||||
|
OfflineCommunity,
|
||||||
|
StreamsError,
|
||||||
|
InvalidTwitchCredentials,
|
||||||
|
)
|
||||||
from . import streamtypes as StreamClasses
|
from . import streamtypes as StreamClasses
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -20,21 +35,11 @@ _ = Translator("Streams", __file__)
|
|||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Streams:
|
class Streams:
|
||||||
|
|
||||||
global_defaults = {
|
global_defaults = {"tokens": {}, "streams": [], "communities": []}
|
||||||
"tokens": {},
|
|
||||||
"streams": [],
|
|
||||||
"communities": []
|
|
||||||
}
|
|
||||||
|
|
||||||
guild_defaults = {
|
guild_defaults = {"autodelete": False, "mention_everyone": False, "mention_here": False}
|
||||||
"autodelete": False,
|
|
||||||
"mention_everyone": False,
|
|
||||||
"mention_here": False
|
|
||||||
}
|
|
||||||
|
|
||||||
role_defaults = {
|
role_defaults = {"mention": False}
|
||||||
"mention": False
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.db = Config.get_conf(self, 26262626)
|
self.db = Config.get_conf(self, 26262626)
|
||||||
@@ -67,8 +72,7 @@ class Streams:
|
|||||||
async def twitch(self, ctx: commands.Context, channel_name: str):
|
async def twitch(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Twitch channel is streaming"""
|
"""Checks if a Twitch channel is streaming"""
|
||||||
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
||||||
stream = TwitchStream(name=channel_name,
|
stream = TwitchStream(name=channel_name, token=token)
|
||||||
token=token)
|
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@@ -110,14 +114,21 @@ class Streams:
|
|||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
await ctx.send(_("The channel doesn't seem to exist."))
|
await ctx.send(_("The channel doesn't seem to exist."))
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(_("The twitch token is either invalid or has not been set. "
|
await ctx.send(
|
||||||
"See `{}`.").format("{}streamset twitchtoken".format(ctx.prefix)))
|
_("The twitch token is either invalid or has not been set. " "See `{}`.").format(
|
||||||
|
"{}streamset twitchtoken".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
|
await ctx.send(
|
||||||
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
|
_("The Youtube API key is either invalid or has not been set. " "See {}.").format(
|
||||||
|
"`{}streamset youtubekey`".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(_("Something went wrong whilst trying to contact the "
|
await ctx.send(
|
||||||
"stream service's API."))
|
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@@ -166,7 +177,7 @@ class Streams:
|
|||||||
await self.stream_alert(ctx, PicartoStream, channel_name)
|
await self.stream_alert(ctx, PicartoStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="stop")
|
@streamalert.command(name="stop")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool=False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Stops all stream notifications in the channel
|
"""Stops all stream notifications in the channel
|
||||||
|
|
||||||
Adding 'yes' will disable all notifications in the server"""
|
Adding 'yes' will disable all notifications in the server"""
|
||||||
@@ -191,8 +202,9 @@ class Streams:
|
|||||||
self.streams = streams
|
self.streams = streams
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
|
|
||||||
msg = _("All {}'s stream alerts have been disabled."
|
msg = _("All {}'s stream alerts have been disabled." "").format(
|
||||||
"").format("server" if _all else "channel")
|
"server" if _all else "channel"
|
||||||
|
)
|
||||||
|
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@@ -226,23 +238,27 @@ class Streams:
|
|||||||
if is_yt and not self.check_name_or_id(channel_name):
|
if is_yt and not self.check_name_or_id(channel_name):
|
||||||
stream = _class(id=channel_name, token=token)
|
stream = _class(id=channel_name, token=token)
|
||||||
else:
|
else:
|
||||||
stream = _class(name=channel_name,
|
stream = _class(name=channel_name, token=token)
|
||||||
token=token)
|
|
||||||
try:
|
try:
|
||||||
exists = await self.check_exists(stream)
|
exists = await self.check_exists(stream)
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. "
|
_("The twitch token is either invalid or has not been set. " "See {}.").format(
|
||||||
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
|
"`{}streamset twitchtoken`".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
|
await ctx.send(
|
||||||
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
|
_(
|
||||||
|
"The Youtube API key is either invalid or has not been set. " "See {}."
|
||||||
|
).format("`{}streamset youtubekey`".format(ctx.prefix))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Something went wrong whilst trying to contact the "
|
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
||||||
"stream service's API."))
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if not exists:
|
if not exists:
|
||||||
@@ -260,16 +276,18 @@ class Streams:
|
|||||||
await community.get_community_streams()
|
await community.get_community_streams()
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. "
|
_("The twitch token is either invalid or has not been set. " "See {}.").format(
|
||||||
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
|
"`{}streamset twitchtoken`".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except CommunityNotFound:
|
except CommunityNotFound:
|
||||||
await ctx.send(_("That community doesn't seem to exist."))
|
await ctx.send(_("That community doesn't seem to exist."))
|
||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Something went wrong whilst trying to contact the "
|
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
||||||
"stream service's API."))
|
)
|
||||||
return
|
return
|
||||||
except OfflineCommunity:
|
except OfflineCommunity:
|
||||||
pass
|
pass
|
||||||
@@ -331,12 +349,19 @@ class Streams:
|
|||||||
current_setting = await self.db.guild(guild).mention_everyone()
|
current_setting = await self.db.guild(guild).mention_everyone()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_everyone.set(False)
|
await self.db.guild(guild).mention_everyone.set(False)
|
||||||
await ctx.send(_("{} will no longer be mentioned "
|
await ctx.send(
|
||||||
"for a stream alert.").format("@\u200beveryone"))
|
_("{} will no longer be mentioned " "for a stream alert.").format(
|
||||||
|
"@\u200beveryone"
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_everyone.set(True)
|
await self.db.guild(guild).mention_everyone.set(True)
|
||||||
await ctx.send(_("When a stream configured for stream alerts "
|
await ctx.send(
|
||||||
"comes online, {} will be mentioned").format("@\u200beveryone"))
|
_(
|
||||||
|
"When a stream configured for stream alerts "
|
||||||
|
"comes online, {} will be mentioned"
|
||||||
|
).format("@\u200beveryone")
|
||||||
|
)
|
||||||
|
|
||||||
@mention.command(aliases=["here"])
|
@mention.command(aliases=["here"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -346,12 +371,17 @@ class Streams:
|
|||||||
current_setting = await self.db.guild(guild).mention_here()
|
current_setting = await self.db.guild(guild).mention_here()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_here.set(False)
|
await self.db.guild(guild).mention_here.set(False)
|
||||||
await ctx.send(_("{} will no longer be mentioned "
|
await ctx.send(
|
||||||
"for a stream alert.").format("@\u200bhere"))
|
_("{} will no longer be mentioned " "for a stream alert.").format("@\u200bhere")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_here.set(True)
|
await self.db.guild(guild).mention_here.set(True)
|
||||||
await ctx.send(_("When a stream configured for stream alerts "
|
await ctx.send(
|
||||||
"comes online, {} will be mentioned").format("@\u200bhere"))
|
_(
|
||||||
|
"When a stream configured for stream alerts "
|
||||||
|
"comes online, {} will be mentioned"
|
||||||
|
).format("@\u200bhere")
|
||||||
|
)
|
||||||
|
|
||||||
@mention.command()
|
@mention.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -363,13 +393,20 @@ class Streams:
|
|||||||
return
|
return
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.role(role).mention.set(False)
|
await self.db.role(role).mention.set(False)
|
||||||
await ctx.send(_("{} will no longer be mentioned "
|
await ctx.send(
|
||||||
"for a stream alert").format("@\u200b{}".format(role.name)))
|
_("{} will no longer be mentioned " "for a stream alert").format(
|
||||||
|
"@\u200b{}".format(role.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.role(role).mention.set(True)
|
await self.db.role(role).mention.set(True)
|
||||||
await ctx.send(_("When a stream configured for stream alerts "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"When a stream configured for stream alerts "
|
||||||
"comes online, {} will be mentioned"
|
"comes online, {} will be mentioned"
|
||||||
"").format("@\u200b{}".format(role.name)))
|
""
|
||||||
|
).format("@\u200b{}".format(role.name))
|
||||||
|
)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -377,8 +414,7 @@ class Streams:
|
|||||||
"""Toggles automatic deletion of notifications for streams that go offline"""
|
"""Toggles automatic deletion of notifications for streams that go offline"""
|
||||||
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
||||||
if on_off:
|
if on_off:
|
||||||
await ctx.send("The notifications will be deleted once "
|
await ctx.send("The notifications will be deleted once " "streams go offline.")
|
||||||
"streams go offline.")
|
|
||||||
else:
|
else:
|
||||||
await ctx.send("Notifications will never be deleted.")
|
await ctx.send("Notifications will never be deleted.")
|
||||||
|
|
||||||
@@ -387,14 +423,20 @@ class Streams:
|
|||||||
stream.channels.append(ctx.channel.id)
|
stream.channels.append(ctx.channel.id)
|
||||||
if stream not in self.streams:
|
if stream not in self.streams:
|
||||||
self.streams.append(stream)
|
self.streams.append(stream)
|
||||||
await ctx.send(_("I'll send a notification in this channel when {} "
|
await ctx.send(
|
||||||
"is online.").format(stream.name))
|
_("I'll send a notification in this channel when {} " "is online.").format(
|
||||||
|
stream.name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
stream.channels.remove(ctx.channel.id)
|
stream.channels.remove(ctx.channel.id)
|
||||||
if not stream.channels:
|
if not stream.channels:
|
||||||
self.streams.remove(stream)
|
self.streams.remove(stream)
|
||||||
await ctx.send(_("I won't send notifications about {} in this "
|
await ctx.send(
|
||||||
"channel anymore.").format(stream.name))
|
_("I won't send notifications about {} in this " "channel anymore.").format(
|
||||||
|
stream.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
|
|
||||||
@@ -403,16 +445,24 @@ class Streams:
|
|||||||
community.channels.append(ctx.channel.id)
|
community.channels.append(ctx.channel.id)
|
||||||
if community not in self.communities:
|
if community not in self.communities:
|
||||||
self.communities.append(community)
|
self.communities.append(community)
|
||||||
await ctx.send(_("I'll send a notification in this channel when a "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"I'll send a notification in this channel when a "
|
||||||
"channel is streaming to the {} community"
|
"channel is streaming to the {} community"
|
||||||
"").format(community.name))
|
""
|
||||||
|
).format(community.name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
community.channels.remove(ctx.channel.id)
|
community.channels.remove(ctx.channel.id)
|
||||||
if not community.channels:
|
if not community.channels:
|
||||||
self.communities.remove(community)
|
self.communities.remove(community)
|
||||||
await ctx.send(_("I won't send notifications about channels streaming "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"I won't send notifications about channels streaming "
|
||||||
"to the {} community in this channel anymore"
|
"to the {} community in this channel anymore"
|
||||||
"").format(community.name))
|
""
|
||||||
|
).format(community.name)
|
||||||
|
)
|
||||||
await self.save_communities()
|
await self.save_communities()
|
||||||
|
|
||||||
def get_stream(self, _class, name):
|
def get_stream(self, _class, name):
|
||||||
@@ -499,13 +549,13 @@ class Streams:
|
|||||||
settings = self.db.guild(guild)
|
settings = self.db.guild(guild)
|
||||||
mentions = []
|
mentions = []
|
||||||
if await settings.mention_everyone():
|
if await settings.mention_everyone():
|
||||||
mentions.append('@everyone')
|
mentions.append("@everyone")
|
||||||
if await settings.mention_here():
|
if await settings.mention_here():
|
||||||
mentions.append('@here')
|
mentions.append("@here")
|
||||||
for role in guild.roles:
|
for role in guild.roles:
|
||||||
if await self.db.role(role).mention():
|
if await self.db.role(role).mention():
|
||||||
mentions.append(role.mention)
|
mentions.append(role.mention)
|
||||||
return ' '.join(mentions)
|
return " ".join(mentions)
|
||||||
|
|
||||||
async def check_communities(self):
|
async def check_communities(self):
|
||||||
for community in self.communities:
|
for community in self.communities:
|
||||||
@@ -579,8 +629,7 @@ class Streams:
|
|||||||
# Fast dedupe below
|
# Fast dedupe below
|
||||||
seen = set()
|
seen = set()
|
||||||
seen_add = seen.add
|
seen_add = seen.add
|
||||||
return [x for x in streams
|
return [x for x in streams if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
||||||
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
|
||||||
|
|
||||||
# return streams
|
# return streams
|
||||||
|
|
||||||
@@ -604,8 +653,7 @@ class Streams:
|
|||||||
# Fast dedupe below
|
# Fast dedupe below
|
||||||
seen = set()
|
seen = set()
|
||||||
seen_add = seen.add
|
seen_add = seen.add
|
||||||
return [x for x in communities
|
return [x for x in communities if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
||||||
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
|
||||||
# return communities
|
# return communities
|
||||||
|
|
||||||
async def save_streams(self):
|
async def save_streams(self):
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
from .errors import StreamNotFound, APIError, OfflineStream, CommunityNotFound, OfflineCommunity, \
|
from .errors import (
|
||||||
InvalidYoutubeCredentials, InvalidTwitchCredentials
|
StreamNotFound,
|
||||||
|
APIError,
|
||||||
|
OfflineStream,
|
||||||
|
CommunityNotFound,
|
||||||
|
OfflineCommunity,
|
||||||
|
InvalidYoutubeCredentials,
|
||||||
|
InvalidTwitchCredentials,
|
||||||
|
)
|
||||||
from random import choice, sample
|
from random import choice, sample
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
import discord
|
import discord
|
||||||
@@ -23,6 +30,7 @@ def rnd(url):
|
|||||||
|
|
||||||
|
|
||||||
class TwitchCommunity:
|
class TwitchCommunity:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.name = kwargs.pop("name")
|
self.name = kwargs.pop("name")
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
@@ -32,15 +40,12 @@ class TwitchCommunity:
|
|||||||
self.type = self.__class__.__name__
|
self.type = self.__class__.__name__
|
||||||
|
|
||||||
async def get_community_id(self):
|
async def get_community_id(self):
|
||||||
headers = {
|
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
|
||||||
"Accept": "application/vnd.twitchtv.v5+json",
|
params = {"name": self.name}
|
||||||
"Client-ID": str(self._token)
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
"name": self.name
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params) as r:
|
async with session.get(
|
||||||
|
TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params
|
||||||
|
) as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
if r.status == 200:
|
if r.status == 200:
|
||||||
return data["_id"]
|
return data["_id"]
|
||||||
@@ -57,14 +62,8 @@ class TwitchCommunity:
|
|||||||
self.id = await self.get_community_id()
|
self.id = await self.get_community_id()
|
||||||
except CommunityNotFound:
|
except CommunityNotFound:
|
||||||
raise
|
raise
|
||||||
headers = {
|
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
|
||||||
"Accept": "application/vnd.twitchtv.v5+json",
|
params = {"community_id": self.id, "limit": 100}
|
||||||
"Client-ID": str(self._token)
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
"community_id": self.id,
|
|
||||||
"limit": 100
|
|
||||||
}
|
|
||||||
url = TWITCH_BASE_URL + "/kraken/streams"
|
url = TWITCH_BASE_URL + "/kraken/streams"
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, headers=headers, params=params) as r:
|
async with session.get(url, headers=headers, params=params) as r:
|
||||||
@@ -82,14 +81,11 @@ class TwitchCommunity:
|
|||||||
raise APIError()
|
raise APIError()
|
||||||
|
|
||||||
async def make_embed(self, streams: list) -> discord.Embed:
|
async def make_embed(self, streams: list) -> discord.Embed:
|
||||||
headers = {
|
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
|
||||||
"Accept": "application/vnd.twitchtv.v5+json",
|
|
||||||
"Client-ID": str(self._token)
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id),
|
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id), headers=headers
|
||||||
headers=headers) as r:
|
) as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
|
|
||||||
avatar = data["avatar_image_url"]
|
avatar = data["avatar_image_url"]
|
||||||
@@ -102,9 +98,7 @@ class TwitchCommunity:
|
|||||||
else:
|
else:
|
||||||
stream_list = streams
|
stream_list = streams
|
||||||
for stream in stream_list:
|
for stream in stream_list:
|
||||||
name = "[{}]({})".format(
|
name = "[{}]({})".format(stream["channel"]["display_name"], stream["channel"]["url"])
|
||||||
stream["channel"]["display_name"], stream["channel"]["url"]
|
|
||||||
)
|
|
||||||
embed.add_field(name=stream["channel"]["status"], value=name, inline=False)
|
embed.add_field(name=stream["channel"]["status"], value=name, inline=False)
|
||||||
embed.color = 0x6441A4
|
embed.color = 0x6441A4
|
||||||
|
|
||||||
@@ -125,10 +119,11 @@ class TwitchCommunity:
|
|||||||
|
|
||||||
|
|
||||||
class Stream:
|
class Stream:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.name = kwargs.pop("name", None)
|
self.name = kwargs.pop("name", None)
|
||||||
self.channels = kwargs.pop("channels", [])
|
self.channels = kwargs.pop("channels", [])
|
||||||
#self.already_online = kwargs.pop("already_online", False)
|
# self.already_online = kwargs.pop("already_online", False)
|
||||||
self._messages_cache = kwargs.pop("_messages_cache", [])
|
self._messages_cache = kwargs.pop("_messages_cache", [])
|
||||||
self.type = self.__class__.__name__
|
self.type = self.__class__.__name__
|
||||||
|
|
||||||
@@ -153,6 +148,7 @@ class Stream:
|
|||||||
|
|
||||||
|
|
||||||
class YoutubeStream(Stream):
|
class YoutubeStream(Stream):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
self._token = kwargs.pop("token", None)
|
self._token = kwargs.pop("token", None)
|
||||||
@@ -167,7 +163,7 @@ class YoutubeStream(Stream):
|
|||||||
"part": "snippet",
|
"part": "snippet",
|
||||||
"channelId": self.id,
|
"channelId": self.id,
|
||||||
"type": "video",
|
"type": "video",
|
||||||
"eventType": "live"
|
"eventType": "live",
|
||||||
}
|
}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, params=params) as r:
|
async with session.get(url, params=params) as r:
|
||||||
@@ -176,11 +172,7 @@ class YoutubeStream(Stream):
|
|||||||
raise OfflineStream()
|
raise OfflineStream()
|
||||||
elif "items" in data:
|
elif "items" in data:
|
||||||
vid_id = data["items"][0]["id"]["videoId"]
|
vid_id = data["items"][0]["id"]["videoId"]
|
||||||
params = {
|
params = {"key": self._token, "id": vid_id, "part": "snippet"}
|
||||||
"key": self._token,
|
|
||||||
"id": vid_id,
|
|
||||||
"part": "snippet"
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
|
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
@@ -199,17 +191,16 @@ class YoutubeStream(Stream):
|
|||||||
return embed
|
return embed
|
||||||
|
|
||||||
async def fetch_id(self):
|
async def fetch_id(self):
|
||||||
params = {
|
params = {"key": self._token, "forUsername": self.name, "part": "id"}
|
||||||
"key": self._token,
|
|
||||||
"forUsername": self.name,
|
|
||||||
"part": "id"
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r:
|
async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
|
|
||||||
if "error" in data and data["error"]["code"] == 400 and\
|
if (
|
||||||
data["error"]["errors"][0]["reason"] == "keyInvalid":
|
"error" in data
|
||||||
|
and data["error"]["code"] == 400
|
||||||
|
and data["error"]["errors"][0]["reason"] == "keyInvalid"
|
||||||
|
):
|
||||||
raise InvalidYoutubeCredentials()
|
raise InvalidYoutubeCredentials()
|
||||||
elif "items" in data and len(data["items"]) == 0:
|
elif "items" in data and len(data["items"]) == 0:
|
||||||
raise StreamNotFound()
|
raise StreamNotFound()
|
||||||
@@ -222,6 +213,7 @@ class YoutubeStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class TwitchStream(Stream):
|
class TwitchStream(Stream):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
self._token = kwargs.pop("token", None)
|
self._token = kwargs.pop("token", None)
|
||||||
@@ -232,19 +224,16 @@ class TwitchStream(Stream):
|
|||||||
self.id = await self.fetch_id()
|
self.id = await self.fetch_id()
|
||||||
|
|
||||||
url = TWITCH_STREAMS_ENDPOINT + self.id
|
url = TWITCH_STREAMS_ENDPOINT + self.id
|
||||||
header = {
|
header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
|
||||||
'Client-ID': str(self._token),
|
|
||||||
'Accept': 'application/vnd.twitchtv.v5+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, headers=header) as r:
|
async with session.get(url, headers=header) as r:
|
||||||
data = await r.json(encoding='utf-8')
|
data = await r.json(encoding="utf-8")
|
||||||
if r.status == 200:
|
if r.status == 200:
|
||||||
if data["stream"] is None:
|
if data["stream"] is None:
|
||||||
#self.already_online = False
|
# self.already_online = False
|
||||||
raise OfflineStream()
|
raise OfflineStream()
|
||||||
#self.already_online = True
|
# self.already_online = True
|
||||||
# In case of rename
|
# In case of rename
|
||||||
self.name = data["stream"]["channel"]["name"]
|
self.name = data["stream"]["channel"]["name"]
|
||||||
return self.make_embed(data)
|
return self.make_embed(data)
|
||||||
@@ -256,10 +245,7 @@ class TwitchStream(Stream):
|
|||||||
raise APIError()
|
raise APIError()
|
||||||
|
|
||||||
async def fetch_id(self):
|
async def fetch_id(self):
|
||||||
header = {
|
header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
|
||||||
'Client-ID': str(self._token),
|
|
||||||
'Accept': 'application/vnd.twitchtv.v5+json'
|
|
||||||
}
|
|
||||||
url = TWITCH_ID_ENDPOINT + self.name
|
url = TWITCH_ID_ENDPOINT + self.name
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
@@ -280,8 +266,7 @@ class TwitchStream(Stream):
|
|||||||
url = channel["url"]
|
url = channel["url"]
|
||||||
logo = channel["logo"]
|
logo = channel["logo"]
|
||||||
if logo is None:
|
if logo is None:
|
||||||
logo = ("https://static-cdn.jtvnw.net/"
|
logo = "https://static-cdn.jtvnw.net/" "jtv_user_pictures/xarth/404_user_70x70.png"
|
||||||
"jtv_user_pictures/xarth/404_user_70x70.png")
|
|
||||||
status = channel["status"]
|
status = channel["status"]
|
||||||
if not status:
|
if not status:
|
||||||
status = "Untitled broadcast"
|
status = "Untitled broadcast"
|
||||||
@@ -303,21 +288,22 @@ class TwitchStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class HitboxStream(Stream):
|
class HitboxStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://api.hitbox.tv/media/live/" + self.name
|
url = "https://api.hitbox.tv/media/live/" + self.name
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as r:
|
async with session.get(url) as r:
|
||||||
#data = await r.json(encoding='utf-8')
|
# data = await r.json(encoding='utf-8')
|
||||||
data = await r.text()
|
data = await r.text()
|
||||||
data = json.loads(data, strict=False)
|
data = json.loads(data, strict=False)
|
||||||
if "livestream" not in data:
|
if "livestream" not in data:
|
||||||
raise StreamNotFound()
|
raise StreamNotFound()
|
||||||
elif data["livestream"][0]["media_is_live"] == "0":
|
elif data["livestream"][0]["media_is_live"] == "0":
|
||||||
#self.already_online = False
|
# self.already_online = False
|
||||||
raise OfflineStream()
|
raise OfflineStream()
|
||||||
elif data["livestream"][0]["media_is_live"] == "1":
|
elif data["livestream"][0]["media_is_live"] == "1":
|
||||||
#self.already_online = True
|
# self.already_online = True
|
||||||
return self.make_embed(data)
|
return self.make_embed(data)
|
||||||
|
|
||||||
raise APIError()
|
raise APIError()
|
||||||
@@ -340,20 +326,21 @@ class HitboxStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class MixerStream(Stream):
|
class MixerStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://mixer.com/api/v1/channels/" + self.name
|
url = "https://mixer.com/api/v1/channels/" + self.name
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as r:
|
async with session.get(url) as r:
|
||||||
#data = await r.json(encoding='utf-8')
|
# data = await r.json(encoding='utf-8')
|
||||||
data = await r.text(encoding='utf-8')
|
data = await r.text(encoding="utf-8")
|
||||||
if r.status == 200:
|
if r.status == 200:
|
||||||
data = json.loads(data, strict=False)
|
data = json.loads(data, strict=False)
|
||||||
if data["online"] is True:
|
if data["online"] is True:
|
||||||
#self.already_online = True
|
# self.already_online = True
|
||||||
return self.make_embed(data)
|
return self.make_embed(data)
|
||||||
else:
|
else:
|
||||||
#self.already_online = False
|
# self.already_online = False
|
||||||
raise OfflineStream()
|
raise OfflineStream()
|
||||||
elif r.status == 404:
|
elif r.status == 404:
|
||||||
raise StreamNotFound()
|
raise StreamNotFound()
|
||||||
@@ -361,8 +348,7 @@ class MixerStream(Stream):
|
|||||||
raise APIError()
|
raise APIError()
|
||||||
|
|
||||||
def make_embed(self, data):
|
def make_embed(self, data):
|
||||||
default_avatar = ("https://mixer.com/_latest/assets/images/main/"
|
default_avatar = "https://mixer.com/_latest/assets/images/main/" "avatars/default.jpg"
|
||||||
"avatars/default.jpg")
|
|
||||||
user = data["user"]
|
user = data["user"]
|
||||||
url = "https://mixer.com/" + data["token"]
|
url = "https://mixer.com/" + data["token"]
|
||||||
embed = discord.Embed(title=data["name"], url=url)
|
embed = discord.Embed(title=data["name"], url=url)
|
||||||
@@ -382,19 +368,20 @@ class MixerStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class PicartoStream(Stream):
|
class PicartoStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://api.picarto.tv/v1/channel/name/" + self.name
|
url = "https://api.picarto.tv/v1/channel/name/" + self.name
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as r:
|
async with session.get(url) as r:
|
||||||
data = await r.text(encoding='utf-8')
|
data = await r.text(encoding="utf-8")
|
||||||
if r.status == 200:
|
if r.status == 200:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
if data["online"] is True:
|
if data["online"] is True:
|
||||||
#self.already_online = True
|
# self.already_online = True
|
||||||
return self.make_embed(data)
|
return self.make_embed(data)
|
||||||
else:
|
else:
|
||||||
#self.already_online = False
|
# self.already_online = False
|
||||||
raise OfflineStream()
|
raise OfflineStream()
|
||||||
elif r.status == 404:
|
elif r.status == 404:
|
||||||
raise StreamNotFound()
|
raise StreamNotFound()
|
||||||
@@ -402,8 +389,9 @@ class PicartoStream(Stream):
|
|||||||
raise APIError()
|
raise APIError()
|
||||||
|
|
||||||
def make_embed(self, data):
|
def make_embed(self, data):
|
||||||
avatar = rnd("https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg"
|
avatar = rnd(
|
||||||
"".format(data["name"].lower()))
|
"https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" "".format(data["name"].lower())
|
||||||
|
)
|
||||||
url = "https://picarto.tv/" + data["name"]
|
url = "https://picarto.tv/" + data["name"]
|
||||||
thumbnail = data["thumbnails"]["web"]
|
thumbnail = data["thumbnails"]["web"]
|
||||||
embed = discord.Embed(title=data["title"], url=url)
|
embed = discord.Embed(title=data["title"], url=url)
|
||||||
@@ -424,6 +412,5 @@ class PicartoStream(Stream):
|
|||||||
data["adult"] = ""
|
data["adult"] = ""
|
||||||
|
|
||||||
embed.color = 0x4C90F3
|
embed.color = 0x4C90F3
|
||||||
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}"
|
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" "".format(**data))
|
||||||
"".format(**data))
|
|
||||||
return embed
|
return embed
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../mod.py"]
|
||||||
'../mod.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ from .log import LOG
|
|||||||
|
|
||||||
__all__ = ["TriviaSession"]
|
__all__ = ["TriviaSession"]
|
||||||
|
|
||||||
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.",
|
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
|
||||||
"Oh really? It's {} of course.")
|
_FAIL_MESSAGES = (
|
||||||
_FAIL_MESSAGES = ("To the next one I guess...", "Moving on...",
|
"To the next one I guess...",
|
||||||
|
"Moving on...",
|
||||||
"I'm sure you'll know the answer of the next one.",
|
"I'm sure you'll know the answer of the next one.",
|
||||||
"\N{PENSIVE FACE} Next one.")
|
"\N{PENSIVE FACE} Next one.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TriviaSession():
|
class TriviaSession:
|
||||||
"""Class to run a session of trivia with the user.
|
"""Class to run a session of trivia with the user.
|
||||||
|
|
||||||
To run the trivia session immediately, use `TriviaSession.start` instead of
|
To run the trivia session immediately, use `TriviaSession.start` instead of
|
||||||
@@ -49,10 +51,7 @@ class TriviaSession():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, ctx, question_list: dict, settings: dict):
|
||||||
ctx,
|
|
||||||
question_list: dict,
|
|
||||||
settings: dict):
|
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
list_ = list(question_list.items())
|
list_ = list(question_list.items())
|
||||||
random.shuffle(list_)
|
random.shuffle(list_)
|
||||||
@@ -128,9 +127,9 @@ class TriviaSession():
|
|||||||
num_lists = len(list_names)
|
num_lists = len(list_names)
|
||||||
if num_lists > 2:
|
if num_lists > 2:
|
||||||
# at least 3 lists, join all but last with comma
|
# at least 3 lists, join all but last with comma
|
||||||
msg = ", ".join(list_names[:num_lists-1])
|
msg = ", ".join(list_names[: num_lists - 1])
|
||||||
# join onto last with "and"
|
# join onto last with "and"
|
||||||
msg = " and ".join((msg, list_names[num_lists-1]))
|
msg = " and ".join((msg, list_names[num_lists - 1]))
|
||||||
else:
|
else:
|
||||||
# either 1 or 2 lists, join together with "and"
|
# either 1 or 2 lists, join together with "and"
|
||||||
msg = " and ".join(list_names)
|
msg = " and ".join(list_names)
|
||||||
@@ -150,10 +149,7 @@ class TriviaSession():
|
|||||||
answers = _parse_answers(answers)
|
answers = _parse_answers(answers)
|
||||||
yield question, answers
|
yield question, answers
|
||||||
|
|
||||||
async def wait_for_answer(self,
|
async def wait_for_answer(self, answers, delay: float, timeout: float):
|
||||||
answers,
|
|
||||||
delay: float,
|
|
||||||
timeout: float):
|
|
||||||
"""Wait for a correct answer, and then respond.
|
"""Wait for a correct answer, and then respond.
|
||||||
|
|
||||||
Scores are also updated in this method.
|
Scores are also updated in this method.
|
||||||
@@ -178,7 +174,8 @@ class TriviaSession():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
message = await self.ctx.bot.wait_for(
|
message = await self.ctx.bot.wait_for(
|
||||||
"message", check=self.check_answer(answers), timeout=delay)
|
"message", check=self.check_answer(answers), timeout=delay
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if time.time() - self._last_response >= timeout:
|
if time.time() - self._last_response >= timeout:
|
||||||
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
|
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
|
||||||
@@ -194,8 +191,7 @@ class TriviaSession():
|
|||||||
await self.ctx.send(reply)
|
await self.ctx.send(reply)
|
||||||
else:
|
else:
|
||||||
self.scores[message.author] += 1
|
self.scores[message.author] += 1
|
||||||
reply = "You got it {}! **+1** to you!".format(
|
reply = "You got it {}! **+1** to you!".format(message.author.display_name)
|
||||||
message.author.display_name)
|
|
||||||
await self.ctx.send(reply)
|
await self.ctx.send(reply)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -218,9 +214,9 @@ class TriviaSession():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
answers = tuple(s.lower() for s in answers)
|
answers = tuple(s.lower() for s in answers)
|
||||||
|
|
||||||
def _pred(message: discord.Message):
|
def _pred(message: discord.Message):
|
||||||
early_exit = (message.channel != self.ctx.channel
|
early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me
|
||||||
or message.author == self.ctx.guild.me)
|
|
||||||
if early_exit:
|
if early_exit:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -260,8 +256,7 @@ class TriviaSession():
|
|||||||
"""Cancel whichever tasks this session is running."""
|
"""Cancel whichever tasks this session is running."""
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
channel = self.ctx.channel
|
channel = self.ctx.channel
|
||||||
LOG.debug("Force stopping trivia session; #%s in %s", channel,
|
LOG.debug("Force stopping trivia session; #%s in %s", channel, channel.guild.id)
|
||||||
channel.guild.id)
|
|
||||||
|
|
||||||
async def pay_winner(self, multiplier: float):
|
async def pay_winner(self, multiplier: float):
|
||||||
"""Pay the winner of this trivia session.
|
"""Pay the winner of this trivia session.
|
||||||
@@ -275,8 +270,7 @@ class TriviaSession():
|
|||||||
paid.
|
paid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
(winner, score) = next((tup for tup in self.scores.most_common(1)),
|
(winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))
|
||||||
(None, None))
|
|
||||||
me_ = self.ctx.guild.me
|
me_ = self.ctx.guild.me
|
||||||
if winner is not None and winner != me_ and score > 0:
|
if winner is not None and winner != me_ and score > 0:
|
||||||
contestants = list(self.scores.keys())
|
contestants = list(self.scores.keys())
|
||||||
@@ -285,13 +279,12 @@ class TriviaSession():
|
|||||||
if len(contestants) >= 3:
|
if len(contestants) >= 3:
|
||||||
amount = int(multiplier * score)
|
amount = int(multiplier * score)
|
||||||
if amount > 0:
|
if amount > 0:
|
||||||
LOG.debug("Paying trivia winner: %d credits --> %s",
|
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
|
||||||
amount, str(winner))
|
|
||||||
await deposit_credits(winner, int(multiplier * score))
|
await deposit_credits(winner, int(multiplier * score))
|
||||||
await self.ctx.send(
|
await self.ctx.send(
|
||||||
"Congratulations, {0}, you have received {1} credits"
|
"Congratulations, {0}, you have received {1} credits"
|
||||||
" for coming first.".format(winner.display_name,
|
" for coming first.".format(winner.display_name, amount)
|
||||||
amount))
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_answers(answers):
|
def _parse_answers(answers):
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ class Trivia:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.trivia_sessions = []
|
self.trivia_sessions = []
|
||||||
self.conf = Config.get_conf(
|
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
|
||||||
self, identifier=UNIQUE_ID, force_registration=True)
|
|
||||||
|
|
||||||
self.conf.register_guild(
|
self.conf.register_guild(
|
||||||
max_score=10,
|
max_score=10,
|
||||||
@@ -36,10 +35,10 @@ class Trivia:
|
|||||||
bot_plays=False,
|
bot_plays=False,
|
||||||
reveal_answer=True,
|
reveal_answer=True,
|
||||||
payout_multiplier=0.0,
|
payout_multiplier=0.0,
|
||||||
allow_override=True)
|
allow_override=True,
|
||||||
|
)
|
||||||
|
|
||||||
self.conf.register_member(
|
self.conf.register_member(wins=0, games=0, total_score=0)
|
||||||
wins=0, games=0, total_score=0)
|
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -60,7 +59,8 @@ class Trivia:
|
|||||||
"Payout multiplier: {payout_multiplier}\n"
|
"Payout multiplier: {payout_multiplier}\n"
|
||||||
"Allow lists to override settings: {allow_override}"
|
"Allow lists to override settings: {allow_override}"
|
||||||
"".format(**settings_dict),
|
"".format(**settings_dict),
|
||||||
lang="py")
|
lang="py",
|
||||||
|
)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@triviaset.command(name="maxscore")
|
@triviaset.command(name="maxscore")
|
||||||
@@ -81,8 +81,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.delay.set(seconds)
|
await settings.delay.set(seconds)
|
||||||
await ctx.send("Done. Maximum seconds to answer set to {}."
|
await ctx.send("Done. Maximum seconds to answer set to {}." "".format(seconds))
|
||||||
"".format(seconds))
|
|
||||||
|
|
||||||
@triviaset.command(name="stopafter")
|
@triviaset.command(name="stopafter")
|
||||||
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
||||||
@@ -92,38 +91,41 @@ class Trivia:
|
|||||||
await ctx.send("Must be larger than the answer time limit.")
|
await ctx.send("Must be larger than the answer time limit.")
|
||||||
return
|
return
|
||||||
await settings.timeout.set(seconds)
|
await settings.timeout.set(seconds)
|
||||||
await ctx.send("Done. Trivia sessions will now time out after {}"
|
await ctx.send(
|
||||||
" seconds of no responses.".format(seconds))
|
"Done. Trivia sessions will now time out after {}"
|
||||||
|
" seconds of no responses.".format(seconds)
|
||||||
|
)
|
||||||
|
|
||||||
@triviaset.command(name="override")
|
@triviaset.command(name="override")
|
||||||
async def triviaset_allowoverride(self,
|
async def triviaset_allowoverride(self, ctx: commands.Context, enabled: bool):
|
||||||
ctx: commands.Context,
|
|
||||||
enabled: bool):
|
|
||||||
"""Allow/disallow trivia lists to override settings."""
|
"""Allow/disallow trivia lists to override settings."""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.allow_override.set(enabled)
|
await settings.allow_override.set(enabled)
|
||||||
enabled = "now" if enabled else "no longer"
|
enabled = "now" if enabled else "no longer"
|
||||||
await ctx.send("Done. Trivia lists can {} override the trivia settings"
|
await ctx.send(
|
||||||
" for this server.".format(enabled))
|
"Done. Trivia lists can {} override the trivia settings"
|
||||||
|
" for this server.".format(enabled)
|
||||||
|
)
|
||||||
|
|
||||||
@triviaset.command(name="botplays")
|
@triviaset.command(name="botplays")
|
||||||
async def trivaset_bot_plays(self,
|
async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
|
||||||
ctx: commands.Context,
|
|
||||||
true_or_false: bool):
|
|
||||||
"""Set whether or not the bot gains points.
|
"""Set whether or not the bot gains points.
|
||||||
|
|
||||||
If enabled, the bot will gain a point if no one guesses correctly.
|
If enabled, the bot will gain a point if no one guesses correctly.
|
||||||
"""
|
"""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.bot_plays.set(true_or_false)
|
await settings.bot_plays.set(true_or_false)
|
||||||
await ctx.send("Done. " + (
|
await ctx.send(
|
||||||
"I'll gain a point if users don't answer in time." if true_or_false
|
"Done. "
|
||||||
else "Alright, I won't embarass you at trivia anymore."))
|
+ (
|
||||||
|
"I'll gain a point if users don't answer in time."
|
||||||
|
if true_or_false
|
||||||
|
else "Alright, I won't embarass you at trivia anymore."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@triviaset.command(name="revealanswer")
|
@triviaset.command(name="revealanswer")
|
||||||
async def trivaset_reveal_answer(self,
|
async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
|
||||||
ctx: commands.Context,
|
|
||||||
true_or_false: bool):
|
|
||||||
"""Set whether or not the answer is revealed.
|
"""Set whether or not the answer is revealed.
|
||||||
|
|
||||||
If enabled, the bot will reveal the answer if no one guesses correctly
|
If enabled, the bot will reveal the answer if no one guesses correctly
|
||||||
@@ -131,15 +133,18 @@ class Trivia:
|
|||||||
"""
|
"""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.reveal_answer.set(true_or_false)
|
await settings.reveal_answer.set(true_or_false)
|
||||||
await ctx.send("Done. " + (
|
await ctx.send(
|
||||||
"I'll reveal the answer if no one knows it." if true_or_false else
|
"Done. "
|
||||||
"I won't reveal the answer to the questions anymore."))
|
+ (
|
||||||
|
"I'll reveal the answer if no one knows it."
|
||||||
|
if true_or_false
|
||||||
|
else "I won't reveal the answer to the questions anymore."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@triviaset.command(name="payout")
|
@triviaset.command(name="payout")
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def triviaset_payout_multiplier(self,
|
async def triviaset_payout_multiplier(self, ctx: commands.Context, multiplier: float):
|
||||||
ctx: commands.Context,
|
|
||||||
multiplier: float):
|
|
||||||
"""Set the payout multiplier.
|
"""Set the payout multiplier.
|
||||||
|
|
||||||
This can be any positive decimal number. If a user wins trivia when at
|
This can be any positive decimal number. If a user wins trivia when at
|
||||||
@@ -155,8 +160,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
await settings.payout_multiplier.set(multiplier)
|
await settings.payout_multiplier.set(multiplier)
|
||||||
if not multiplier:
|
if not multiplier:
|
||||||
await ctx.send("Done. I will no longer reward the winner with a"
|
await ctx.send("Done. I will no longer reward the winner with a" " payout.")
|
||||||
" payout.")
|
|
||||||
return
|
return
|
||||||
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
||||||
|
|
||||||
@@ -174,8 +178,7 @@ class Trivia:
|
|||||||
categories = [c.lower() for c in categories]
|
categories = [c.lower() for c in categories]
|
||||||
session = self._get_trivia_session(ctx.channel)
|
session = self._get_trivia_session(ctx.channel)
|
||||||
if session is not None:
|
if session is not None:
|
||||||
await ctx.send(
|
await ctx.send("There is already an ongoing trivia session in this channel.")
|
||||||
"There is already an ongoing trivia session in this channel.")
|
|
||||||
return
|
return
|
||||||
trivia_dict = {}
|
trivia_dict = {}
|
||||||
authors = []
|
authors = []
|
||||||
@@ -185,21 +188,26 @@ class Trivia:
|
|||||||
try:
|
try:
|
||||||
dict_ = self.get_trivia_list(category)
|
dict_ = self.get_trivia_list(category)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
await ctx.send("Invalid category `{0}`. See `{1}trivia list`"
|
await ctx.send(
|
||||||
|
"Invalid category `{0}`. See `{1}trivia list`"
|
||||||
" for a list of trivia categories."
|
" for a list of trivia categories."
|
||||||
"".format(category, ctx.prefix))
|
"".format(category, ctx.prefix)
|
||||||
|
)
|
||||||
except InvalidListError:
|
except InvalidListError:
|
||||||
await ctx.send("There was an error parsing the trivia list for"
|
await ctx.send(
|
||||||
|
"There was an error parsing the trivia list for"
|
||||||
" the `{}` category. It may be formatted"
|
" the `{}` category. It may be formatted"
|
||||||
" incorrectly.".format(category))
|
" incorrectly.".format(category)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
trivia_dict.update(dict_)
|
trivia_dict.update(dict_)
|
||||||
authors.append(trivia_dict.pop("AUTHOR", None))
|
authors.append(trivia_dict.pop("AUTHOR", None))
|
||||||
continue
|
continue
|
||||||
return
|
return
|
||||||
if not trivia_dict:
|
if not trivia_dict:
|
||||||
await ctx.send("The trivia list was parsed successfully, however"
|
await ctx.send(
|
||||||
" it appears to be empty!")
|
"The trivia list was parsed successfully, however" " it appears to be empty!"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
settings = await self.conf.guild(ctx.guild).all()
|
settings = await self.conf.guild(ctx.guild).all()
|
||||||
config = trivia_dict.pop("CONFIG", None)
|
config = trivia_dict.pop("CONFIG", None)
|
||||||
@@ -215,13 +223,16 @@ class Trivia:
|
|||||||
"""Stop an ongoing trivia session."""
|
"""Stop an ongoing trivia session."""
|
||||||
session = self._get_trivia_session(ctx.channel)
|
session = self._get_trivia_session(ctx.channel)
|
||||||
if session is None:
|
if session is None:
|
||||||
await ctx.send(
|
await ctx.send("There is no ongoing trivia session in this channel.")
|
||||||
"There is no ongoing trivia session in this channel.")
|
|
||||||
return
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
auth_checks = (await ctx.bot.is_owner(author), await
|
auth_checks = (
|
||||||
ctx.bot.is_mod(author), await ctx.bot.is_admin(author),
|
await ctx.bot.is_owner(author),
|
||||||
author == ctx.guild.owner, author == session.ctx.author)
|
await ctx.bot.is_mod(author),
|
||||||
|
await ctx.bot.is_admin(author),
|
||||||
|
author == ctx.guild.owner,
|
||||||
|
author == session.ctx.author,
|
||||||
|
)
|
||||||
if any(auth_checks):
|
if any(auth_checks):
|
||||||
await session.end_game()
|
await session.end_game()
|
||||||
session.force_stop()
|
session.force_stop()
|
||||||
@@ -234,8 +245,7 @@ class Trivia:
|
|||||||
"""List available trivia categories."""
|
"""List available trivia categories."""
|
||||||
lists = set(p.stem for p in self._all_lists())
|
lists = set(p.stem for p in self._all_lists())
|
||||||
|
|
||||||
msg = box("**Available trivia lists**\n\n{}"
|
msg = box("**Available trivia lists**\n\n{}" "".format(", ".join(sorted(lists))))
|
||||||
"".format(", ".join(sorted(lists))))
|
|
||||||
if len(msg) > 1000:
|
if len(msg) > 1000:
|
||||||
await ctx.author.send(msg)
|
await ctx.author.send(msg)
|
||||||
return
|
return
|
||||||
@@ -256,10 +266,9 @@ class Trivia:
|
|||||||
|
|
||||||
@trivia_leaderboard.command(name="server")
|
@trivia_leaderboard.command(name="server")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def trivia_leaderboard_server(self,
|
async def trivia_leaderboard_server(
|
||||||
ctx: commands.Context,
|
self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
|
||||||
sort_by: str="wins",
|
):
|
||||||
top: int=10):
|
|
||||||
"""Leaderboard for this server.
|
"""Leaderboard for this server.
|
||||||
|
|
||||||
<sort_by> can be any of the following fields:
|
<sort_by> can be any of the following fields:
|
||||||
@@ -271,9 +280,11 @@ class Trivia:
|
|||||||
"""
|
"""
|
||||||
key = self._get_sort_key(sort_by)
|
key = self._get_sort_key(sort_by)
|
||||||
if key is None:
|
if key is None:
|
||||||
await ctx.send("Unknown field `{}`, see `{}help trivia "
|
await ctx.send(
|
||||||
|
"Unknown field `{}`, see `{}help trivia "
|
||||||
"leaderboard server` for valid fields to sort by."
|
"leaderboard server` for valid fields to sort by."
|
||||||
"".format(sort_by, ctx.prefix))
|
"".format(sort_by, ctx.prefix)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
data = await self.conf.all_members(guild)
|
data = await self.conf.all_members(guild)
|
||||||
@@ -282,10 +293,9 @@ class Trivia:
|
|||||||
await self.send_leaderboard(ctx, data, key, top)
|
await self.send_leaderboard(ctx, data, key, top)
|
||||||
|
|
||||||
@trivia_leaderboard.command(name="global")
|
@trivia_leaderboard.command(name="global")
|
||||||
async def trivia_leaderboard_global(self,
|
async def trivia_leaderboard_global(
|
||||||
ctx: commands.Context,
|
self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
|
||||||
sort_by: str="wins",
|
):
|
||||||
top: int=10):
|
|
||||||
"""Global trivia leaderboard.
|
"""Global trivia leaderboard.
|
||||||
|
|
||||||
<sort_by> can be any of the following fields:
|
<sort_by> can be any of the following fields:
|
||||||
@@ -298,9 +308,11 @@ class Trivia:
|
|||||||
"""
|
"""
|
||||||
key = self._get_sort_key(sort_by)
|
key = self._get_sort_key(sort_by)
|
||||||
if key is None:
|
if key is None:
|
||||||
await ctx.send("Unknown field `{}`, see `{}help trivia "
|
await ctx.send(
|
||||||
|
"Unknown field `{}`, see `{}help trivia "
|
||||||
"leaderboard global` for valid fields to sort by."
|
"leaderboard global` for valid fields to sort by."
|
||||||
"".format(sort_by, ctx.prefix))
|
"".format(sort_by, ctx.prefix)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
data = await self.conf.all_members()
|
data = await self.conf.all_members()
|
||||||
collated_data = {}
|
collated_data = {}
|
||||||
@@ -327,11 +339,7 @@ class Trivia:
|
|||||||
elif key in ("total", "score", "answers", "correct"):
|
elif key in ("total", "score", "answers", "correct"):
|
||||||
return "total_score"
|
return "total_score"
|
||||||
|
|
||||||
async def send_leaderboard(self,
|
async def send_leaderboard(self, ctx: commands.Context, data: dict, key: str, top: int):
|
||||||
ctx: commands.Context,
|
|
||||||
data: dict,
|
|
||||||
key: str,
|
|
||||||
top: int):
|
|
||||||
"""Send the leaderboard from the given data.
|
"""Send the leaderboard from the given data.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -382,23 +390,34 @@ class Trivia:
|
|||||||
items = sorted(items, key=lambda t: t[1][key], reverse=True)
|
items = sorted(items, key=lambda t: t[1][key], reverse=True)
|
||||||
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
|
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
|
||||||
# Headers
|
# Headers
|
||||||
headers = ("Rank", "Member{}".format(" " * (max_name_len - 6)), "Wins",
|
headers = (
|
||||||
"Games Played", "Total Score", "Average Score")
|
"Rank",
|
||||||
|
"Member{}".format(" " * (max_name_len - 6)),
|
||||||
|
"Wins",
|
||||||
|
"Games Played",
|
||||||
|
"Total Score",
|
||||||
|
"Average Score",
|
||||||
|
)
|
||||||
lines = [" | ".join(headers)]
|
lines = [" | ".join(headers)]
|
||||||
# Header underlines
|
# Header underlines
|
||||||
lines.append(" | ".join(("-" * len(h) for h in headers)))
|
lines.append(" | ".join(("-" * len(h) for h in headers)))
|
||||||
for rank, tup in enumerate(items, 1):
|
for rank, tup in enumerate(items, 1):
|
||||||
member, m_data = tup
|
member, m_data = tup
|
||||||
# Align fields to header width
|
# Align fields to header width
|
||||||
fields = tuple(map(str, (rank,
|
fields = tuple(
|
||||||
|
map(
|
||||||
|
str,
|
||||||
|
(
|
||||||
|
rank,
|
||||||
member,
|
member,
|
||||||
m_data["wins"],
|
m_data["wins"],
|
||||||
m_data["games"],
|
m_data["games"],
|
||||||
m_data["total_score"],
|
m_data["total_score"],
|
||||||
round(m_data["average_score"], 2))))
|
round(m_data["average_score"], 2),
|
||||||
padding = [
|
),
|
||||||
" " * (len(h) - len(f)) for h, f in zip(headers, fields)
|
)
|
||||||
]
|
)
|
||||||
|
padding = [" " * (len(h) - len(f)) for h, f in zip(headers, fields)]
|
||||||
fields = tuple(f + padding[i] for i, f in enumerate(fields))
|
fields = tuple(f + padding[i] for i, f in enumerate(fields))
|
||||||
lines.append(" | ".join(fields).format(member=member, **m_data))
|
lines.append(" | ".join(fields).format(member=member, **m_data))
|
||||||
if rank == top:
|
if rank == top:
|
||||||
@@ -418,8 +437,7 @@ class Trivia:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
channel = session.ctx.channel
|
channel = session.ctx.channel
|
||||||
LOG.debug("Ending trivia session; #%s in %s", channel,
|
LOG.debug("Ending trivia session; #%s in %s", channel, channel.guild.id)
|
||||||
channel.guild.id)
|
|
||||||
if session in self.trivia_sessions:
|
if session in self.trivia_sessions:
|
||||||
self.trivia_sessions.remove(session)
|
self.trivia_sessions.remove(session)
|
||||||
if session.scores:
|
if session.scores:
|
||||||
@@ -462,10 +480,9 @@ class Trivia:
|
|||||||
try:
|
try:
|
||||||
path = next(p for p in self._all_lists() if p.stem == category)
|
path = next(p for p in self._all_lists() if p.stem == category)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise FileNotFoundError("Could not find the `{}` category"
|
raise FileNotFoundError("Could not find the `{}` category" "".format(category))
|
||||||
"".format(category))
|
|
||||||
|
|
||||||
with path.open(encoding='utf-8') as file:
|
with path.open(encoding="utf-8") as file:
|
||||||
try:
|
try:
|
||||||
dict_ = yaml.load(file)
|
dict_ = yaml.load(file)
|
||||||
except yaml.error.YAMLError as exc:
|
except yaml.error.YAMLError as exc:
|
||||||
@@ -473,14 +490,13 @@ class Trivia:
|
|||||||
else:
|
else:
|
||||||
return dict_
|
return dict_
|
||||||
|
|
||||||
def _get_trivia_session(self,
|
def _get_trivia_session(self, channel: discord.TextChannel) -> TriviaSession:
|
||||||
channel: discord.TextChannel) -> TriviaSession:
|
return next(
|
||||||
return next((session for session in self.trivia_sessions
|
(session for session in self.trivia_sessions if session.ctx.channel == channel), None
|
||||||
if session.ctx.channel == channel), None)
|
)
|
||||||
|
|
||||||
def _all_lists(self):
|
def _all_lists(self):
|
||||||
personal_lists = tuple(p.resolve()
|
personal_lists = tuple(p.resolve() for p in cog_data_path(self).glob("*.yaml"))
|
||||||
for p in cog_data_path(self).glob("*.yaml"))
|
|
||||||
|
|
||||||
return personal_lists + tuple(ext_trivia.lists())
|
return personal_lists + tuple(ext_trivia.lists())
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from redbot.core.i18n import Translator
|
|||||||
_ = Translator("Warnings", __file__)
|
_ = Translator("Warnings", __file__)
|
||||||
|
|
||||||
|
|
||||||
async def warning_points_add_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
|
async def warning_points_add_check(
|
||||||
|
config: Config, ctx: commands.Context, user: discord.Member, points: int
|
||||||
|
):
|
||||||
"""Handles any action that needs to be taken or not based on the points"""
|
"""Handles any action that needs to be taken or not based on the points"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = config.guild(guild)
|
guild_settings = config.guild(guild)
|
||||||
@@ -24,7 +26,9 @@ async def warning_points_add_check(config: Config, ctx: commands.Context, user:
|
|||||||
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
||||||
|
|
||||||
|
|
||||||
async def warning_points_remove_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
|
async def warning_points_remove_check(
|
||||||
|
config: Config, ctx: commands.Context, user: discord.Member, points: int
|
||||||
|
):
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = config.guild(guild)
|
guild_settings = config.guild(guild)
|
||||||
act = {}
|
act = {}
|
||||||
@@ -38,7 +42,9 @@ async def warning_points_remove_check(config: Config, ctx: commands.Context, use
|
|||||||
await create_and_invoke_context(ctx, act["drop_command"], user)
|
await create_and_invoke_context(ctx, act["drop_command"], user)
|
||||||
|
|
||||||
|
|
||||||
async def create_and_invoke_context(realctx: commands.Context, command_str: str, user: discord.Member):
|
async def create_and_invoke_context(
|
||||||
|
realctx: commands.Context, command_str: str, user: discord.Member
|
||||||
|
):
|
||||||
m = copy(realctx.message)
|
m = copy(realctx.message)
|
||||||
m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
|
m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
|
||||||
fctx = await realctx.bot.get_context(m, cls=commands.Context)
|
fctx = await realctx.bot.get_context(m, cls=commands.Context)
|
||||||
@@ -54,7 +60,7 @@ def get_command_from_input(bot, userinput: str):
|
|||||||
while com is None:
|
while com is None:
|
||||||
com = bot.get_command(userinput)
|
com = bot.get_command(userinput)
|
||||||
if com is None:
|
if com is None:
|
||||||
userinput = ' '.join(userinput.split(' ')[:-1])
|
userinput = " ".join(userinput.split(" ")[:-1])
|
||||||
if len(userinput) == 0:
|
if len(userinput) == 0:
|
||||||
break
|
break
|
||||||
if com is None:
|
if com is None:
|
||||||
@@ -63,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
|
|||||||
check_str = inspect.getsource(checks.is_owner)
|
check_str = inspect.getsource(checks.is_owner)
|
||||||
if any(inspect.getsource(x) in check_str for x in com.checks):
|
if any(inspect.getsource(x) in check_str for x in com.checks):
|
||||||
# command the user specified has the is_owner check
|
# command the user specified has the is_owner check
|
||||||
return None, _("That command requires bot owner. I can't "
|
return None, _(
|
||||||
"allow you to use that for an action")
|
"That command requires bot owner. I can't " "allow you to use that for an action"
|
||||||
|
)
|
||||||
return "{prefix}" + orig, None
|
return "{prefix}" + orig, None
|
||||||
|
|
||||||
|
|
||||||
@@ -72,13 +79,15 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
"""Gets the command to be executed when the user is at or exceeding
|
"""Gets the command to be executed when the user is at or exceeding
|
||||||
the points threshold for the action"""
|
the points threshold for the action"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Enter the command to be run when the user exceeds the points for "
|
_(
|
||||||
|
"Enter the command to be run when the user exceeds the points for "
|
||||||
"this action to occur.\nEnter it exactly as you would if you were "
|
"this action to occur.\nEnter it exactly as you would if you were "
|
||||||
"actually trying to run the command, except don't put a prefix and "
|
"actually trying to run the command, except don't put a prefix and "
|
||||||
"use {user} in place of any user/member arguments\n\n"
|
"use {user} in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response.")
|
"Please wait 15 seconds before entering your response."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await asyncio.sleep(15)
|
await asyncio.sleep(15)
|
||||||
|
|
||||||
@@ -110,7 +119,8 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
when the user exceeded the threshold
|
when the user exceeded the threshold
|
||||||
"""
|
"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Enter the command to be run when the user returns to a value below "
|
_(
|
||||||
|
"Enter the command to be run when the user returns to a value below "
|
||||||
"the points for this action to occur. Please note that this is "
|
"the points for this action to occur. Please note that this is "
|
||||||
"intended to be used for reversal of the action taken when the user "
|
"intended to be used for reversal of the action taken when the user "
|
||||||
"exceeded the action's point value\nEnter it exactly as you would "
|
"exceeded the action's point value\nEnter it exactly as you would "
|
||||||
@@ -118,7 +128,8 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
"and use {user} in place of any user/member arguments\n\n"
|
"and use {user} in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response.")
|
"Please wait 15 seconds before entering your response."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await asyncio.sleep(15)
|
await asyncio.sleep(15)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
TO_TRANSLATE = ["../warnings.py", "../helpers.py"]
|
||||||
'../warnings.py',
|
|
||||||
'../helpers.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
def regen_messages():
|
||||||
subprocess.run(
|
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ from collections import namedtuple
|
|||||||
import discord
|
import discord
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \
|
from redbot.cogs.warnings.helpers import (
|
||||||
get_command_for_dropping_points, warning_points_remove_check
|
warning_points_add_check,
|
||||||
|
get_command_for_exceeded_points,
|
||||||
|
get_command_for_dropping_points,
|
||||||
|
warning_points_remove_check,
|
||||||
|
)
|
||||||
from redbot.core import Config, modlog, checks, commands
|
from redbot.core import Config, modlog, checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
@@ -18,17 +22,9 @@ _ = Translator("Warnings", __file__)
|
|||||||
class Warnings:
|
class Warnings:
|
||||||
"""A warning system for Red"""
|
"""A warning system for Red"""
|
||||||
|
|
||||||
default_guild = {
|
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
|
||||||
"actions": [],
|
|
||||||
"reasons": {},
|
|
||||||
"allow_custom_reasons": False
|
|
||||||
}
|
|
||||||
|
|
||||||
default_member = {
|
default_member = {"total_points": 0, "status": "", "warnings": {}}
|
||||||
"total_points": 0,
|
|
||||||
"status": "",
|
|
||||||
"warnings": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.config = Config.get_conf(self, identifier=5757575755)
|
self.config = Config.get_conf(self, identifier=5757575755)
|
||||||
@@ -41,9 +37,7 @@ class Warnings:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def register_warningtype():
|
async def register_warningtype():
|
||||||
try:
|
try:
|
||||||
await modlog.register_casetype(
|
await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
|
||||||
"warning", True, "\N{WARNING SIGN}", "Warning", None
|
|
||||||
)
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -77,7 +71,9 @@ class Warnings:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def action_add(self, ctx: commands.Context, name: str, points: int):
|
async def action_add(self, ctx: commands.Context, name: str, points: int):
|
||||||
"""Create an action to be taken at a specified point count
|
"""Create an action to be taken at a specified point count
|
||||||
Duplicate action names are not allowed"""
|
|
||||||
|
Duplicate action names are not allowed
|
||||||
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await ctx.send("Would you like to enter commands to be run? (y/n)")
|
await ctx.send("Would you like to enter commands to be run? (y/n)")
|
||||||
@@ -105,7 +101,7 @@ class Warnings:
|
|||||||
"action_name": name,
|
"action_name": name,
|
||||||
"points": points,
|
"points": points,
|
||||||
"exceed_command": exceed_command,
|
"exceed_command": exceed_command,
|
||||||
"drop_command": drop_command
|
"drop_command": drop_command,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Have all details for the action, now save the action
|
# Have all details for the action, now save the action
|
||||||
@@ -138,9 +134,7 @@ class Warnings:
|
|||||||
registered_actions.remove(to_remove)
|
registered_actions.remove(to_remove)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(_("No action named {} exists!").format(action_name))
|
||||||
_("No action named {} exists!").format(action_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -159,13 +153,8 @@ class Warnings:
|
|||||||
if name.lower() == "custom":
|
if name.lower() == "custom":
|
||||||
await ctx.send("That cannot be used as a reason name!")
|
await ctx.send("That cannot be used as a reason name!")
|
||||||
return
|
return
|
||||||
to_add = {
|
to_add = {"points": points, "description": description}
|
||||||
"points": points,
|
completed = {name.lower(): to_add}
|
||||||
"description": description
|
|
||||||
}
|
|
||||||
completed = {
|
|
||||||
name.lower(): to_add
|
|
||||||
}
|
|
||||||
|
|
||||||
guild_settings = self.config.guild(guild)
|
guild_settings = self.config.guild(guild)
|
||||||
|
|
||||||
@@ -219,8 +208,7 @@ class Warnings:
|
|||||||
msg_list.append(
|
msg_list.append(
|
||||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||||
"Drop command: {}".format(
|
"Drop command: {}".format(
|
||||||
r["action_name"], r["points"], r["exceed_command"],
|
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
||||||
r["drop_command"]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if msg_list:
|
if msg_list:
|
||||||
@@ -233,8 +221,9 @@ class Warnings:
|
|||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
|
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
|
||||||
"""Warn the user for the specified reason
|
"""Warn the user for the specified reason
|
||||||
Reason must be a registered reason, or custom if custom reasons are allowed"""
|
|
||||||
reason_type = {}
|
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
||||||
|
"""
|
||||||
if reason.lower() == "custom":
|
if reason.lower() == "custom":
|
||||||
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
||||||
if not custom_allowed:
|
if not custom_allowed:
|
||||||
@@ -242,9 +231,7 @@ class Warnings:
|
|||||||
_(
|
_(
|
||||||
"Custom reasons are not allowed! Please see {} for "
|
"Custom reasons are not allowed! Please see {} for "
|
||||||
"a complete list of valid reasons"
|
"a complete list of valid reasons"
|
||||||
).format(
|
).format("`{}reasonlist`".format(ctx.prefix))
|
||||||
"`{}reasonlist`".format(ctx.prefix)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
reason_type = await self.custom_warning_reason(ctx)
|
reason_type = await self.custom_warning_reason(ctx)
|
||||||
@@ -253,6 +240,7 @@ class Warnings:
|
|||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
if reason.lower() not in registered_reasons:
|
if reason.lower() not in registered_reasons:
|
||||||
await ctx.send(_("That is not a registered reason!"))
|
await ctx.send(_("That is not a registered reason!"))
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
reason_type = registered_reasons[reason.lower()]
|
reason_type = registered_reasons[reason.lower()]
|
||||||
|
|
||||||
@@ -262,7 +250,7 @@ class Warnings:
|
|||||||
str(ctx.message.id): {
|
str(ctx.message.id): {
|
||||||
"points": reason_type["points"],
|
"points": reason_type["points"],
|
||||||
"description": reason_type["description"],
|
"description": reason_type["description"],
|
||||||
"mod": ctx.author.id
|
"mod": ctx.author.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async with member_settings.warnings() as user_warnings:
|
async with member_settings.warnings() as user_warnings:
|
||||||
@@ -275,20 +263,19 @@ class Warnings:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def warnings(self, ctx: commands.Context, userid: int=None):
|
async def warnings(self, ctx: commands.Context, userid: int = None):
|
||||||
"""Show warnings for the specified user.
|
"""Show warnings for the specified user.
|
||||||
|
|
||||||
If userid is None, show warnings for the person running the command
|
If userid is None, show warnings for the person running the command
|
||||||
Note that showing warnings for users other than yourself requires
|
Note that showing warnings for users other than yourself requires
|
||||||
appropriate permissions"""
|
appropriate permissions
|
||||||
|
"""
|
||||||
if userid is None:
|
if userid is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
else:
|
else:
|
||||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
warning(
|
warning(_("You are not allowed to check " "warnings for other users!"))
|
||||||
_("You are not allowed to check "
|
|
||||||
"warnings for other users!")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -305,22 +292,14 @@ class Warnings:
|
|||||||
mod = ctx.guild.get_member(user_warnings[key]["mod"])
|
mod = ctx.guild.get_member(user_warnings[key]["mod"])
|
||||||
if mod is None:
|
if mod is None:
|
||||||
mod = discord.utils.get(
|
mod = discord.utils.get(
|
||||||
self.bot.get_all_members(),
|
self.bot.get_all_members(), id=user_warnings[key]["mod"]
|
||||||
id=user_warnings[key]["mod"]
|
|
||||||
)
|
)
|
||||||
if mod is None:
|
if mod is None:
|
||||||
mod = await self.bot.get_user_info(
|
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
|
||||||
user_warnings[key]["mod"]
|
|
||||||
)
|
|
||||||
msg += "{} point warning {} issued by {} for {}\n".format(
|
msg += "{} point warning {} issued by {} for {}\n".format(
|
||||||
user_warnings[key]["points"],
|
user_warnings[key]["points"], key, mod, user_warnings[key]["description"]
|
||||||
key,
|
|
||||||
mod,
|
|
||||||
user_warnings[key]["description"]
|
|
||||||
)
|
|
||||||
await ctx.send_interactive(
|
|
||||||
pagify(msg), box_lang="Warnings for {}".format(user)
|
|
||||||
)
|
)
|
||||||
|
await ctx.send_interactive(pagify(msg), box_lang="Warnings for {}".format(user))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -348,10 +327,7 @@ class Warnings:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def custom_warning_reason(ctx: commands.Context):
|
async def custom_warning_reason(ctx: commands.Context):
|
||||||
"""Handles getting description and points for custom reasons"""
|
"""Handles getting description and points for custom reasons"""
|
||||||
to_add = {
|
to_add = {"points": 0, "description": ""}
|
||||||
"points": 0,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
def same_author_check(m):
|
def same_author_check(m):
|
||||||
return m.author == ctx.author
|
return m.author == ctx.author
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ __all__ = ["Config", "__version__"]
|
|||||||
|
|
||||||
|
|
||||||
class VersionInfo:
|
class VersionInfo:
|
||||||
|
|
||||||
def __init__(self, major, minor, micro, releaselevel, serial):
|
def __init__(self, major, minor, micro, releaselevel, serial):
|
||||||
self._levels = ['alpha', 'beta', 'final']
|
self._levels = ["alpha", "beta", "final"]
|
||||||
self.major = major
|
self.major = major
|
||||||
self.minor = minor
|
self.minor = minor
|
||||||
self.micro = micro
|
self.micro = micro
|
||||||
|
|
||||||
if releaselevel not in self._levels:
|
if releaselevel not in self._levels:
|
||||||
raise TypeError("'releaselevel' must be one of: {}".format(
|
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels)))
|
||||||
', '.join(self._levels)
|
|
||||||
))
|
|
||||||
|
|
||||||
self.releaselevel = releaselevel
|
self.releaselevel = releaselevel
|
||||||
self.serial = serial
|
self.serial = serial
|
||||||
@@ -21,8 +20,13 @@ class VersionInfo:
|
|||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
my_index = self._levels.index(self.releaselevel)
|
my_index = self._levels.index(self.releaselevel)
|
||||||
other_index = self._levels.index(other.releaselevel)
|
other_index = self._levels.index(other.releaselevel)
|
||||||
return (self.major, self.minor, self.micro, my_index, self.serial) < \
|
return (self.major, self.minor, self.micro, my_index, self.serial) < (
|
||||||
(other.major, other.minor, other.micro, other_index, other.serial)
|
other.major,
|
||||||
|
other.minor,
|
||||||
|
other.micro,
|
||||||
|
other_index,
|
||||||
|
other.serial,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
|
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
|
||||||
@@ -32,5 +36,6 @@ class VersionInfo:
|
|||||||
def to_json(self):
|
def to_json(self):
|
||||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||||
|
|
||||||
__version__ = "3.0.0b14"
|
|
||||||
version_info = VersionInfo(3, 0, 0, 'beta', 14)
|
__version__ = "3.0.0b15"
|
||||||
|
version_info = VersionInfo(3, 0, 0, "beta", 15)
|
||||||
|
|||||||
@@ -6,29 +6,36 @@ import discord
|
|||||||
|
|
||||||
from redbot.core import Config
|
from redbot.core import Config
|
||||||
|
|
||||||
__all__ = ["Account", "get_balance", "set_balance", "withdraw_credits", "deposit_credits",
|
__all__ = [
|
||||||
"can_spend", "transfer_credits", "wipe_bank", "get_account", "is_global",
|
"Account",
|
||||||
"set_global", "get_bank_name", "set_bank_name", "get_currency_name",
|
"get_balance",
|
||||||
"set_currency_name", "get_default_balance", "set_default_balance"]
|
"set_balance",
|
||||||
|
"withdraw_credits",
|
||||||
|
"deposit_credits",
|
||||||
|
"can_spend",
|
||||||
|
"transfer_credits",
|
||||||
|
"wipe_bank",
|
||||||
|
"get_account",
|
||||||
|
"is_global",
|
||||||
|
"set_global",
|
||||||
|
"get_bank_name",
|
||||||
|
"set_bank_name",
|
||||||
|
"get_currency_name",
|
||||||
|
"set_currency_name",
|
||||||
|
"get_default_balance",
|
||||||
|
"set_default_balance",
|
||||||
|
]
|
||||||
|
|
||||||
_DEFAULT_GLOBAL = {
|
_DEFAULT_GLOBAL = {
|
||||||
"is_global": False,
|
"is_global": False,
|
||||||
"bank_name": "Twentysix bank",
|
"bank_name": "Twentysix bank",
|
||||||
"currency": "credits",
|
"currency": "credits",
|
||||||
"default_balance": 100
|
"default_balance": 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT_GUILD = {
|
_DEFAULT_GUILD = {"bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100}
|
||||||
"bank_name": "Twentysix bank",
|
|
||||||
"currency": "credits",
|
|
||||||
"default_balance": 100
|
|
||||||
}
|
|
||||||
|
|
||||||
_DEFAULT_MEMBER = {
|
_DEFAULT_MEMBER = {"name": "", "balance": 0, "created_at": 0}
|
||||||
"name": "",
|
|
||||||
"balance": 0,
|
|
||||||
"created_at": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_DEFAULT_USER = _DEFAULT_MEMBER
|
_DEFAULT_USER = _DEFAULT_MEMBER
|
||||||
|
|
||||||
@@ -50,9 +57,9 @@ def _register_defaults():
|
|||||||
_conf.register_member(**_DEFAULT_MEMBER)
|
_conf.register_member(**_DEFAULT_MEMBER)
|
||||||
_conf.register_user(**_DEFAULT_USER)
|
_conf.register_user(**_DEFAULT_USER)
|
||||||
|
|
||||||
if not os.environ.get('BUILDING_DOCS'):
|
|
||||||
_conf = Config.get_conf(
|
if not os.environ.get("BUILDING_DOCS"):
|
||||||
None, 384734293238749, cog_name="Bank", force_registration=True)
|
_conf = Config.get_conf(None, 384734293238749, cog_name="Bank", force_registration=True)
|
||||||
_register_defaults()
|
_register_defaults()
|
||||||
|
|
||||||
|
|
||||||
@@ -285,7 +292,7 @@ async def wipe_bank():
|
|||||||
await _conf.clear_all_members()
|
await _conf.clear_all_members()
|
||||||
|
|
||||||
|
|
||||||
async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> List[tuple]:
|
async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:
|
||||||
"""
|
"""
|
||||||
Gets the bank's leaderboard
|
Gets the bank's leaderboard
|
||||||
|
|
||||||
@@ -319,14 +326,16 @@ async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> Lis
|
|||||||
if guild is None:
|
if guild is None:
|
||||||
raise TypeError("Expected a guild, got NoneType object instead!")
|
raise TypeError("Expected a guild, got NoneType object instead!")
|
||||||
raw_accounts = await _conf.all_members(guild)
|
raw_accounts = await _conf.all_members(guild)
|
||||||
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]['balance'], reverse=True)
|
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]["balance"], reverse=True)
|
||||||
if positions is None:
|
if positions is None:
|
||||||
return sorted_acc
|
return sorted_acc
|
||||||
else:
|
else:
|
||||||
return sorted_acc[:positions]
|
return sorted_acc[:positions]
|
||||||
|
|
||||||
|
|
||||||
async def get_leaderboard_position(member: Union[discord.User, discord.Member]) -> Union[int, None]:
|
async def get_leaderboard_position(
|
||||||
|
member: Union[discord.User, discord.Member]
|
||||||
|
) -> Union[int, None]:
|
||||||
"""
|
"""
|
||||||
Get the leaderboard position for the specified user
|
Get the leaderboard position for the specified user
|
||||||
|
|
||||||
@@ -387,13 +396,13 @@ async def get_account(member: Union[discord.Member, discord.User]) -> Account:
|
|||||||
|
|
||||||
if acc_data == {}:
|
if acc_data == {}:
|
||||||
acc_data = default
|
acc_data = default
|
||||||
acc_data['name'] = member.display_name
|
acc_data["name"] = member.display_name
|
||||||
try:
|
try:
|
||||||
acc_data['balance'] = await get_default_balance(member.guild)
|
acc_data["balance"] = await get_default_balance(member.guild)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
acc_data['balance'] = await get_default_balance()
|
acc_data["balance"] = await get_default_balance()
|
||||||
|
|
||||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
acc_data["created_at"] = _decode_time(acc_data["created_at"])
|
||||||
return Account(**acc_data)
|
return Account(**acc_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -444,7 +453,7 @@ async def set_global(global_: bool) -> bool:
|
|||||||
return global_
|
return global_
|
||||||
|
|
||||||
|
|
||||||
async def get_bank_name(guild: discord.Guild=None) -> str:
|
async def get_bank_name(guild: discord.Guild = None) -> str:
|
||||||
"""Get the current bank name.
|
"""Get the current bank name.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -472,7 +481,7 @@ async def get_bank_name(guild: discord.Guild=None) -> str:
|
|||||||
raise RuntimeError("Guild parameter is required and missing.")
|
raise RuntimeError("Guild parameter is required and missing.")
|
||||||
|
|
||||||
|
|
||||||
async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
|
async def set_bank_name(name: str, guild: discord.Guild = None) -> str:
|
||||||
"""Set the bank name.
|
"""Set the bank name.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -499,12 +508,13 @@ async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
|
|||||||
elif guild is not None:
|
elif guild is not None:
|
||||||
await _conf.guild(guild).bank_name.set(name)
|
await _conf.guild(guild).bank_name.set(name)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Guild must be provided if setting the name of a guild"
|
raise RuntimeError(
|
||||||
"-specific bank.")
|
"Guild must be provided if setting the name of a guild" "-specific bank."
|
||||||
|
)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
async def get_currency_name(guild: discord.Guild=None) -> str:
|
async def get_currency_name(guild: discord.Guild = None) -> str:
|
||||||
"""Get the currency name of the bank.
|
"""Get the currency name of the bank.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -532,7 +542,7 @@ async def get_currency_name(guild: discord.Guild=None) -> str:
|
|||||||
raise RuntimeError("Guild must be provided.")
|
raise RuntimeError("Guild must be provided.")
|
||||||
|
|
||||||
|
|
||||||
async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
|
async def set_currency_name(name: str, guild: discord.Guild = None) -> str:
|
||||||
"""Set the currency name for the bank.
|
"""Set the currency name for the bank.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -559,12 +569,13 @@ async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
|
|||||||
elif guild is not None:
|
elif guild is not None:
|
||||||
await _conf.guild(guild).currency.set(name)
|
await _conf.guild(guild).currency.set(name)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Guild must be provided if setting the currency"
|
raise RuntimeError(
|
||||||
" name of a guild-specific bank.")
|
"Guild must be provided if setting the currency" " name of a guild-specific bank."
|
||||||
|
)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
async def get_default_balance(guild: discord.Guild=None) -> int:
|
async def get_default_balance(guild: discord.Guild = None) -> int:
|
||||||
"""Get the current default balance amount.
|
"""Get the current default balance amount.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -592,7 +603,7 @@ async def get_default_balance(guild: discord.Guild=None) -> int:
|
|||||||
raise RuntimeError("Guild is missing and required!")
|
raise RuntimeError("Guild is missing and required!")
|
||||||
|
|
||||||
|
|
||||||
async def set_default_balance(amount: int, guild: discord.Guild=None) -> int:
|
async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
|
||||||
"""Set the default balance amount.
|
"""Set the default balance amount.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -14,32 +14,16 @@ from discord.ext.commands import when_mentioned_or
|
|||||||
|
|
||||||
# This supresses the PyNaCl warning that isn't relevant here
|
# This supresses the PyNaCl warning that isn't relevant here
|
||||||
from discord.voice_client import VoiceClient
|
from discord.voice_client import VoiceClient
|
||||||
|
|
||||||
VoiceClient.warn_nacl = False
|
VoiceClient.warn_nacl = False
|
||||||
|
|
||||||
from .cog_manager import CogManager
|
from .cog_manager import CogManager
|
||||||
from . import (
|
from . import Config, i18n, commands, rpc
|
||||||
Config,
|
|
||||||
i18n,
|
|
||||||
commands,
|
|
||||||
rpc
|
|
||||||
)
|
|
||||||
from .help_formatter import Help, help as help_
|
from .help_formatter import Help, help as help_
|
||||||
from .sentry import SentryManager
|
from .sentry import SentryManager
|
||||||
from .utils import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiohttp_json_rpc import JsonRpc
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
class RpcMethodMixin:
|
|
||||||
async def rpc__cogs(self, request):
|
|
||||||
return list(self.cogs.keys())
|
|
||||||
|
|
||||||
async def rpc__extensions(self, request):
|
|
||||||
return list(self.extensions.keys())
|
|
||||||
|
|
||||||
|
|
||||||
class RedBase(BotBase, RpcMethodMixin):
|
class RedBase(BotBase):
|
||||||
"""Mixin for the main bot class.
|
"""Mixin for the main bot class.
|
||||||
|
|
||||||
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
||||||
@@ -48,7 +32,8 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
|
|
||||||
Selfbots should inherit from this mixin along with `discord.Client`.
|
Selfbots should inherit from this mixin along with `discord.Client`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
|
|
||||||
|
def __init__(self, cli_flags, bot_dir: Path = Path.cwd(), **kwargs):
|
||||||
self._shutdown_mode = ExitCodes.CRITICAL
|
self._shutdown_mode = ExitCodes.CRITICAL
|
||||||
self.db = Config.get_core_conf(force_registration=True)
|
self.db = Config.get_core_conf(force_registration=True)
|
||||||
self._co_owners = cli_flags.co_owner
|
self._co_owners = cli_flags.co_owner
|
||||||
@@ -62,8 +47,12 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
whitelist=[],
|
whitelist=[],
|
||||||
blacklist=[],
|
blacklist=[],
|
||||||
enable_sentry=None,
|
enable_sentry=None,
|
||||||
locale='en',
|
locale="en",
|
||||||
embeds=True
|
embeds=True,
|
||||||
|
color=15158332,
|
||||||
|
help__page_char_limit=1000,
|
||||||
|
help__max_pages_in_guild=2,
|
||||||
|
help__tagline="",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.register_guild(
|
self.db.register_guild(
|
||||||
@@ -72,12 +61,11 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
blacklist=[],
|
blacklist=[],
|
||||||
admin_role=None,
|
admin_role=None,
|
||||||
mod_role=None,
|
mod_role=None,
|
||||||
embeds=None
|
embeds=None,
|
||||||
|
use_bot_color=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.register_user(
|
self.db.register_user(embeds=None)
|
||||||
embeds=None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def prefix_manager(bot, message):
|
async def prefix_manager(bot, message):
|
||||||
if not cli_flags.prefix:
|
if not cli_flags.prefix:
|
||||||
@@ -88,9 +76,11 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
return global_prefix
|
return global_prefix
|
||||||
server_prefix = await bot.db.guild(message.guild).prefix()
|
server_prefix = await bot.db.guild(message.guild).prefix()
|
||||||
if cli_flags.mentionable:
|
if cli_flags.mentionable:
|
||||||
return when_mentioned_or(*server_prefix)(bot, message) \
|
return (
|
||||||
if server_prefix else \
|
when_mentioned_or(*server_prefix)(bot, message)
|
||||||
when_mentioned_or(*global_prefix)(bot, message)
|
if server_prefix
|
||||||
|
else when_mentioned_or(*global_prefix)(bot, message)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return server_prefix if server_prefix else global_prefix
|
return server_prefix if server_prefix else global_prefix
|
||||||
|
|
||||||
@@ -104,18 +94,23 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(self._dict_abuse(kwargs))
|
loop.run_until_complete(self._dict_abuse(kwargs))
|
||||||
|
|
||||||
|
if "command_not_found" not in kwargs:
|
||||||
|
kwargs["command_not_found"] = "Command {} not found.\n{}"
|
||||||
|
|
||||||
self.counter = Counter()
|
self.counter = Counter()
|
||||||
self.uptime = None
|
self.uptime = None
|
||||||
|
self.color = None
|
||||||
|
|
||||||
self.main_dir = bot_dir
|
self.main_dir = bot_dir
|
||||||
|
|
||||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
|
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||||
|
|
||||||
self.register_rpc_methods()
|
|
||||||
|
|
||||||
super().__init__(formatter=Help(), **kwargs)
|
super().__init__(formatter=Help(), **kwargs)
|
||||||
|
|
||||||
self.remove_command('help')
|
if self.rpc_enabled:
|
||||||
|
self.rpc = rpc.RPC(self)
|
||||||
|
|
||||||
|
self.remove_command("help")
|
||||||
|
|
||||||
self.add_command(help_)
|
self.add_command(help_)
|
||||||
|
|
||||||
@@ -124,7 +119,7 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
def enable_sentry(self):
|
def enable_sentry(self):
|
||||||
"""Enable Sentry logging for Red."""
|
"""Enable Sentry logging for Red."""
|
||||||
if self._sentry_mgr is None:
|
if self._sentry_mgr is None:
|
||||||
sentry_log = logging.getLogger('red.sentry')
|
sentry_log = logging.getLogger("red.sentry")
|
||||||
sentry_log.setLevel(logging.WARNING)
|
sentry_log.setLevel(logging.WARNING)
|
||||||
self._sentry_mgr = SentryManager(sentry_log)
|
self._sentry_mgr = SentryManager(sentry_log)
|
||||||
self._sentry_mgr.enable()
|
self._sentry_mgr.enable()
|
||||||
@@ -143,7 +138,7 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
indict['owner_id'] = await self.db.owner()
|
indict["owner_id"] = await self.db.owner()
|
||||||
i18n.set_locale(await self.db.locale())
|
i18n.set_locale(await self.db.locale())
|
||||||
|
|
||||||
async def embed_requested(self, channel, user, command=None) -> bool:
|
async def embed_requested(self, channel, user, command=None) -> bool:
|
||||||
@@ -214,14 +209,14 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
curr_pkgs.remove(pkg_name)
|
curr_pkgs.remove(pkg_name)
|
||||||
|
|
||||||
async def load_extension(self, spec: ModuleSpec):
|
async def load_extension(self, spec: ModuleSpec):
|
||||||
name = spec.name.split('.')[-1]
|
name = spec.name.split(".")[-1]
|
||||||
if name in self.extensions:
|
if name in self.extensions:
|
||||||
return
|
return
|
||||||
|
|
||||||
lib = spec.loader.load_module()
|
lib = spec.loader.load_module()
|
||||||
if not hasattr(lib, 'setup'):
|
if not hasattr(lib, "setup"):
|
||||||
del lib
|
del lib
|
||||||
raise discord.ClientException('extension does not have a setup function')
|
raise discord.ClientException("extension does not have a setup function")
|
||||||
|
|
||||||
if asyncio.iscoroutinefunction(lib.setup):
|
if asyncio.iscoroutinefunction(lib.setup):
|
||||||
await lib.setup(self)
|
await lib.setup(self)
|
||||||
@@ -262,7 +257,7 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
del event_list[index]
|
del event_list[index]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
func = getattr(lib, 'teardown')
|
func = getattr(lib, "teardown")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -279,19 +274,16 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
if m.startswith(pkg_name):
|
if m.startswith(pkg_name):
|
||||||
del sys.modules[m]
|
del sys.modules[m]
|
||||||
|
|
||||||
if pkg_name.startswith('redbot.cogs'):
|
if pkg_name.startswith("redbot.cogs"):
|
||||||
del sys.modules['redbot.cogs'].__dict__[name]
|
del sys.modules["redbot.cogs"].__dict__[name]
|
||||||
|
|
||||||
def register_rpc_methods(self):
|
|
||||||
rpc.add_method('bot', self.rpc__cogs)
|
|
||||||
rpc.add_method('bot', self.rpc__extensions)
|
|
||||||
|
|
||||||
|
|
||||||
class Red(RedBase, discord.AutoShardedClient):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
You're welcome Caleb.
|
You're welcome Caleb.
|
||||||
"""
|
"""
|
||||||
async def shutdown(self, *, restart: bool=False):
|
|
||||||
|
async def shutdown(self, *, restart: bool = False):
|
||||||
"""Gracefully quit Red.
|
"""Gracefully quit Red.
|
||||||
|
|
||||||
The program will exit with code :code:`0` by default.
|
The program will exit with code :code:`0` by default.
|
||||||
|
|||||||
@@ -5,23 +5,22 @@ from discord.ext import commands
|
|||||||
async def check_overrides(ctx, *, level):
|
async def check_overrides(ctx, *, level):
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
return True
|
return True
|
||||||
perm_cog = ctx.bot.get_cog('Permissions')
|
perm_cog = ctx.bot.get_cog("Permissions")
|
||||||
if not perm_cog or ctx.cog == perm_cog:
|
if not perm_cog or ctx.cog == perm_cog:
|
||||||
return None
|
return None
|
||||||
# don't break if someone loaded a cog named
|
# don't break if someone loaded a cog named
|
||||||
# permissions that doesn't implement this
|
# permissions that doesn't implement this
|
||||||
func = getattr(perm_cog, 'check_overrides', None)
|
func = getattr(perm_cog, "check_overrides", None)
|
||||||
val = None if func is None else await func(ctx, level)
|
val = None if func is None else await func(ctx, level)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def is_owner(**kwargs):
|
def is_owner(**kwargs):
|
||||||
|
|
||||||
async def check(ctx):
|
async def check(ctx):
|
||||||
override = await check_overrides(ctx, level='owner')
|
override = await check_overrides(ctx, level="owner")
|
||||||
return (
|
return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||||
override if override is not None
|
|
||||||
else await ctx.bot.is_owner(ctx.author, **kwargs)
|
|
||||||
)
|
|
||||||
return commands.check(check)
|
return commands.check(check)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,9 +31,8 @@ async def check_permissions(ctx, perms):
|
|||||||
return False
|
return False
|
||||||
resolved = ctx.channel.permissions_for(ctx.author)
|
resolved = ctx.channel.permissions_for(ctx.author)
|
||||||
|
|
||||||
return all(
|
return resolved.administrator or all(
|
||||||
getattr(resolved, name, None) == value
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
for name, value in perms.items()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -75,47 +73,49 @@ async def is_admin_or_superior(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def mod_or_permissions(**perms):
|
def mod_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
override = await check_overrides(ctx, level='mod')
|
override = await check_overrides(ctx, level="mod")
|
||||||
return (
|
return (
|
||||||
override if override is not None
|
override
|
||||||
else await check_permissions(ctx, perms)
|
if override is not None
|
||||||
or await is_mod_or_superior(ctx)
|
else await check_permissions(ctx, perms) or await is_mod_or_superior(ctx)
|
||||||
)
|
)
|
||||||
|
|
||||||
return commands.check(predicate)
|
return commands.check(predicate)
|
||||||
|
|
||||||
|
|
||||||
def admin_or_permissions(**perms):
|
def admin_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
override = await check_overrides(ctx, level='admin')
|
override = await check_overrides(ctx, level="admin")
|
||||||
return (
|
return (
|
||||||
override if override is not None
|
override
|
||||||
else await check_permissions(ctx, perms)
|
if override is not None
|
||||||
or await is_admin_or_superior(ctx)
|
else await check_permissions(ctx, perms) or await is_admin_or_superior(ctx)
|
||||||
)
|
)
|
||||||
|
|
||||||
return commands.check(predicate)
|
return commands.check(predicate)
|
||||||
|
|
||||||
|
|
||||||
def bot_in_a_guild(**kwargs):
|
def bot_in_a_guild(**kwargs):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
return len(ctx.bot.guilds) > 0
|
return len(ctx.bot.guilds) > 0
|
||||||
|
|
||||||
return commands.check(predicate)
|
return commands.check(predicate)
|
||||||
|
|
||||||
|
|
||||||
def guildowner_or_permissions(**perms):
|
def guildowner_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||||
if ctx.guild is None:
|
if ctx.guild is None:
|
||||||
return has_perms_or_is_owner
|
return has_perms_or_is_owner
|
||||||
is_guild_owner = ctx.author == ctx.guild.owner
|
is_guild_owner = ctx.author == ctx.guild.owner
|
||||||
|
|
||||||
override = await check_overrides(ctx, level='guildowner')
|
override = await check_overrides(ctx, level="guildowner")
|
||||||
return (
|
return override if override is not None else is_guild_owner or has_perms_or_is_owner
|
||||||
override if override is not None
|
|
||||||
else is_guild_owner or has_perms_or_is_owner
|
|
||||||
)
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
return commands.check(predicate)
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ def interactive_config(red, token_set, prefix_set):
|
|||||||
|
|
||||||
if not prefix_set:
|
if not prefix_set:
|
||||||
prefix = ""
|
prefix = ""
|
||||||
print("\nPick a prefix. A prefix is what you type before a "
|
print(
|
||||||
|
"\nPick a prefix. A prefix is what you type before a "
|
||||||
"command. Example:\n"
|
"command. Example:\n"
|
||||||
"!help\n^ The exclamation mark is the prefix in this case.\n"
|
"!help\n^ The exclamation mark is the prefix in this case.\n"
|
||||||
"Can be multiple characters. You will be able to change it "
|
"Can be multiple characters. You will be able to change it "
|
||||||
"later and add more of them.\nChoose your prefix:\n")
|
"later and add more of them.\nChoose your prefix:\n"
|
||||||
|
)
|
||||||
while not prefix:
|
while not prefix:
|
||||||
prefix = input("Prefix> ")
|
prefix = input("Prefix> ")
|
||||||
if len(prefix) > 10:
|
if len(prefix) > 10:
|
||||||
print("Your prefix seems overly long. Are you sure it "
|
print("Your prefix seems overly long. Are you sure it " "is correct? (y/n)")
|
||||||
"is correct? (y/n)")
|
|
||||||
if not confirm("> "):
|
if not confirm("> "):
|
||||||
prefix = ""
|
prefix = ""
|
||||||
if prefix:
|
if prefix:
|
||||||
@@ -48,12 +49,14 @@ def interactive_config(red, token_set, prefix_set):
|
|||||||
|
|
||||||
def ask_sentry(red: Red):
|
def ask_sentry(red: Red):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
print("\nThank you for installing Red V3 beta! The current version\n"
|
print(
|
||||||
|
"\nThank you for installing Red V3 beta! The current version\n"
|
||||||
" is not suited for production use and is aimed at testing\n"
|
" is not suited for production use and is aimed at testing\n"
|
||||||
" the current and upcoming featureset, that's why we will\n"
|
" the current and upcoming featureset, that's why we will\n"
|
||||||
" also collect the fatal error logs to help us fix any new\n"
|
" also collect the fatal error logs to help us fix any new\n"
|
||||||
" found issues in a timely manner. If you wish to opt in\n"
|
" found issues in a timely manner. If you wish to opt in\n"
|
||||||
" the process please type \"yes\":\n")
|
' the process please type "yes":\n'
|
||||||
|
)
|
||||||
if not confirm("> "):
|
if not confirm("> "):
|
||||||
loop.run_until_complete(red.db.enable_sentry.set(False))
|
loop.run_until_complete(red.db.enable_sentry.set(False))
|
||||||
else:
|
else:
|
||||||
@@ -62,64 +65,82 @@ def ask_sentry(red: Red):
|
|||||||
|
|
||||||
|
|
||||||
def parse_cli_flags(args):
|
def parse_cli_flags(args):
|
||||||
parser = argparse.ArgumentParser(description="Red - Discord Bot",
|
parser = argparse.ArgumentParser(
|
||||||
usage="redbot <instance_name> [arguments]")
|
description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
|
||||||
parser.add_argument("--version", "-V", action="store_true",
|
)
|
||||||
help="Show Red's current version")
|
parser.add_argument("--version", "-V", action="store_true", help="Show Red's current version")
|
||||||
parser.add_argument("--list-instances", action="store_true",
|
parser.add_argument(
|
||||||
help="List all instance names setup "
|
"--list-instances",
|
||||||
"with 'redbot-setup'")
|
action="store_true",
|
||||||
parser.add_argument("--owner", type=int,
|
help="List all instance names setup " "with 'redbot-setup'",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--owner",
|
||||||
|
type=int,
|
||||||
help="ID of the owner. Only who hosts "
|
help="ID of the owner. Only who hosts "
|
||||||
"Red should be owner, this has "
|
"Red should be owner, this has "
|
||||||
"serious security implications if misused.")
|
"serious security implications if misused.",
|
||||||
parser.add_argument("--co-owner", type=int, default=[], nargs="*",
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--co-owner",
|
||||||
|
type=int,
|
||||||
|
default=[],
|
||||||
|
nargs="*",
|
||||||
help="ID of a co-owner. Only people who have access "
|
help="ID of a co-owner. Only people who have access "
|
||||||
"to the system that is hosting Red should be "
|
"to the system that is hosting Red should be "
|
||||||
"co-owners, as this gives them complete access "
|
"co-owners, as this gives them complete access "
|
||||||
"to the system's data. This has serious "
|
"to the system's data. This has serious "
|
||||||
"security implications if misused. Can be "
|
"security implications if misused. Can be "
|
||||||
"multiple.")
|
"multiple.",
|
||||||
parser.add_argument("--prefix", "-p", action="append",
|
)
|
||||||
help="Global prefix. Can be multiple")
|
parser.add_argument("--prefix", "-p", action="append", help="Global prefix. Can be multiple")
|
||||||
parser.add_argument("--no-prompt", action="store_true",
|
parser.add_argument(
|
||||||
|
"--no-prompt",
|
||||||
|
action="store_true",
|
||||||
help="Disables console inputs. Features requiring "
|
help="Disables console inputs. Features requiring "
|
||||||
"console interaction could be disabled as a "
|
"console interaction could be disabled as a "
|
||||||
"result")
|
"result",
|
||||||
parser.add_argument("--no-cogs",
|
)
|
||||||
action="store_true",
|
parser.add_argument(
|
||||||
help="Starts Red with no cogs loaded, only core")
|
"--no-cogs", action="store_true", help="Starts Red with no cogs loaded, only core"
|
||||||
parser.add_argument("--load-cogs", type=str, nargs="*",
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--load-cogs",
|
||||||
|
type=str,
|
||||||
|
nargs="*",
|
||||||
help="Force loading specified cogs from the installed packages. "
|
help="Force loading specified cogs from the installed packages. "
|
||||||
"Can be used with the --no-cogs flag to load these cogs exclusively.")
|
"Can be used with the --no-cogs flag to load these cogs exclusively.",
|
||||||
parser.add_argument("--self-bot",
|
)
|
||||||
action='store_true',
|
parser.add_argument(
|
||||||
help="Specifies if Red should log in as selfbot")
|
"--self-bot", action="store_true", help="Specifies if Red should log in as selfbot"
|
||||||
parser.add_argument("--not-bot",
|
)
|
||||||
action='store_true',
|
parser.add_argument(
|
||||||
help="Specifies if the token used belongs to a bot "
|
"--not-bot",
|
||||||
"account.")
|
action="store_true",
|
||||||
parser.add_argument("--dry-run",
|
help="Specifies if the token used belongs to a bot " "account.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Makes Red quit with code 0 just before the "
|
help="Makes Red quit with code 0 just before the "
|
||||||
"login. This is useful for testing the boot "
|
"login. This is useful for testing the boot "
|
||||||
"process.")
|
"process.",
|
||||||
parser.add_argument("--debug",
|
)
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Sets the loggers level as debug")
|
||||||
|
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
|
||||||
|
parser.add_argument(
|
||||||
|
"--mentionable",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Sets the loggers level as debug")
|
help="Allows mentioning the bot as an alternative " "to using the bot prefix",
|
||||||
parser.add_argument("--dev",
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rpc",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enables developer mode")
|
help="Enables the built-in RPC server. Please read the docs" "prior to enabling this!",
|
||||||
parser.add_argument("--mentionable",
|
)
|
||||||
action="store_true",
|
parser.add_argument(
|
||||||
help="Allows mentioning the bot as an alternative "
|
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
||||||
"to using the bot prefix")
|
)
|
||||||
parser.add_argument("--rpc",
|
|
||||||
action="store_true",
|
|
||||||
help="Enables the built-in RPC server. Please read the docs"
|
|
||||||
"prior to enabling this!")
|
|
||||||
parser.add_argument("instance_name", nargs="?",
|
|
||||||
help="Name of the bot instance created during `redbot-setup`.")
|
|
||||||
|
|
||||||
args = parser.parse_args(args)
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
@@ -129,4 +150,3 @@ def parse_cli_flags(args):
|
|||||||
args.prefix = []
|
args.prefix = []
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,12 @@ class CogManager:
|
|||||||
install new cogs to, the default being the :code:`cogs/` folder in the root
|
install new cogs to, the default being the :code:`cogs/` folder in the root
|
||||||
bot directory.
|
bot directory.
|
||||||
"""
|
"""
|
||||||
def __init__(self, paths: Tuple[str]=()):
|
|
||||||
|
def __init__(self, paths: Tuple[str] = ()):
|
||||||
self.conf = Config.get_conf(self, 2938473984732, True)
|
self.conf = Config.get_conf(self, 2938473984732, True)
|
||||||
tmp_cog_install_path = cog_data_path(self) / "cogs"
|
tmp_cog_install_path = cog_data_path(self) / "cogs"
|
||||||
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
|
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
|
||||||
self.conf.register_global(
|
self.conf.register_global(paths=(), install_path=str(tmp_cog_install_path))
|
||||||
paths=(),
|
|
||||||
install_path=str(tmp_cog_install_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._paths = [Path(p) for p in paths]
|
self._paths = [Path(p) for p in paths]
|
||||||
|
|
||||||
@@ -158,7 +156,7 @@ class CogManager:
|
|||||||
if path == await self.install_path():
|
if path == await self.install_path():
|
||||||
raise ValueError("Cannot add the install path as an additional path.")
|
raise ValueError("Cannot add the install path as an additional path.")
|
||||||
|
|
||||||
all_paths = _deduplicate(await self.paths() + (path, ))
|
all_paths = _deduplicate(await self.paths() + (path,))
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self.set_paths(all_paths)
|
await self.set_paths(all_paths)
|
||||||
|
|
||||||
@@ -225,8 +223,10 @@ class CogManager:
|
|||||||
if spec:
|
if spec:
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
raise RuntimeError("No 3rd party module by the name of '{}' was found"
|
raise RuntimeError(
|
||||||
" in any available path.".format(name))
|
"No 3rd party module by the name of '{}' was found"
|
||||||
|
" in any available path.".format(name)
|
||||||
|
)
|
||||||
|
|
||||||
async def _find_core_cog(self, name: str) -> ModuleSpec:
|
async def _find_core_cog(self, name: str) -> ModuleSpec:
|
||||||
"""
|
"""
|
||||||
@@ -247,10 +247,11 @@ class CogManager:
|
|||||||
"""
|
"""
|
||||||
real_name = ".{}".format(name)
|
real_name = ".{}".format(name)
|
||||||
try:
|
try:
|
||||||
mod = import_module(real_name, package='redbot.cogs')
|
mod = import_module(real_name, package="redbot.cogs")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise RuntimeError("No core cog by the name of '{}' could"
|
raise RuntimeError(
|
||||||
"be found.".format(name)) from e
|
"No core cog by the name of '{}' could" "be found.".format(name)
|
||||||
|
) from e
|
||||||
return mod.__spec__
|
return mod.__spec__
|
||||||
|
|
||||||
# noinspection PyUnreachableCode
|
# noinspection PyUnreachableCode
|
||||||
@@ -284,7 +285,7 @@ class CogManager:
|
|||||||
async def available_modules(self) -> List[str]:
|
async def available_modules(self) -> List[str]:
|
||||||
"""Finds the names of all available modules to load.
|
"""Finds the names of all available modules to load.
|
||||||
"""
|
"""
|
||||||
paths = (await self.install_path(), ) + await self.paths()
|
paths = (await self.install_path(),) + await self.paths()
|
||||||
paths = [str(p) for p in paths]
|
paths = [str(p) for p in paths]
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
@@ -341,8 +342,9 @@ class CogManagerUI:
|
|||||||
Add a path to the list of available cog paths.
|
Add a path to the list of available cog paths.
|
||||||
"""
|
"""
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
await ctx.send(_("That path does not exist or does not"
|
await ctx.send(
|
||||||
" point to a valid directory."))
|
_("That path does not exist or does not" " point to a valid directory.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -398,7 +400,7 @@ class CogManagerUI:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def installpath(self, ctx: commands.Context, path: Path=None):
|
async def installpath(self, ctx: commands.Context, path: Path = None):
|
||||||
"""
|
"""
|
||||||
Returns the current install path or sets it if one is provided.
|
Returns the current install path or sets it if one is provided.
|
||||||
The provided path must be absolute or relative to the bot's
|
The provided path must be absolute or relative to the bot's
|
||||||
@@ -416,8 +418,9 @@ class CogManagerUI:
|
|||||||
return
|
return
|
||||||
|
|
||||||
install_path = await ctx.bot.cog_mgr.install_path()
|
install_path = await ctx.bot.cog_mgr.install_path()
|
||||||
await ctx.send(_("The bot will install new cogs to the `{}`"
|
await ctx.send(
|
||||||
" directory.").format(install_path))
|
_("The bot will install new cogs to the `{}`" " directory.").format(install_path)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -435,22 +438,20 @@ class CogManagerUI:
|
|||||||
unloaded = sorted(list(unloaded), key=str.lower)
|
unloaded = sorted(list(unloaded), key=str.lower)
|
||||||
|
|
||||||
if await ctx.embed_requested():
|
if await ctx.embed_requested():
|
||||||
loaded = ('**{} loaded:**\n').format(len(loaded)) + ", ".join(loaded)
|
loaded = ("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded)
|
||||||
unloaded = ('**{} unloaded:**\n').format(len(unloaded)) + ", ".join(unloaded)
|
unloaded = ("**{} unloaded:**\n").format(len(unloaded)) + ", ".join(unloaded)
|
||||||
|
|
||||||
for page in pagify(loaded, delims=[', ', '\n'], page_length=1800):
|
for page in pagify(loaded, delims=[", ", "\n"], page_length=1800):
|
||||||
e = discord.Embed(description=page,
|
e = discord.Embed(description=page, colour=discord.Colour.dark_green())
|
||||||
colour=discord.Colour.dark_green())
|
|
||||||
await ctx.send(embed=e)
|
await ctx.send(embed=e)
|
||||||
|
|
||||||
for page in pagify(unloaded, delims=[', ', '\n'], page_length=1800):
|
for page in pagify(unloaded, delims=[", ", "\n"], page_length=1800):
|
||||||
e = discord.Embed(description=page,
|
e = discord.Embed(description=page, colour=discord.Colour.dark_red())
|
||||||
colour=discord.Colour.dark_red())
|
|
||||||
await ctx.send(embed=e)
|
await ctx.send(embed=e)
|
||||||
else:
|
else:
|
||||||
loaded_count = '**{} loaded:**\n'.format(len(loaded))
|
loaded_count = "**{} loaded:**\n".format(len(loaded))
|
||||||
loaded = ", ".join(loaded)
|
loaded = ", ".join(loaded)
|
||||||
unloaded_count = '**{} unloaded:**\n'.format(len(unloaded))
|
unloaded_count = "**{} unloaded:**\n".format(len(unloaded))
|
||||||
unloaded = ", ".join(unloaded)
|
unloaded = ", ".join(unloaded)
|
||||||
loaded_count_sent = False
|
loaded_count_sent = False
|
||||||
unloaded_count_sent = False
|
unloaded_count_sent = False
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Command(commands.Command):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._help_override = kwargs.pop('help_override', None)
|
self._help_override = kwargs.pop("help_override", None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.translator = kwargs.pop("i18n", None)
|
self.translator = kwargs.pop("i18n", None)
|
||||||
|
|
||||||
@@ -38,7 +38,10 @@ class Command(commands.Command):
|
|||||||
translator = lambda s: s
|
translator = lambda s: s
|
||||||
else:
|
else:
|
||||||
translator = self.translator
|
translator = self.translator
|
||||||
return inspect.cleandoc(translator(self.callback.__doc__))
|
command_doc = self.callback.__doc__
|
||||||
|
if command_doc is None:
|
||||||
|
return ""
|
||||||
|
return inspect.cleandoc(translator(command_doc))
|
||||||
|
|
||||||
@help.setter
|
@help.setter
|
||||||
def help(self, value):
|
def help(self, value):
|
||||||
@@ -57,6 +60,7 @@ class Group(Command, commands.Group):
|
|||||||
|
|
||||||
# decorators
|
# decorators
|
||||||
|
|
||||||
|
|
||||||
def command(name=None, cls=Command, **attrs):
|
def command(name=None, cls=Command, **attrs):
|
||||||
"""A decorator which transforms an async function into a `Command`.
|
"""A decorator which transforms an async function into a `Command`.
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ class Context(commands.Context):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
command = self.invoked_subcommand or self.command
|
command = self.invoked_subcommand or self.command
|
||||||
embeds = await self.bot.formatter.format_help_for(self, command)
|
embed_wanted = await self.bot.embed_requested(
|
||||||
destination = self
|
self.channel, self.author, command=self.bot.get_command("help")
|
||||||
|
)
|
||||||
|
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
||||||
|
embed_wanted = False
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
destination = self
|
||||||
|
if embed_wanted:
|
||||||
|
embeds = await self.bot.formatter.format_help_for(self, command)
|
||||||
for embed in embeds:
|
for embed in embeds:
|
||||||
try:
|
try:
|
||||||
m = await destination.send(embed=embed)
|
m = await destination.send(embed=embed)
|
||||||
@@ -40,6 +47,16 @@ class Context(commands.Context):
|
|||||||
destination = self.author
|
destination = self.author
|
||||||
m = await destination.send(embed=embed)
|
m = await destination.send(embed=embed)
|
||||||
ret.append(m)
|
ret.append(m)
|
||||||
|
else:
|
||||||
|
f = commands.HelpFormatter()
|
||||||
|
msgs = await f.format_help_for(self, command)
|
||||||
|
for msg in msgs:
|
||||||
|
try:
|
||||||
|
m = await destination.send(msg)
|
||||||
|
except discord.HTTPException:
|
||||||
|
destination = self.author
|
||||||
|
m = await destination.send(msg)
|
||||||
|
ret.append(m)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -59,10 +76,9 @@ class Context(commands.Context):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def send_interactive(self,
|
async def send_interactive(
|
||||||
messages: Iterable[str],
|
self, messages: Iterable[str], box_lang: str = None, timeout: int = 15
|
||||||
box_lang: str=None,
|
) -> List[discord.Message]:
|
||||||
timeout: int=15) -> List[discord.Message]:
|
|
||||||
"""Send multiple messages interactively.
|
"""Send multiple messages interactively.
|
||||||
|
|
||||||
The user will be prompted for whether or not they would like to view
|
The user will be prompted for whether or not they would like to view
|
||||||
@@ -84,9 +100,9 @@ class Context(commands.Context):
|
|||||||
messages = tuple(messages)
|
messages = tuple(messages)
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
more_check = lambda m: (m.author == self.author and
|
more_check = lambda m: (
|
||||||
m.channel == self.channel and
|
m.author == self.author and m.channel == self.channel and m.content.lower() == "more"
|
||||||
m.content.lower() == "more")
|
)
|
||||||
|
|
||||||
for idx, page in enumerate(messages, 1):
|
for idx, page in enumerate(messages, 1):
|
||||||
if box_lang is None:
|
if box_lang is None:
|
||||||
@@ -105,10 +121,10 @@ class Context(commands.Context):
|
|||||||
query = await self.send(
|
query = await self.send(
|
||||||
"There {} still {} message{} remaining. "
|
"There {} still {} message{} remaining. "
|
||||||
"Type `more` to continue."
|
"Type `more` to continue."
|
||||||
"".format(is_are, n_remaining, plural))
|
"".format(is_are, n_remaining, plural)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
resp = await self.bot.wait_for(
|
resp = await self.bot.wait_for("message", check=more_check, timeout=timeout)
|
||||||
'message', check=more_check, timeout=timeout)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await query.delete()
|
await query.delete()
|
||||||
break
|
break
|
||||||
@@ -122,6 +138,20 @@ class Context(commands.Context):
|
|||||||
await query.delete()
|
await query.delete()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
async def embed_colour(self):
|
||||||
|
"""
|
||||||
|
Helper function to get the colour for an embed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
discord.Colour:
|
||||||
|
The colour to be used
|
||||||
|
"""
|
||||||
|
if self.guild and await self.bot.db.guild(self.guild).use_bot_color():
|
||||||
|
return self.guild.me.color
|
||||||
|
else:
|
||||||
|
return self.bot.color
|
||||||
|
|
||||||
async def embed_requested(self):
|
async def embed_requested(self):
|
||||||
"""
|
"""
|
||||||
Simple helper to call bot.embed_requested
|
Simple helper to call bot.embed_requested
|
||||||
@@ -134,9 +164,7 @@ class Context(commands.Context):
|
|||||||
"""
|
"""
|
||||||
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
||||||
return False
|
return False
|
||||||
return await self.bot.embed_requested(
|
return await self.bot.embed_requested(self.channel, self.author, command=self.command)
|
||||||
self.channel, self.author, command=self.command
|
|
||||||
)
|
|
||||||
|
|
||||||
async def maybe_send_embed(self, message: str) -> discord.Message:
|
async def maybe_send_embed(self, message: str) -> discord.Message:
|
||||||
"""
|
"""
|
||||||
@@ -163,6 +191,8 @@ class Context(commands.Context):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if await self.embed_requested():
|
if await self.embed_requested():
|
||||||
return await self.send(embed=discord.Embed(description=message))
|
return await self.send(
|
||||||
|
embed=discord.Embed(description=message, color=(await self.embed_colour()))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return await self.send(message)
|
return await self.send(message)
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ class _ValueCtxManager:
|
|||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self.raw_value = await self
|
self.raw_value = await self
|
||||||
if not isinstance(self.raw_value, (list, dict)):
|
if not isinstance(self.raw_value, (list, dict)):
|
||||||
raise TypeError("Type of retrieved value must be mutable (i.e. "
|
raise TypeError(
|
||||||
|
"Type of retrieved value must be mutable (i.e. "
|
||||||
"list or dict) in order to use a config value as "
|
"list or dict) in order to use a config value as "
|
||||||
"a context manager.")
|
"a context manager."
|
||||||
|
)
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
async def __aexit__(self, *exc_info):
|
async def __aexit__(self, *exc_info):
|
||||||
@@ -61,6 +63,7 @@ class Value:
|
|||||||
A reference to `Config.driver`.
|
A reference to `Config.driver`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, identifiers: Tuple[str], default_value, driver):
|
def __init__(self, identifiers: Tuple[str], default_value, driver):
|
||||||
self._identifiers = identifiers
|
self._identifiers = identifiers
|
||||||
self.default = default_value
|
self.default = default_value
|
||||||
@@ -168,10 +171,10 @@ class Group(Value):
|
|||||||
A reference to `Config.driver`.
|
A reference to `Config.driver`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, identifiers: Tuple[str],
|
|
||||||
defaults: dict,
|
def __init__(
|
||||||
driver,
|
self, identifiers: Tuple[str], defaults: dict, driver, force_registration: bool = False
|
||||||
force_registration: bool=False):
|
):
|
||||||
self._defaults = defaults
|
self._defaults = defaults
|
||||||
self.force_registration = force_registration
|
self.force_registration = force_registration
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
@@ -209,31 +212,22 @@ class Group(Value):
|
|||||||
"""
|
"""
|
||||||
is_group = self.is_group(item)
|
is_group = self.is_group(item)
|
||||||
is_value = not is_group and self.is_value(item)
|
is_value = not is_group and self.is_value(item)
|
||||||
new_identifiers = self.identifiers + (item, )
|
new_identifiers = self.identifiers + (item,)
|
||||||
if is_group:
|
if is_group:
|
||||||
return Group(
|
return Group(
|
||||||
identifiers=new_identifiers,
|
identifiers=new_identifiers,
|
||||||
defaults=self._defaults[item],
|
defaults=self._defaults[item],
|
||||||
driver=self.driver,
|
driver=self.driver,
|
||||||
force_registration=self.force_registration
|
force_registration=self.force_registration,
|
||||||
)
|
)
|
||||||
elif is_value:
|
elif is_value:
|
||||||
return Value(
|
return Value(
|
||||||
identifiers=new_identifiers,
|
identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
|
||||||
default_value=self._defaults[item],
|
|
||||||
driver=self.driver
|
|
||||||
)
|
)
|
||||||
elif self.force_registration:
|
elif self.force_registration:
|
||||||
raise AttributeError(
|
raise AttributeError("'{}' is not a valid registered Group " "or value.".format(item))
|
||||||
"'{}' is not a valid registered Group "
|
|
||||||
"or value.".format(item)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Value(
|
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
||||||
identifiers=new_identifiers,
|
|
||||||
default_value=None,
|
|
||||||
driver=self.driver
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_group(self, item: str) -> bool:
|
def is_group(self, item: str) -> bool:
|
||||||
"""A helper method for `__getattr__`. Most developers will have no need
|
"""A helper method for `__getattr__`. Most developers will have no need
|
||||||
@@ -385,9 +379,7 @@ class Group(Value):
|
|||||||
|
|
||||||
async def set(self, value):
|
async def set(self, value):
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise ValueError(
|
raise ValueError("You may only set the value of a group to be a dict.")
|
||||||
"You may only set the value of a group to be a dict."
|
|
||||||
)
|
|
||||||
await super().set(value)
|
await super().set(value)
|
||||||
|
|
||||||
async def set_raw(self, *nested_path: str, value):
|
async def set_raw(self, *nested_path: str, value):
|
||||||
@@ -456,10 +448,14 @@ class Config:
|
|||||||
USER = "USER"
|
USER = "USER"
|
||||||
MEMBER = "MEMBER"
|
MEMBER = "MEMBER"
|
||||||
|
|
||||||
def __init__(self, cog_name: str, unique_identifier: str,
|
def __init__(
|
||||||
|
self,
|
||||||
|
cog_name: str,
|
||||||
|
unique_identifier: str,
|
||||||
driver: "BaseDriver",
|
driver: "BaseDriver",
|
||||||
force_registration: bool=False,
|
force_registration: bool = False,
|
||||||
defaults: dict=None):
|
defaults: dict = None,
|
||||||
|
):
|
||||||
self.cog_name = cog_name
|
self.cog_name = cog_name
|
||||||
self.unique_identifier = unique_identifier
|
self.unique_identifier = unique_identifier
|
||||||
|
|
||||||
@@ -472,8 +468,7 @@ class Config:
|
|||||||
return deepcopy(self._defaults)
|
return deepcopy(self._defaults)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_conf(cls, cog_instance, identifier: int,
|
def get_conf(cls, cog_instance, identifier: int, force_registration=False, cog_name=None):
|
||||||
force_registration=False, cog_name=None):
|
|
||||||
"""Get a Config instance for your cog.
|
"""Get a Config instance for your cog.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
@@ -519,20 +514,24 @@ class Config:
|
|||||||
|
|
||||||
log.debug("Basic config: \n\n{}".format(basic_config))
|
log.debug("Basic config: \n\n{}".format(basic_config))
|
||||||
|
|
||||||
driver_name = basic_config.get('STORAGE_TYPE', 'JSON')
|
driver_name = basic_config.get("STORAGE_TYPE", "JSON")
|
||||||
driver_details = basic_config.get('STORAGE_DETAILS', {})
|
driver_details = basic_config.get("STORAGE_DETAILS", {})
|
||||||
|
|
||||||
log.debug("Using driver: '{}'".format(driver_name))
|
log.debug("Using driver: '{}'".format(driver_name))
|
||||||
|
|
||||||
driver = get_driver(driver_name, cog_name, uuid, data_path_override=cog_path_override,
|
driver = get_driver(
|
||||||
**driver_details)
|
driver_name, cog_name, uuid, data_path_override=cog_path_override, **driver_details
|
||||||
conf = cls(cog_name=cog_name, unique_identifier=uuid,
|
)
|
||||||
|
conf = cls(
|
||||||
|
cog_name=cog_name,
|
||||||
|
unique_identifier=uuid,
|
||||||
force_registration=force_registration,
|
force_registration=force_registration,
|
||||||
driver=driver)
|
driver=driver,
|
||||||
|
)
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_core_conf(cls, force_registration: bool=False):
|
def get_core_conf(cls, force_registration: bool = False):
|
||||||
"""Get a Config instance for a core module.
|
"""Get a Config instance for a core module.
|
||||||
|
|
||||||
All core modules that require a config instance should use this
|
All core modules that require a config instance should use this
|
||||||
@@ -549,14 +548,18 @@ class Config:
|
|||||||
# We have to import this here otherwise we have a circular dependency
|
# We have to import this here otherwise we have a circular dependency
|
||||||
from .data_manager import basic_config
|
from .data_manager import basic_config
|
||||||
|
|
||||||
driver_name = basic_config.get('STORAGE_TYPE', 'JSON')
|
driver_name = basic_config.get("STORAGE_TYPE", "JSON")
|
||||||
driver_details = basic_config.get('STORAGE_DETAILS', {})
|
driver_details = basic_config.get("STORAGE_DETAILS", {})
|
||||||
|
|
||||||
driver = get_driver(driver_name, "Core", '0', data_path_override=core_path,
|
driver = get_driver(
|
||||||
**driver_details)
|
driver_name, "Core", "0", data_path_override=core_path, **driver_details
|
||||||
conf = cls(cog_name="Core", driver=driver,
|
)
|
||||||
unique_identifier='0',
|
conf = cls(
|
||||||
force_registration=force_registration)
|
cog_name="Core",
|
||||||
|
driver=driver,
|
||||||
|
unique_identifier="0",
|
||||||
|
force_registration=force_registration,
|
||||||
|
)
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def __getattr__(self, item: str) -> Union[Group, Value]:
|
def __getattr__(self, item: str) -> Union[Group, Value]:
|
||||||
@@ -593,7 +596,7 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
ret = {}
|
ret = {}
|
||||||
partial = ret
|
partial = ret
|
||||||
splitted = key.split('__')
|
splitted = key.split("__")
|
||||||
for i, k in enumerate(splitted, start=1):
|
for i, k in enumerate(splitted, start=1):
|
||||||
if not k.isidentifier():
|
if not k.isidentifier():
|
||||||
raise RuntimeError("'{}' is an invalid config key.".format(k))
|
raise RuntimeError("'{}' is an invalid config key.".format(k))
|
||||||
@@ -621,8 +624,9 @@ class Config:
|
|||||||
existing_is_dict = isinstance(_partial[k], dict)
|
existing_is_dict = isinstance(_partial[k], dict)
|
||||||
if val_is_dict != existing_is_dict:
|
if val_is_dict != existing_is_dict:
|
||||||
# != is XOR
|
# != is XOR
|
||||||
raise KeyError("You cannot register a Group and a Value under"
|
raise KeyError(
|
||||||
" the same name.")
|
"You cannot register a Group and a Value under" " the same name."
|
||||||
|
)
|
||||||
if val_is_dict:
|
if val_is_dict:
|
||||||
Config._update_defaults(v, _partial=_partial[k])
|
Config._update_defaults(v, _partial=_partial[k])
|
||||||
else:
|
else:
|
||||||
@@ -736,7 +740,7 @@ class Config:
|
|||||||
identifiers=(key, *identifiers),
|
identifiers=(key, *identifiers),
|
||||||
defaults=self.defaults.get(key, {}),
|
defaults=self.defaults.get(key, {}),
|
||||||
driver=self.driver,
|
driver=self.driver,
|
||||||
force_registration=self.force_registration
|
force_registration=self.force_registration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def guild(self, guild: discord.Guild) -> Group:
|
def guild(self, guild: discord.Guild) -> Group:
|
||||||
@@ -749,7 +753,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The guild's Group object.
|
The guild's Group object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -767,7 +771,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The channel's Group object.
|
The channel's Group object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -783,7 +787,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The role's Group object.
|
The role's Group object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -799,7 +803,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The user's Group object.
|
The user's Group object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -815,7 +819,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The member's Group object.
|
The member's Group object.
|
||||||
"""
|
"""
|
||||||
return self._get_base_group(self.MEMBER, member.guild.id, member.id)
|
return self._get_base_group(self.MEMBER, member.guild.id, member.id)
|
||||||
@@ -834,7 +838,7 @@ class Config:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Group
|
`Group <redbot.core.config.Group>`
|
||||||
The custom group's Group object.
|
The custom group's Group object.
|
||||||
"""
|
"""
|
||||||
return self._get_base_group(group_identifier, *identifiers)
|
return self._get_base_group(group_identifier, *identifiers)
|
||||||
@@ -935,7 +939,7 @@ class Config:
|
|||||||
ret[int(member_id)] = new_member_data
|
ret[int(member_id)] = new_member_data
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def all_members(self, guild: discord.Guild=None) -> dict:
|
async def all_members(self, guild: discord.Guild = None) -> dict:
|
||||||
"""Get data for all members.
|
"""Get data for all members.
|
||||||
|
|
||||||
If :code:`guild` is specified, only the data for the members of that
|
If :code:`guild` is specified, only the data for the members of that
|
||||||
@@ -965,8 +969,7 @@ class Config:
|
|||||||
group = self._get_base_group(self.MEMBER)
|
group = self._get_base_group(self.MEMBER)
|
||||||
dict_ = await group()
|
dict_ = await group()
|
||||||
for guild_id, guild_data in dict_.items():
|
for guild_id, guild_data in dict_.items():
|
||||||
ret[int(guild_id)] = self._all_members_from_guild(
|
ret[int(guild_id)] = self._all_members_from_guild(group, guild_data)
|
||||||
group, guild_data)
|
|
||||||
else:
|
else:
|
||||||
group = self._get_base_group(self.MEMBER, guild.id)
|
group = self._get_base_group(self.MEMBER, guild.id)
|
||||||
guild_data = await group()
|
guild_data = await group()
|
||||||
@@ -992,9 +995,7 @@ class Config:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not scopes:
|
if not scopes:
|
||||||
group = Group(identifiers=[],
|
group = Group(identifiers=[], defaults={}, driver=self.driver)
|
||||||
defaults={},
|
|
||||||
driver=self.driver)
|
|
||||||
else:
|
else:
|
||||||
group = self._get_base_group(*scopes)
|
group = self._get_base_group(*scopes)
|
||||||
await group.clear()
|
await group.clear()
|
||||||
@@ -1046,7 +1047,7 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
await self._clear_scope(self.USER)
|
await self._clear_scope(self.USER)
|
||||||
|
|
||||||
async def clear_all_members(self, guild: discord.Guild=None):
|
async def clear_all_members(self, guild: discord.Guild = None):
|
||||||
"""Clear all member data.
|
"""Clear all member data.
|
||||||
|
|
||||||
This resets all specified member data to its registered defaults.
|
This resets all specified member data to its registered defaults.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import datetime
|
import datetime
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -32,10 +33,12 @@ __all__ = ["Core"]
|
|||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
|
|
||||||
OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
|
OWNER_DISCLAIMER = (
|
||||||
|
"⚠ **Only** the person who is hosting Red should be "
|
||||||
"owner. **This has SERIOUS security implications. The "
|
"owner. **This has SERIOUS security implications. The "
|
||||||
"owner can access any data that is present on the host "
|
"owner can access any data that is present on the host "
|
||||||
"system.** ⚠")
|
"system.** ⚠"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_ = i18n.Translator("Core", __file__)
|
_ = i18n.Translator("Core", __file__)
|
||||||
@@ -44,20 +47,14 @@ _ = i18n.Translator("Core", __file__)
|
|||||||
@i18n.cog_i18n(_)
|
@i18n.cog_i18n(_)
|
||||||
class Core:
|
class Core:
|
||||||
"""Commands related to core functions"""
|
"""Commands related to core functions"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot # type: Red
|
self.bot = bot # type: Red
|
||||||
|
|
||||||
rpc.add_method('core', self.rpc_load)
|
|
||||||
rpc.add_method('core', self.rpc_unload)
|
|
||||||
rpc.add_method('core', self.rpc_reload)
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
@commands.command(hidden=True)
|
||||||
async def ping(self, ctx):
|
async def ping(self, ctx):
|
||||||
"""Pong."""
|
"""Pong."""
|
||||||
if ctx.guild is None or ctx.channel.permissions_for(ctx.guild.me).add_reactions:
|
await ctx.send("Pong.")
|
||||||
await ctx.message.add_reaction("\U0001f3d3") # ping pong paddle
|
|
||||||
else:
|
|
||||||
await ctx.maybe_send_embed("Pong.")
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def info(self, ctx: commands.Context):
|
async def info(self, ctx: commands.Context):
|
||||||
@@ -72,15 +69,13 @@ class Core:
|
|||||||
since = datetime.datetime(2016, 1, 2, 0, 0)
|
since = datetime.datetime(2016, 1, 2, 0, 0)
|
||||||
days_since = (datetime.datetime.utcnow() - since).days
|
days_since = (datetime.datetime.utcnow() - since).days
|
||||||
dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
|
dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
|
||||||
python_version = "[{}.{}.{}]({})".format(
|
python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url)
|
||||||
*sys.version_info[:3], python_url
|
|
||||||
)
|
|
||||||
red_version = "[{}]({})".format(__version__, red_pypi)
|
red_version = "[{}]({})".format(__version__, red_pypi)
|
||||||
app_info = await self.bot.application_info()
|
app_info = await self.bot.application_info()
|
||||||
owner = app_info.owner
|
owner = app_info.owner
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get('{}/json'.format(red_pypi)) as r:
|
async with session.get("{}/json".format(red_pypi)) as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
||||||
about = (
|
about = (
|
||||||
@@ -89,7 +84,8 @@ class Core:
|
|||||||
"Red is backed by a passionate community who contributes and "
|
"Red is backed by a passionate community who contributes and "
|
||||||
"creates content for everyone to enjoy. [Join us today]({}) "
|
"creates content for everyone to enjoy. [Join us today]({}) "
|
||||||
"and help us improve!\n\n"
|
"and help us improve!\n\n"
|
||||||
"".format(red_repo, author_repo, org_repo, support_server_url))
|
"".format(red_repo, author_repo, org_repo, support_server_url)
|
||||||
|
)
|
||||||
|
|
||||||
embed = discord.Embed(color=discord.Color.red())
|
embed = discord.Embed(color=discord.Color.red())
|
||||||
embed.add_field(name="Instance owned by", value=str(owner))
|
embed.add_field(name="Instance owned by", value=str(owner))
|
||||||
@@ -97,14 +93,14 @@ class Core:
|
|||||||
embed.add_field(name="discord.py", value=dpy_version)
|
embed.add_field(name="discord.py", value=dpy_version)
|
||||||
embed.add_field(name="Red version", value=red_version)
|
embed.add_field(name="Red version", value=red_version)
|
||||||
if outdated:
|
if outdated:
|
||||||
embed.add_field(name="Outdated", value="Yes, {} is available".format(
|
embed.add_field(
|
||||||
data["info"]["version"]
|
name="Outdated", value="Yes, {} is available".format(data["info"]["version"])
|
||||||
)
|
|
||||||
)
|
)
|
||||||
embed.add_field(name="About Red", value=about, inline=False)
|
embed.add_field(name="About Red", value=about, inline=False)
|
||||||
|
|
||||||
embed.set_footer(text="Bringing joy since 02 Jan 2016 (over "
|
embed.set_footer(
|
||||||
"{} days ago!)".format(days_since))
|
text="Bringing joy since 02 Jan 2016 (over " "{} days ago!)".format(days_since)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
@@ -115,11 +111,7 @@ class Core:
|
|||||||
"""Shows Red's uptime"""
|
"""Shows Red's uptime"""
|
||||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
passed = self.get_bot_uptime()
|
passed = self.get_bot_uptime()
|
||||||
await ctx.send(
|
await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since))
|
||||||
"Been up for: **{}** (since {} UTC)".format(
|
|
||||||
passed, since
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_bot_uptime(self, *, brief=False):
|
def get_bot_uptime(self, *, brief=False):
|
||||||
# Courtesy of Danny
|
# Courtesy of Danny
|
||||||
@@ -131,13 +123,13 @@ class Core:
|
|||||||
|
|
||||||
if not brief:
|
if not brief:
|
||||||
if days:
|
if days:
|
||||||
fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds'
|
fmt = "{d} days, {h} hours, {m} minutes, and {s} seconds"
|
||||||
else:
|
else:
|
||||||
fmt = '{h} hours, {m} minutes, and {s} seconds'
|
fmt = "{h} hours, {m} minutes, and {s} seconds"
|
||||||
else:
|
else:
|
||||||
fmt = '{h}h {m}m {s}s'
|
fmt = "{h}h {m}m {s}s"
|
||||||
if days:
|
if days:
|
||||||
fmt = '{d}d ' + fmt
|
fmt = "{d}d " + fmt
|
||||||
|
|
||||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
||||||
|
|
||||||
@@ -176,14 +168,12 @@ class Core:
|
|||||||
current = await self.bot.db.embeds()
|
current = await self.bot.db.embeds()
|
||||||
await self.bot.db.embeds.set(not current)
|
await self.bot.db.embeds.set(not current)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} by default.").format(
|
_("Embeds are now {} by default.").format("disabled" if current else "enabled")
|
||||||
"disabled" if current else "enabled"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@embedset.command(name="guild")
|
@embedset.command(name="guild")
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool=None):
|
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""
|
"""
|
||||||
Toggle the guild's embed setting.
|
Toggle the guild's embed setting.
|
||||||
|
|
||||||
@@ -197,18 +187,14 @@ class Core:
|
|||||||
"""
|
"""
|
||||||
await self.bot.db.guild(ctx.guild).embeds.set(enabled)
|
await self.bot.db.guild(ctx.guild).embeds.set(enabled)
|
||||||
if enabled is None:
|
if enabled is None:
|
||||||
await ctx.send(
|
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||||
_("Embeds will now fall back to the global setting.")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} for this guild.").format(
|
_("Embeds are now {} for this guild.").format("enabled" if enabled else "disabled")
|
||||||
"enabled" if enabled else "disabled"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@embedset.command(name="user")
|
@embedset.command(name="user")
|
||||||
async def embedset_user(self, ctx: commands.Context, enabled: bool=None):
|
async def embedset_user(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""
|
"""
|
||||||
Toggle the user's embed setting.
|
Toggle the user's embed setting.
|
||||||
|
|
||||||
@@ -222,19 +208,15 @@ class Core:
|
|||||||
"""
|
"""
|
||||||
await self.bot.db.user(ctx.author).embeds.set(enabled)
|
await self.bot.db.user(ctx.author).embeds.set(enabled)
|
||||||
if enabled is None:
|
if enabled is None:
|
||||||
await ctx.send(
|
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||||
_("Embeds will now fall back to the global setting.")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Embeds are now {} for you.").format(
|
_("Embeds are now {} for you.").format("enabled" if enabled else "disabled")
|
||||||
"enabled" if enabled else "disabled"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def traceback(self, ctx, public: bool=False):
|
async def traceback(self, ctx, public: bool = False):
|
||||||
"""Sends to the owner the last command exception that has occurred
|
"""Sends to the owner the last command exception that has occurred
|
||||||
|
|
||||||
If public (yes is specified), it will be sent to the chat instead"""
|
If public (yes is specified), it will be sent to the chat instead"""
|
||||||
@@ -267,8 +249,7 @@ class Core:
|
|||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await ctx.send("Are you sure you want me to leave this server?"
|
await ctx.send("Are you sure you want me to leave this server?" " Type yes to confirm.")
|
||||||
" Type yes to confirm.")
|
|
||||||
|
|
||||||
def conf_check(m):
|
def conf_check(m):
|
||||||
return m.author == author
|
return m.author == author
|
||||||
@@ -285,15 +266,14 @@ class Core:
|
|||||||
async def servers(self, ctx):
|
async def servers(self, ctx):
|
||||||
"""Lists and allows to leave servers"""
|
"""Lists and allows to leave servers"""
|
||||||
owner = ctx.author
|
owner = ctx.author
|
||||||
guilds = sorted(list(self.bot.guilds),
|
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
|
||||||
key=lambda s: s.name.lower())
|
|
||||||
msg = ""
|
msg = ""
|
||||||
for i, server in enumerate(guilds, 1):
|
for i, server in enumerate(guilds, 1):
|
||||||
msg += "{}: {}\n".format(i, server.name)
|
msg += "{}: {}\n".format(i, server.name)
|
||||||
|
|
||||||
msg += "\nTo leave a server, just type its number."
|
msg += "\nTo leave a server, just type its number."
|
||||||
|
|
||||||
for page in pagify(msg, ['\n']):
|
for page in pagify(msg, ["\n"]):
|
||||||
await ctx.send(page)
|
await ctx.send(page)
|
||||||
|
|
||||||
def msg_check(m):
|
def msg_check(m):
|
||||||
@@ -343,7 +323,7 @@ class Core:
|
|||||||
loaded_packages = []
|
loaded_packages = []
|
||||||
notfound_packages = []
|
notfound_packages = []
|
||||||
|
|
||||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
cognames = [c.strip() for c in cog_name.split(" ")]
|
||||||
cogspecs = []
|
cogspecs = []
|
||||||
|
|
||||||
for c in cognames:
|
for c in cognames:
|
||||||
@@ -352,7 +332,7 @@ class Core:
|
|||||||
cogspecs.append((spec, c))
|
cogspecs.append((spec, c))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
notfound_packages.append(inline(c))
|
notfound_packages.append(inline(c))
|
||||||
#await ctx.send(_("No module named '{}' was found in any"
|
# await ctx.send(_("No module named '{}' was found in any"
|
||||||
# " cog path.").format(c))
|
# " cog path.").format(c))
|
||||||
|
|
||||||
if len(cogspecs) > 0:
|
if len(cogspecs) > 0:
|
||||||
@@ -362,10 +342,12 @@ class Core:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Package loading failed", exc_info=e)
|
log.exception("Package loading failed", exc_info=e)
|
||||||
|
|
||||||
exception_log = ("Exception in command '{}'\n"
|
exception_log = "Exception in command '{}'\n" "".format(
|
||||||
"".format(ctx.command.qualified_name))
|
ctx.command.qualified_name
|
||||||
exception_log += "".join(traceback.format_exception(type(e),
|
)
|
||||||
e, e.__traceback__))
|
exception_log += "".join(
|
||||||
|
traceback.format_exception(type(e), e, e.__traceback__)
|
||||||
|
)
|
||||||
self.bot._last_exception = exception_log
|
self.bot._last_exception = exception_log
|
||||||
failed_packages.append(inline(name))
|
failed_packages.append(inline(name))
|
||||||
else:
|
else:
|
||||||
@@ -378,21 +360,23 @@ class Core:
|
|||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if failed_packages:
|
if failed_packages:
|
||||||
fmt = ("Failed to load package{plural} {packs}. Check your console or "
|
fmt = (
|
||||||
"logs for details.")
|
"Failed to load package{plural} {packs}. Check your console or "
|
||||||
|
"logs for details."
|
||||||
|
)
|
||||||
formed = self.get_package_strings(failed_packages, fmt)
|
formed = self.get_package_strings(failed_packages, fmt)
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if notfound_packages:
|
if notfound_packages:
|
||||||
fmt = 'The package{plural} {packs} {other} not found in any cog path.'
|
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||||
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were'))
|
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def unload(self, ctx, *, cog_name: str):
|
async def unload(self, ctx, *, cog_name: str):
|
||||||
"""Unloads packages"""
|
"""Unloads packages"""
|
||||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
cognames = [c.strip() for c in cog_name.split(" ")]
|
||||||
failed_packages = []
|
failed_packages = []
|
||||||
unloaded_packages = []
|
unloaded_packages = []
|
||||||
|
|
||||||
@@ -406,12 +390,12 @@ class Core:
|
|||||||
|
|
||||||
if unloaded_packages:
|
if unloaded_packages:
|
||||||
fmt = "Package{plural} {packs} {other} unloaded."
|
fmt = "Package{plural} {packs} {other} unloaded."
|
||||||
formed = self.get_package_strings(unloaded_packages, fmt, ('was', 'were'))
|
formed = self.get_package_strings(unloaded_packages, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if failed_packages:
|
if failed_packages:
|
||||||
fmt = "The package{plural} {packs} {other} not loaded."
|
fmt = "The package{plural} {packs} {other} not loaded."
|
||||||
formed = self.get_package_strings(failed_packages, fmt, ('is', 'are'))
|
formed = self.get_package_strings(failed_packages, fmt, ("is", "are"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
@commands.command(name="reload")
|
@commands.command(name="reload")
|
||||||
@@ -419,7 +403,7 @@ class Core:
|
|||||||
async def _reload(self, ctx, *, cog_name: str):
|
async def _reload(self, ctx, *, cog_name: str):
|
||||||
"""Reloads packages"""
|
"""Reloads packages"""
|
||||||
|
|
||||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
cognames = [c.strip() for c in cog_name.split(" ")]
|
||||||
|
|
||||||
for c in cognames:
|
for c in cognames:
|
||||||
ctx.bot.unload_extension(c)
|
ctx.bot.unload_extension(c)
|
||||||
@@ -444,50 +428,44 @@ class Core:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Package reloading failed", exc_info=e)
|
log.exception("Package reloading failed", exc_info=e)
|
||||||
|
|
||||||
exception_log = ("Exception in command '{}'\n"
|
exception_log = "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
|
||||||
"".format(ctx.command.qualified_name))
|
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||||
exception_log += "".join(traceback.format_exception(type(e),
|
|
||||||
e, e.__traceback__))
|
|
||||||
self.bot._last_exception = exception_log
|
self.bot._last_exception = exception_log
|
||||||
|
|
||||||
failed_packages.append(inline(name))
|
failed_packages.append(inline(name))
|
||||||
|
|
||||||
if loaded_packages:
|
if loaded_packages:
|
||||||
fmt = "Package{plural} {packs} {other} reloaded."
|
fmt = "Package{plural} {packs} {other} reloaded."
|
||||||
formed = self.get_package_strings(loaded_packages, fmt, ('was', 'were'))
|
formed = self.get_package_strings(loaded_packages, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if failed_packages:
|
if failed_packages:
|
||||||
fmt = ("Failed to reload package{plural} {packs}. Check your "
|
fmt = "Failed to reload package{plural} {packs}. Check your " "logs for details"
|
||||||
"logs for details")
|
|
||||||
formed = self.get_package_strings(failed_packages, fmt)
|
formed = self.get_package_strings(failed_packages, fmt)
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if notfound_packages:
|
if notfound_packages:
|
||||||
fmt = 'The package{plural} {packs} {other} not found in any cog path.'
|
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||||
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were'))
|
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
def get_package_strings(self, packages: list, fmt: str, other: tuple=None):
|
def get_package_strings(self, packages: list, fmt: str, other: tuple = None):
|
||||||
"""
|
"""
|
||||||
Gets the strings needed for the load, unload and reload commands
|
Gets the strings needed for the load, unload and reload commands
|
||||||
"""
|
"""
|
||||||
if other is None:
|
if other is None:
|
||||||
other = ('', '')
|
other = ("", "")
|
||||||
plural = 's' if len(packages) > 1 else ''
|
plural = "s" if len(packages) > 1 else ""
|
||||||
use_and, other = ('', other[0]) if len(packages) == 1 else (' and ', other[1])
|
use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
|
||||||
packages_string = ', '.join(packages[:-1]) + use_and + packages[-1]
|
packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
|
||||||
|
|
||||||
form = {'plural': plural,
|
form = {"plural": plural, "packs": packages_string, "other": other}
|
||||||
'packs' : packages_string,
|
|
||||||
'other' : other
|
|
||||||
}
|
|
||||||
final_string = fmt.format(**form)
|
final_string = fmt.format(**form)
|
||||||
return final_string
|
return final_string
|
||||||
|
|
||||||
@commands.command(name="shutdown")
|
@commands.command(name="shutdown")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _shutdown(self, ctx, silently: bool=False):
|
async def _shutdown(self, ctx, silently: bool = False):
|
||||||
"""Shuts down the bot"""
|
"""Shuts down the bot"""
|
||||||
wave = "\N{WAVING HAND SIGN}"
|
wave = "\N{WAVING HAND SIGN}"
|
||||||
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
||||||
@@ -500,7 +478,7 @@ class Core:
|
|||||||
|
|
||||||
@commands.command(name="restart")
|
@commands.command(name="restart")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _restart(self, ctx, silently: bool=False):
|
async def _restart(self, ctx, silently: bool = False):
|
||||||
"""Attempts to restart Red
|
"""Attempts to restart Red
|
||||||
|
|
||||||
Makes Red quit with exit code 26
|
Makes Red quit with exit code 26
|
||||||
@@ -515,7 +493,7 @@ class Core:
|
|||||||
|
|
||||||
def cleanup_and_refresh_modules(self, module_name: str):
|
def cleanup_and_refresh_modules(self, module_name: str):
|
||||||
"""Interally reloads modules so that changes are detected"""
|
"""Interally reloads modules so that changes are detected"""
|
||||||
splitted = module_name.split('.')
|
splitted = module_name.split(".")
|
||||||
|
|
||||||
def maybe_reload(new_name):
|
def maybe_reload(new_name):
|
||||||
try:
|
try:
|
||||||
@@ -553,9 +531,11 @@ class Core:
|
|||||||
"Mod role: {}\n"
|
"Mod role: {}\n"
|
||||||
"Locale: {}"
|
"Locale: {}"
|
||||||
"".format(
|
"".format(
|
||||||
ctx.bot.user.name, " ".join(prefixes),
|
ctx.bot.user.name,
|
||||||
|
" ".join(prefixes),
|
||||||
admin_role.name if admin_role else "Not set",
|
admin_role.name if admin_role else "Not set",
|
||||||
mod_role.name if mod_role else "Not set", locale
|
mod_role.name if mod_role else "Not set",
|
||||||
|
locale,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
@@ -577,6 +557,42 @@ class Core:
|
|||||||
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
|
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
|
||||||
await ctx.send(_("The mod role for this guild has been set."))
|
await ctx.send(_("The mod role for this guild has been set."))
|
||||||
|
|
||||||
|
@_set.command(aliases=["usebotcolor"])
|
||||||
|
@checks.guildowner()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def usebotcolour(self, ctx):
|
||||||
|
"""
|
||||||
|
Toggle whether to use the bot owner-configured colour for embeds.
|
||||||
|
|
||||||
|
Default is to not use the bot's configured colour, in which case the
|
||||||
|
colour used will be the colour of the bot's top role.
|
||||||
|
"""
|
||||||
|
current_setting = await ctx.bot.db.guild(ctx.guild).use_bot_color()
|
||||||
|
await ctx.bot.db.guild(ctx.guild).use_bot_color.set(not current_setting)
|
||||||
|
await ctx.send(
|
||||||
|
_("The bot {} use its configured color for embeds.").format(
|
||||||
|
_("will not") if current_setting else _("will")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@_set.command(aliases=["color"])
|
||||||
|
@checks.is_owner()
|
||||||
|
async def colour(self, ctx, *, colour: discord.Colour = None):
|
||||||
|
"""
|
||||||
|
Sets a default colour to be used for the bot's embeds.
|
||||||
|
|
||||||
|
Acceptable values cor the colour parameter can be found at:
|
||||||
|
|
||||||
|
http://discordpy.readthedocs.io/en/rewrite/ext/commands/api.html#discord.ext.commands.ColourConverter
|
||||||
|
"""
|
||||||
|
if colour is None:
|
||||||
|
ctx.bot.color = discord.Color.red()
|
||||||
|
await ctx.bot.db.color.set(discord.Color.red().value)
|
||||||
|
return await ctx.send(_("The color has been reset."))
|
||||||
|
ctx.bot.color = colour
|
||||||
|
await ctx.bot.db.color.set(colour.value)
|
||||||
|
await ctx.send(_("The color has been set."))
|
||||||
|
|
||||||
@_set.command()
|
@_set.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def avatar(self, ctx, url: str):
|
async def avatar(self, ctx, url: str):
|
||||||
@@ -588,9 +604,13 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await ctx.bot.user.edit(avatar=data)
|
await ctx.bot.user.edit(avatar=data)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(_("Failed. Remember that you can edit my avatar "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Failed. Remember that you can edit my avatar "
|
||||||
"up to two times a hour. The URL must be a "
|
"up to two times a hour. The URL must be a "
|
||||||
"direct link to a JPG / PNG."))
|
"direct link to a JPG / PNG."
|
||||||
|
)
|
||||||
|
)
|
||||||
except discord.InvalidArgument:
|
except discord.InvalidArgument:
|
||||||
await ctx.send(_("JPG / PNG format only."))
|
await ctx.send(_("JPG / PNG format only."))
|
||||||
else:
|
else:
|
||||||
@@ -599,26 +619,24 @@ class Core:
|
|||||||
@_set.command(name="game")
|
@_set.command(name="game")
|
||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _game(self, ctx, *, game: str=None):
|
async def _game(self, ctx, *, game: str = None):
|
||||||
"""Sets Red's playing status"""
|
"""Sets Red's playing status"""
|
||||||
|
|
||||||
if game:
|
if game:
|
||||||
game = discord.Game(name=game)
|
game = discord.Game(name=game)
|
||||||
else:
|
else:
|
||||||
game = None
|
game = None
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
else discord.Status.online
|
|
||||||
await ctx.bot.change_presence(status=status, activity=game)
|
await ctx.bot.change_presence(status=status, activity=game)
|
||||||
await ctx.send(_("Game set."))
|
await ctx.send(_("Game set."))
|
||||||
|
|
||||||
@_set.command(name="listening")
|
@_set.command(name="listening")
|
||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _listening(self, ctx, *, listening: str=None):
|
async def _listening(self, ctx, *, listening: str = None):
|
||||||
"""Sets Red's listening status"""
|
"""Sets Red's listening status"""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
else discord.Status.online
|
|
||||||
if listening:
|
if listening:
|
||||||
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
|
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
|
||||||
else:
|
else:
|
||||||
@@ -629,11 +647,10 @@ class Core:
|
|||||||
@_set.command(name="watching")
|
@_set.command(name="watching")
|
||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _watching(self, ctx, *, watching: str=None):
|
async def _watching(self, ctx, *, watching: str = None):
|
||||||
"""Sets Red's watching status"""
|
"""Sets Red's watching status"""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
else discord.Status.online
|
|
||||||
if watching:
|
if watching:
|
||||||
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
|
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
|
||||||
else:
|
else:
|
||||||
@@ -658,7 +675,7 @@ class Core:
|
|||||||
"online": discord.Status.online,
|
"online": discord.Status.online,
|
||||||
"idle": discord.Status.idle,
|
"idle": discord.Status.idle,
|
||||||
"dnd": discord.Status.dnd,
|
"dnd": discord.Status.dnd,
|
||||||
"invisible": discord.Status.invisible
|
"invisible": discord.Status.invisible,
|
||||||
}
|
}
|
||||||
|
|
||||||
game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None
|
game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None
|
||||||
@@ -677,8 +694,7 @@ class Core:
|
|||||||
"""Sets Red's streaming status
|
"""Sets Red's streaming status
|
||||||
Leaving both streamer and stream_title empty will clear it."""
|
Leaving both streamer and stream_title empty will clear it."""
|
||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status \
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None
|
||||||
if len(ctx.bot.guilds) > 0 else None
|
|
||||||
|
|
||||||
if stream_title:
|
if stream_title:
|
||||||
stream_title = stream_title.strip()
|
stream_title = stream_title.strip()
|
||||||
@@ -700,23 +716,26 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await ctx.bot.user.edit(username=username)
|
await ctx.bot.user.edit(username=username)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(_("Failed to change name. Remember that you can "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Failed to change name. Remember that you can "
|
||||||
"only do it up to 2 times an hour. Use "
|
"only do it up to 2 times an hour. Use "
|
||||||
"nicknames if you need frequent changes. "
|
"nicknames if you need frequent changes. "
|
||||||
"`{}set nickname`").format(ctx.prefix))
|
"`{}set nickname`"
|
||||||
|
).format(ctx.prefix)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Done."))
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@_set.command(name="nickname")
|
@_set.command(name="nickname")
|
||||||
@checks.admin()
|
@checks.admin()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _nickname(self, ctx, *, nickname: str=None):
|
async def _nickname(self, ctx, *, nickname: str = None):
|
||||||
"""Sets Red's nickname"""
|
"""Sets Red's nickname"""
|
||||||
try:
|
try:
|
||||||
await ctx.guild.me.edit(nick=nickname)
|
await ctx.guild.me.edit(nick=nickname)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I lack the permissions to change my own "
|
await ctx.send(_("I lack the permissions to change my own " "nickname."))
|
||||||
"nickname."))
|
|
||||||
else:
|
else:
|
||||||
await ctx.send("Done.")
|
await ctx.send("Done.")
|
||||||
|
|
||||||
@@ -748,6 +767,7 @@ class Core:
|
|||||||
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
||||||
async def owner(self, ctx):
|
async def owner(self, ctx):
|
||||||
"""Sets Red's main owner"""
|
"""Sets Red's main owner"""
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
return m.author == ctx.author and m.channel == ctx.channel
|
return m.author == ctx.author and m.channel == ctx.channel
|
||||||
|
|
||||||
@@ -759,20 +779,22 @@ class Core:
|
|||||||
|
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
token += random.choice(chars)
|
token += random.choice(chars)
|
||||||
log.info("{0} ({0.id}) requested to be set as owner."
|
log.info("{0} ({0.id}) requested to be set as owner." "".format(ctx.author))
|
||||||
"".format(ctx.author))
|
|
||||||
print(_("\nVerification token:"))
|
print(_("\nVerification token:"))
|
||||||
print(token)
|
print(token)
|
||||||
|
|
||||||
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
|
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
await ctx.send(_("I have printed a one-time token in the console. "
|
await ctx.send(
|
||||||
"Copy and paste it here to confirm you are the owner."))
|
_(
|
||||||
|
"I have printed a one-time token in the console. "
|
||||||
|
"Copy and paste it here to confirm you are the owner."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await ctx.bot.wait_for("message", check=check,
|
message = await ctx.bot.wait_for("message", check=check, timeout=60)
|
||||||
timeout=60)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.owner.reset_cooldown(ctx)
|
self.owner.reset_cooldown(ctx)
|
||||||
await ctx.send(_("The set owner request has timed out."))
|
await ctx.send(_("The set owner request has timed out."))
|
||||||
@@ -798,10 +820,13 @@ class Core:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Please use that command in DM. Since users probably saw your token,"
|
_(
|
||||||
|
"Please use that command in DM. Since users probably saw your token,"
|
||||||
" it is recommended to reset it right now. Go to the following link and"
|
" it is recommended to reset it right now. Go to the following link and"
|
||||||
" select `Reveal Token` and `Generate a new token?`."
|
" select `Reveal Token` and `Generate a new token?`."
|
||||||
"\n\nhttps://discordapp.com/developers/applications/me/{}").format(self.bot.user.id))
|
"\n\nhttps://discordapp.com/developers/applications/me/{}"
|
||||||
|
).format(self.bot.user.id)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await ctx.bot.db.token.set(token)
|
await ctx.bot.db.token.set(token)
|
||||||
@@ -840,6 +865,75 @@ class Core:
|
|||||||
ctx.bot.disable_sentry()
|
ctx.bot.disable_sentry()
|
||||||
await ctx.send(_("Done. Sentry logging is now disabled."))
|
await ctx.send(_("Done. Sentry logging is now disabled."))
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@checks.is_owner()
|
||||||
|
async def helpset(self, ctx: commands.Context):
|
||||||
|
"""Manage settings for the help command."""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send_help()
|
||||||
|
|
||||||
|
@helpset.command(name="pagecharlimit")
|
||||||
|
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
||||||
|
"""Set the character limit for each page in the help message.
|
||||||
|
|
||||||
|
This setting only applies to embedded help.
|
||||||
|
|
||||||
|
Please note that setting a relitavely small character limit may
|
||||||
|
mean some pages will exceed this limit. This is because categories
|
||||||
|
are never spread across multiple pages in the help message.
|
||||||
|
|
||||||
|
The default value is 1000 characters.
|
||||||
|
"""
|
||||||
|
if limit <= 0:
|
||||||
|
await ctx.send(_("You must give a positive value!"))
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.bot.db.help.page_char_limit.set(limit)
|
||||||
|
await ctx.send(_("Done. The character limit per page has been set to {}.").format(limit))
|
||||||
|
|
||||||
|
@helpset.command(name="maxpages")
|
||||||
|
async def helpset_maxpages(self, ctx: commands.Context, pages: int):
|
||||||
|
"""Set the maximum number of help pages sent in a server channel.
|
||||||
|
|
||||||
|
This setting only applies to embedded help.
|
||||||
|
|
||||||
|
If a help message contains more pages than this value, the help message will
|
||||||
|
be sent to the command author via DM. This is to help reduce spam in server
|
||||||
|
text channels.
|
||||||
|
|
||||||
|
The default value is 2 pages.
|
||||||
|
"""
|
||||||
|
if pages < 0:
|
||||||
|
await ctx.send(_("You must give a value of zero or greater!"))
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.bot.db.help.max_pages_in_guild.set(pages)
|
||||||
|
await ctx.send(_("Done. The page limit has been set to {}.").format(pages))
|
||||||
|
|
||||||
|
@helpset.command(name="tagline")
|
||||||
|
async def helpset_tagline(self, ctx: commands.Context, *, tagline: str = None):
|
||||||
|
"""
|
||||||
|
Set the tagline to be used.
|
||||||
|
|
||||||
|
This setting only applies to embedded help. If no tagline is
|
||||||
|
specified, the default will be used instead.
|
||||||
|
"""
|
||||||
|
if tagline is None:
|
||||||
|
await ctx.bot.db.help.tagline.set("")
|
||||||
|
return await ctx.send(_("The tagline has been reset."))
|
||||||
|
|
||||||
|
if len(tagline) > 2048:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Your tagline is too long! Please shorten it to be "
|
||||||
|
"no more than 2048 characters long."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.bot.db.help.tagline.set(tagline)
|
||||||
|
await ctx.send(_("The tagline has been set to {}.").format(tagline[:1900]))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def listlocales(self, ctx: commands.Context):
|
async def listlocales(self, ctx: commands.Context):
|
||||||
@@ -854,19 +948,19 @@ class Core:
|
|||||||
locale_list = sorted(set([loc.stem for loc in list(red_path.glob("**/*.po"))]))
|
locale_list = sorted(set([loc.stem for loc in list(red_path.glob("**/*.po"))]))
|
||||||
pages = pagify("\n".join(locale_list))
|
pages = pagify("\n".join(locale_list))
|
||||||
|
|
||||||
await ctx.send_interactive(
|
await ctx.send_interactive(pages, box_lang="Available Locales:")
|
||||||
pages, box_lang="Available Locales:"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def backup(self, ctx):
|
async def backup(self, ctx, backup_path: str = None):
|
||||||
"""Creates a backup of all data for the instance."""
|
"""Creates a backup of all data for the instance."""
|
||||||
from redbot.core.data_manager import basic_config, instance_name
|
from redbot.core.data_manager import basic_config, instance_name
|
||||||
from redbot.core.drivers.red_json import JSON
|
from redbot.core.drivers.red_json import JSON
|
||||||
|
|
||||||
data_dir = Path(basic_config["DATA_PATH"])
|
data_dir = Path(basic_config["DATA_PATH"])
|
||||||
if basic_config["STORAGE_TYPE"] == "MongoDB":
|
if basic_config["STORAGE_TYPE"] == "MongoDB":
|
||||||
from redbot.core.drivers.red_mongo import Mongo
|
from redbot.core.drivers.red_mongo import Mongo
|
||||||
|
|
||||||
m = Mongo("Core", **basic_config["STORAGE_DETAILS"])
|
m = Mongo("Core", **basic_config["STORAGE_DETAILS"])
|
||||||
db = m.db
|
db = m.db
|
||||||
collection_names = await db.collection_names(include_system_collections=False)
|
collection_names = await db.collection_names(include_system_collections=False)
|
||||||
@@ -886,14 +980,59 @@ class Core:
|
|||||||
instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S")
|
instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S")
|
||||||
)
|
)
|
||||||
if data_dir.exists():
|
if data_dir.exists():
|
||||||
home = data_dir.home()
|
if not backup_path:
|
||||||
backup_file = home / backup_filename
|
backup_pth = data_dir.home()
|
||||||
os.chdir(str(data_dir.parent))
|
else:
|
||||||
|
backup_pth = Path(backup_path)
|
||||||
|
backup_file = backup_pth / backup_filename
|
||||||
|
|
||||||
|
to_backup = []
|
||||||
|
exclusions = [
|
||||||
|
"__pycache__",
|
||||||
|
"Lavalink.jar",
|
||||||
|
os.path.join("Downloader", "lib"),
|
||||||
|
os.path.join("CogManager", "cogs"),
|
||||||
|
os.path.join("RepoManager", "repos"),
|
||||||
|
]
|
||||||
|
downloader_cog = ctx.bot.get_cog("Downloader")
|
||||||
|
if downloader_cog and hasattr(downloader_cog, "_repo_manager"):
|
||||||
|
repo_output = []
|
||||||
|
repo_mgr = downloader_cog._repo_manager
|
||||||
|
for n, repo in repo_mgr._repos:
|
||||||
|
repo_output.append(
|
||||||
|
{{"url": repo.url, "name": repo.name, "branch": repo.branch}}
|
||||||
|
)
|
||||||
|
repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json"
|
||||||
|
with open(str(repo_filename), "w") as f:
|
||||||
|
f.write(json.dumps(repo_output, indent=4))
|
||||||
|
instance_data = {instance_name: basic_config}
|
||||||
|
instance_file = data_dir / "instance.json"
|
||||||
|
with open(str(instance_file), "w") as instance_out:
|
||||||
|
instance_out.write(json.dumps(instance_data, indent=4))
|
||||||
|
for f in data_dir.glob("**/*"):
|
||||||
|
if not any(ex in str(f) for ex in exclusions):
|
||||||
|
to_backup.append(f)
|
||||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||||
tar.add(data_dir.stem)
|
for f in to_backup:
|
||||||
await ctx.send(_("A backup has been made of this instance. It is at {}.").format(
|
tar.add(str(f), recursive=False)
|
||||||
backup_file
|
print(str(backup_file))
|
||||||
))
|
await ctx.send(
|
||||||
|
_("A backup has been made of this instance. It is at {}.").format((backup_file))
|
||||||
|
)
|
||||||
|
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
||||||
|
|
||||||
|
def same_author_check(m):
|
||||||
|
return m.author == ctx.author and m.channel == ctx.channel
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=60)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(_("Ok then."))
|
||||||
|
else:
|
||||||
|
if msg.content.lower().strip() == "y":
|
||||||
|
await ctx.author.send(
|
||||||
|
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That directory doesn't seem to exist..."))
|
await ctx.send(_("That directory doesn't seem to exist..."))
|
||||||
|
|
||||||
@@ -902,8 +1041,7 @@ class Core:
|
|||||||
async def contact(self, ctx, *, message: str):
|
async def contact(self, ctx, *, message: str):
|
||||||
"""Sends a message to the owner"""
|
"""Sends a message to the owner"""
|
||||||
guild = ctx.message.guild
|
guild = ctx.message.guild
|
||||||
owner = discord.utils.get(ctx.bot.get_all_members(),
|
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
|
||||||
id=ctx.bot.owner_id)
|
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
footer = _("User ID: {}").format(author.id)
|
footer = _("User ID: {}").format(author.id)
|
||||||
|
|
||||||
@@ -916,12 +1054,11 @@ class Core:
|
|||||||
# We need to grab the DM command prefix (global)
|
# We need to grab the DM command prefix (global)
|
||||||
# Since it can also be set through cli flags, bot.db is not a reliable
|
# Since it can also be set through cli flags, bot.db is not a reliable
|
||||||
# source. So we'll just mock a DM message instead.
|
# source. So we'll just mock a DM message instead.
|
||||||
fake_message = namedtuple('Message', 'guild')
|
fake_message = namedtuple("Message", "guild")
|
||||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||||
prefix = prefixes[0]
|
prefix = prefixes[0]
|
||||||
|
|
||||||
content = _("Use `{}dm {} <text>` to reply to this user"
|
content = _("Use `{}dm {} <text>` to reply to this user" "").format(prefix, author.id)
|
||||||
"").format(prefix, author.id)
|
|
||||||
|
|
||||||
description = _("Sent by {} {}").format(author, source)
|
description = _("Sent by {} {}").format(author, source)
|
||||||
|
|
||||||
@@ -941,21 +1078,21 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await owner.send(content, embed=e)
|
await owner.send(content, embed=e)
|
||||||
except discord.InvalidArgument:
|
except discord.InvalidArgument:
|
||||||
await ctx.send(_("I cannot send your message, I'm unable to find "
|
await ctx.send(
|
||||||
"my owner... *sigh*"))
|
_("I cannot send your message, I'm unable to find " "my owner... *sigh*")
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Your message has been sent."))
|
await ctx.send(_("Your message has been sent."))
|
||||||
else:
|
else:
|
||||||
msg_text = (
|
msg_text = "{}\nMessage:\n\n{}\n{}".format(description, message, footer)
|
||||||
"{}\nMessage:\n\n{}\n{}".format(description, message, footer)
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
await owner.send("{}\n{}".format(content, box(msg_text)))
|
await owner.send("{}\n{}".format(content, box(msg_text)))
|
||||||
except discord.InvalidArgument:
|
except discord.InvalidArgument:
|
||||||
await ctx.send(_("I cannot send your message, I'm unable to find "
|
await ctx.send(
|
||||||
"my owner... *sigh*"))
|
_("I cannot send your message, I'm unable to find " "my owner... *sigh*")
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||||
else:
|
else:
|
||||||
@@ -970,15 +1107,18 @@ class Core:
|
|||||||
To get a user id enable 'developer mode' in Discord's
|
To get a user id enable 'developer mode' in Discord's
|
||||||
settings, 'appearance' tab. Then right click a user
|
settings, 'appearance' tab. Then right click a user
|
||||||
and copy their id"""
|
and copy their id"""
|
||||||
destination = discord.utils.get(ctx.bot.get_all_members(),
|
destination = discord.utils.get(ctx.bot.get_all_members(), id=user_id)
|
||||||
id=user_id)
|
|
||||||
if destination is None:
|
if destination is None:
|
||||||
await ctx.send(_("Invalid ID or user not found. You can only "
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Invalid ID or user not found. You can only "
|
||||||
"send messages to people I share a server "
|
"send messages to people I share a server "
|
||||||
"with."))
|
"with."
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
fake_message = namedtuple('Message', 'guild')
|
fake_message = namedtuple("Message", "guild")
|
||||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||||
prefix = prefixes[0]
|
prefix = prefixes[0]
|
||||||
description = _("Owner of {}").format(ctx.bot.user)
|
description = _("Owner of {}").format(ctx.bot.user)
|
||||||
@@ -995,8 +1135,9 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await destination.send(embed=e)
|
await destination.send(embed=e)
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("Sorry, I couldn't deliver your message "
|
await ctx.send(
|
||||||
"to {}").format(destination))
|
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
else:
|
else:
|
||||||
@@ -1004,8 +1145,9 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await destination.send("{}\n{}".format(box(response), content))
|
await destination.send("{}\n{}".format(box(response), content))
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("Sorry, I couldn't deliver your message "
|
await ctx.send(
|
||||||
"to {}").format(destination))
|
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
|
|
||||||
@@ -1018,7 +1160,7 @@ class Core:
|
|||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@whitelist.command(name='add')
|
@whitelist.command(name="add")
|
||||||
async def whitelist_add(self, ctx, user: discord.User):
|
async def whitelist_add(self, ctx, user: discord.User):
|
||||||
"""
|
"""
|
||||||
Adds a user to the whitelist.
|
Adds a user to the whitelist.
|
||||||
@@ -1029,7 +1171,7 @@ class Core:
|
|||||||
|
|
||||||
await ctx.send(_("User added to whitelist."))
|
await ctx.send(_("User added to whitelist."))
|
||||||
|
|
||||||
@whitelist.command(name='list')
|
@whitelist.command(name="list")
|
||||||
async def whitelist_list(self, ctx):
|
async def whitelist_list(self, ctx):
|
||||||
"""
|
"""
|
||||||
Lists whitelisted users.
|
Lists whitelisted users.
|
||||||
@@ -1043,7 +1185,7 @@ class Core:
|
|||||||
for page in pagify(msg):
|
for page in pagify(msg):
|
||||||
await ctx.send(box(page))
|
await ctx.send(box(page))
|
||||||
|
|
||||||
@whitelist.command(name='remove')
|
@whitelist.command(name="remove")
|
||||||
async def whitelist_remove(self, ctx, user: discord.User):
|
async def whitelist_remove(self, ctx, user: discord.User):
|
||||||
"""
|
"""
|
||||||
Removes user from whitelist.
|
Removes user from whitelist.
|
||||||
@@ -1060,7 +1202,7 @@ class Core:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("User was not in the whitelist."))
|
await ctx.send(_("User was not in the whitelist."))
|
||||||
|
|
||||||
@whitelist.command(name='clear')
|
@whitelist.command(name="clear")
|
||||||
async def whitelist_clear(self, ctx):
|
async def whitelist_clear(self, ctx):
|
||||||
"""
|
"""
|
||||||
Clears the whitelist.
|
Clears the whitelist.
|
||||||
@@ -1077,7 +1219,7 @@ class Core:
|
|||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
|
||||||
@blacklist.command(name='add')
|
@blacklist.command(name="add")
|
||||||
async def blacklist_add(self, ctx, user: discord.User):
|
async def blacklist_add(self, ctx, user: discord.User):
|
||||||
"""
|
"""
|
||||||
Adds a user to the blacklist.
|
Adds a user to the blacklist.
|
||||||
@@ -1092,7 +1234,7 @@ class Core:
|
|||||||
|
|
||||||
await ctx.send(_("User added to blacklist."))
|
await ctx.send(_("User added to blacklist."))
|
||||||
|
|
||||||
@blacklist.command(name='list')
|
@blacklist.command(name="list")
|
||||||
async def blacklist_list(self, ctx):
|
async def blacklist_list(self, ctx):
|
||||||
"""
|
"""
|
||||||
Lists blacklisted users.
|
Lists blacklisted users.
|
||||||
@@ -1106,7 +1248,7 @@ class Core:
|
|||||||
for page in pagify(msg):
|
for page in pagify(msg):
|
||||||
await ctx.send(box(page))
|
await ctx.send(box(page))
|
||||||
|
|
||||||
@blacklist.command(name='remove')
|
@blacklist.command(name="remove")
|
||||||
async def blacklist_remove(self, ctx, user: discord.User):
|
async def blacklist_remove(self, ctx, user: discord.User):
|
||||||
"""
|
"""
|
||||||
Removes user from blacklist.
|
Removes user from blacklist.
|
||||||
@@ -1123,7 +1265,7 @@ class Core:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("User was not in the blacklist."))
|
await ctx.send(_("User was not in the blacklist."))
|
||||||
|
|
||||||
@blacklist.command(name='clear')
|
@blacklist.command(name="clear")
|
||||||
async def blacklist_clear(self, ctx):
|
async def blacklist_clear(self, ctx):
|
||||||
"""
|
"""
|
||||||
Clears the blacklist.
|
Clears the blacklist.
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ from .utils import TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import Config
|
from . import Config
|
||||||
|
|
||||||
__all__ = ['load_basic_configuration', 'cog_data_path', 'core_data_path',
|
__all__ = [
|
||||||
'load_bundled_data', 'bundled_data_path', 'storage_details',
|
"load_basic_configuration",
|
||||||
'storage_type']
|
"cog_data_path",
|
||||||
|
"core_data_path",
|
||||||
|
"load_bundled_data",
|
||||||
|
"bundled_data_path",
|
||||||
|
"storage_details",
|
||||||
|
"storage_type",
|
||||||
|
]
|
||||||
|
|
||||||
log = logging.getLogger("red.data_manager")
|
log = logging.getLogger("red.data_manager")
|
||||||
|
|
||||||
@@ -25,20 +31,16 @@ basic_config = None
|
|||||||
|
|
||||||
instance_name = None
|
instance_name = None
|
||||||
|
|
||||||
basic_config_default = {
|
basic_config_default = {"DATA_PATH": None, "COG_PATH_APPEND": "cogs", "CORE_PATH_APPEND": "core"}
|
||||||
"DATA_PATH": None,
|
|
||||||
"COG_PATH_APPEND": "cogs",
|
|
||||||
"CORE_PATH_APPEND": "core"
|
|
||||||
}
|
|
||||||
|
|
||||||
config_dir = None
|
config_dir = None
|
||||||
appdir = appdirs.AppDirs("Red-DiscordBot")
|
appdir = appdirs.AppDirs("Red-DiscordBot")
|
||||||
if sys.platform == 'linux':
|
if sys.platform == "linux":
|
||||||
if 0 < os.getuid() < 1000:
|
if 0 < os.getuid() < 1000:
|
||||||
config_dir = Path(appdir.site_data_dir)
|
config_dir = Path(appdir.site_data_dir)
|
||||||
if not config_dir:
|
if not config_dir:
|
||||||
config_dir = Path(appdir.user_config_dir)
|
config_dir = Path(appdir.user_config_dir)
|
||||||
config_file = config_dir / 'config.json'
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
|
|
||||||
def load_basic_configuration(instance_name_: str):
|
def load_basic_configuration(instance_name_: str):
|
||||||
@@ -67,20 +69,23 @@ def load_basic_configuration(instance_name_: str):
|
|||||||
config = jsonio._load_json()
|
config = jsonio._load_json()
|
||||||
basic_config = config[instance_name]
|
basic_config = config[instance_name]
|
||||||
except (FileNotFoundError, KeyError):
|
except (FileNotFoundError, KeyError):
|
||||||
print("You need to configure the bot instance using `redbot-setup`"
|
print(
|
||||||
" prior to running the bot.")
|
"You need to configure the bot instance using `redbot-setup`"
|
||||||
|
" prior to running the bot."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _base_data_path() -> Path:
|
def _base_data_path() -> Path:
|
||||||
if basic_config is None:
|
if basic_config is None:
|
||||||
raise RuntimeError("You must load the basic config before you"
|
raise RuntimeError(
|
||||||
" can get the base data path.")
|
"You must load the basic config before you" " can get the base data path."
|
||||||
path = basic_config['DATA_PATH']
|
)
|
||||||
|
path = basic_config["DATA_PATH"]
|
||||||
return Path(path).resolve()
|
return Path(path).resolve()
|
||||||
|
|
||||||
|
|
||||||
def cog_data_path(cog_instance=None, raw_name: str=None) -> Path:
|
def cog_data_path(cog_instance=None, raw_name: str = None) -> Path:
|
||||||
"""Gets the base cog data path. If you want to get the folder with
|
"""Gets the base cog data path. If you want to get the folder with
|
||||||
which to store your own cog's data please pass in an instance
|
which to store your own cog's data please pass in an instance
|
||||||
of your cog class.
|
of your cog class.
|
||||||
@@ -104,9 +109,10 @@ def cog_data_path(cog_instance=None, raw_name: str=None) -> Path:
|
|||||||
try:
|
try:
|
||||||
base_data_path = Path(_base_data_path())
|
base_data_path = Path(_base_data_path())
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise RuntimeError("You must load the basic config before you"
|
raise RuntimeError(
|
||||||
" can get the cog data path.") from e
|
"You must load the basic config before you" " can get the cog data path."
|
||||||
cog_path = base_data_path / basic_config['COG_PATH_APPEND']
|
) from e
|
||||||
|
cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
|
||||||
|
|
||||||
if raw_name is not None:
|
if raw_name is not None:
|
||||||
cog_path = cog_path / raw_name
|
cog_path = cog_path / raw_name
|
||||||
@@ -121,9 +127,10 @@ def core_data_path() -> Path:
|
|||||||
try:
|
try:
|
||||||
base_data_path = Path(_base_data_path())
|
base_data_path = Path(_base_data_path())
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise RuntimeError("You must load the basic config before you"
|
raise RuntimeError(
|
||||||
" can get the core data path.") from e
|
"You must load the basic config before you" " can get the core data path."
|
||||||
core_path = base_data_path / basic_config['CORE_PATH_APPEND']
|
) from e
|
||||||
|
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
|
||||||
core_path.mkdir(exist_ok=True, parents=True)
|
core_path.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
return core_path.resolve()
|
return core_path.resolve()
|
||||||
@@ -145,15 +152,13 @@ def _find_data_files(init_location: str) -> (Path, List[Path]):
|
|||||||
if not init_file.is_file():
|
if not init_file.is_file():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
package_folder = init_file.parent.resolve() / 'data'
|
package_folder = init_file.parent.resolve() / "data"
|
||||||
if not package_folder.is_dir():
|
if not package_folder.is_dir():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
all_files = list(package_folder.rglob("*"))
|
all_files = list(package_folder.rglob("*"))
|
||||||
|
|
||||||
return package_folder, [p.resolve()
|
return package_folder, [p.resolve() for p in all_files if p.is_file()]
|
||||||
for p in all_files
|
|
||||||
if p.is_file()]
|
|
||||||
|
|
||||||
|
|
||||||
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
|
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
|
||||||
@@ -181,27 +186,24 @@ def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir:
|
|||||||
yield block
|
yield block
|
||||||
block = afile.read(blocksize)
|
block = afile.read(blocksize)
|
||||||
|
|
||||||
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir))
|
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) for p in to_copy}
|
||||||
for p in to_copy}
|
|
||||||
|
|
||||||
for orig, poss_existing in lookup.items():
|
for orig, poss_existing in lookup.items():
|
||||||
if not poss_existing.is_file():
|
if not poss_existing.is_file():
|
||||||
poss_existing.parent.mkdir(exist_ok=True, parents=True)
|
poss_existing.parent.mkdir(exist_ok=True, parents=True)
|
||||||
exists_checksum = None
|
exists_checksum = None
|
||||||
else:
|
else:
|
||||||
exists_checksum = hash_bytestr_iter(file_as_blockiter(
|
exists_checksum = hash_bytestr_iter(
|
||||||
poss_existing.open('rb')), hashlib.sha256())
|
file_as_blockiter(poss_existing.open("rb")), hashlib.sha256()
|
||||||
|
)
|
||||||
|
|
||||||
orig_checksum = ...
|
orig_checksum = ...
|
||||||
if exists_checksum is not None:
|
if exists_checksum is not None:
|
||||||
orig_checksum = hash_bytestr_iter(file_as_blockiter(
|
orig_checksum = hash_bytestr_iter(file_as_blockiter(orig.open("rb")), hashlib.sha256())
|
||||||
orig.open('rb')), hashlib.sha256())
|
|
||||||
|
|
||||||
if exists_checksum != orig_checksum:
|
if exists_checksum != orig_checksum:
|
||||||
shutil.copy(str(orig), str(poss_existing))
|
shutil.copy(str(orig), str(poss_existing))
|
||||||
log.debug("Copying {} to {}".format(
|
log.debug("Copying {} to {}".format(orig, poss_existing))
|
||||||
orig, poss_existing
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def load_bundled_data(cog_instance, init_location: str):
|
def load_bundled_data(cog_instance, init_location: str):
|
||||||
@@ -233,7 +235,7 @@ def load_bundled_data(cog_instance, init_location: str):
|
|||||||
"""
|
"""
|
||||||
bundled_data_folder, to_copy = _find_data_files(init_location)
|
bundled_data_folder, to_copy = _find_data_files(init_location)
|
||||||
|
|
||||||
cog_data_folder = cog_data_path(cog_instance) / 'bundled_data'
|
cog_data_folder = cog_data_path(cog_instance) / "bundled_data"
|
||||||
|
|
||||||
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
|
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
|
||||||
|
|
||||||
@@ -264,12 +266,10 @@ def bundled_data_path(cog_instance) -> Path:
|
|||||||
If no bundled data folder exists or if it hasn't been loaded yet.
|
If no bundled data folder exists or if it hasn't been loaded yet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bundled_path = cog_data_path(cog_instance) / 'bundled_data'
|
bundled_path = cog_data_path(cog_instance) / "bundled_data"
|
||||||
|
|
||||||
if not bundled_path.is_dir():
|
if not bundled_path.is_dir():
|
||||||
raise FileNotFoundError("No such directory {}".format(
|
raise FileNotFoundError("No such directory {}".format(bundled_path))
|
||||||
bundled_path
|
|
||||||
))
|
|
||||||
|
|
||||||
return bundled_path
|
return bundled_path
|
||||||
|
|
||||||
@@ -282,9 +282,9 @@ def storage_type() -> str:
|
|||||||
str
|
str
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return basic_config['STORAGE_TYPE']
|
return basic_config["STORAGE_TYPE"]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise RuntimeError('Bot basic config has not been loaded yet.') from e
|
raise RuntimeError("Bot basic config has not been loaded yet.") from e
|
||||||
|
|
||||||
|
|
||||||
def storage_details() -> dict:
|
def storage_details() -> dict:
|
||||||
@@ -297,6 +297,6 @@ def storage_details() -> dict:
|
|||||||
dict
|
dict
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return basic_config['STORAGE_DETAILS']
|
return basic_config["STORAGE_DETAILS"]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise RuntimeError('Bot basic config has not been loaded yet.') from e
|
raise RuntimeError("Bot basic config has not been loaded yet.") from e
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import discord
|
|||||||
from . import checks, commands
|
from . import checks, commands
|
||||||
from .i18n import Translator
|
from .i18n import Translator
|
||||||
from .utils.chat_formatting import box, pagify
|
from .utils.chat_formatting import box, pagify
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Notice:
|
Notice:
|
||||||
|
|
||||||
@@ -32,11 +33,11 @@ class Dev:
|
|||||||
def cleanup_code(content):
|
def cleanup_code(content):
|
||||||
"""Automatically removes code blocks from the code."""
|
"""Automatically removes code blocks from the code."""
|
||||||
# remove ```py\n```
|
# remove ```py\n```
|
||||||
if content.startswith('```') and content.endswith('```'):
|
if content.startswith("```") and content.endswith("```"):
|
||||||
return '\n'.join(content.split('\n')[1:-1])
|
return "\n".join(content.split("\n")[1:-1])
|
||||||
|
|
||||||
# remove `foo`
|
# remove `foo`
|
||||||
return content.strip('` \n')
|
return content.strip("` \n")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_syntax_error(e):
|
def get_syntax_error(e):
|
||||||
@@ -45,11 +46,10 @@ class Dev:
|
|||||||
Returns a string representation of the error formatted as a codeblock.
|
Returns a string representation of the error formatted as a codeblock.
|
||||||
"""
|
"""
|
||||||
if e.text is None:
|
if e.text is None:
|
||||||
return box('{0.__class__.__name__}: {0}'.format(e), lang="py")
|
return box("{0.__class__.__name__}: {0}".format(e), lang="py")
|
||||||
return box(
|
return box(
|
||||||
'{0.text}{1:>{0.offset}}\n{2}: {0}'
|
"{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
|
||||||
''.format(e, '^', type(e).__name__),
|
)
|
||||||
lang="py")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pages(msg: str):
|
def get_pages(msg: str):
|
||||||
@@ -90,15 +90,15 @@ class Dev:
|
|||||||
_ - The result of the last dev command.
|
_ - The result of the last dev command.
|
||||||
"""
|
"""
|
||||||
env = {
|
env = {
|
||||||
'bot': ctx.bot,
|
"bot": ctx.bot,
|
||||||
'ctx': ctx,
|
"ctx": ctx,
|
||||||
'channel': ctx.channel,
|
"channel": ctx.channel,
|
||||||
'author': ctx.author,
|
"author": ctx.author,
|
||||||
'guild': ctx.guild,
|
"guild": ctx.guild,
|
||||||
'message': ctx.message,
|
"message": ctx.message,
|
||||||
'discord': discord,
|
"discord": discord,
|
||||||
'commands': commands,
|
"commands": commands,
|
||||||
'_': self._last_result
|
"_": self._last_result,
|
||||||
}
|
}
|
||||||
|
|
||||||
code = self.cleanup_code(code)
|
code = self.cleanup_code(code)
|
||||||
@@ -109,8 +109,7 @@ class Dev:
|
|||||||
await ctx.send(self.get_syntax_error(e))
|
await ctx.send(self.get_syntax_error(e))
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(
|
await ctx.send(box("{}: {!s}".format(type(e).__name__, e), lang="py"))
|
||||||
box('{}: {!s}'.format(type(e).__name__, e), lang='py'))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if asyncio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
||||||
@@ -122,7 +121,7 @@ class Dev:
|
|||||||
|
|
||||||
await ctx.send_interactive(self.get_pages(result), box_lang="py")
|
await ctx.send_interactive(self.get_pages(result), box_lang="py")
|
||||||
|
|
||||||
@commands.command(name='eval')
|
@commands.command(name="eval")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _eval(self, ctx, *, body: str):
|
async def _eval(self, ctx, *, body: str):
|
||||||
"""Execute asynchronous code.
|
"""Execute asynchronous code.
|
||||||
@@ -145,28 +144,28 @@ class Dev:
|
|||||||
_ - The result of the last dev command.
|
_ - The result of the last dev command.
|
||||||
"""
|
"""
|
||||||
env = {
|
env = {
|
||||||
'bot': ctx.bot,
|
"bot": ctx.bot,
|
||||||
'ctx': ctx,
|
"ctx": ctx,
|
||||||
'channel': ctx.channel,
|
"channel": ctx.channel,
|
||||||
'author': ctx.author,
|
"author": ctx.author,
|
||||||
'guild': ctx.guild,
|
"guild": ctx.guild,
|
||||||
'message': ctx.message,
|
"message": ctx.message,
|
||||||
'discord': discord,
|
"discord": discord,
|
||||||
'commands': commands,
|
"commands": commands,
|
||||||
'_': self._last_result
|
"_": self._last_result,
|
||||||
}
|
}
|
||||||
|
|
||||||
body = self.cleanup_code(body)
|
body = self.cleanup_code(body)
|
||||||
stdout = io.StringIO()
|
stdout = io.StringIO()
|
||||||
|
|
||||||
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
|
to_compile = "async def func():\n%s" % textwrap.indent(body, " ")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exec(to_compile, env)
|
exec(to_compile, env)
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
return await ctx.send(self.get_syntax_error(e))
|
return await ctx.send(self.get_syntax_error(e))
|
||||||
|
|
||||||
func = env['func']
|
func = env["func"]
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
with redirect_stdout(stdout):
|
with redirect_stdout(stdout):
|
||||||
@@ -199,43 +198,43 @@ class Dev:
|
|||||||
async function.
|
async function.
|
||||||
"""
|
"""
|
||||||
variables = {
|
variables = {
|
||||||
'ctx': ctx,
|
"ctx": ctx,
|
||||||
'bot': ctx.bot,
|
"bot": ctx.bot,
|
||||||
'message': ctx.message,
|
"message": ctx.message,
|
||||||
'guild': ctx.guild,
|
"guild": ctx.guild,
|
||||||
'channel': ctx.channel,
|
"channel": ctx.channel,
|
||||||
'author': ctx.author,
|
"author": ctx.author,
|
||||||
'_': None,
|
"_": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.channel.id in self.sessions:
|
if ctx.channel.id in self.sessions:
|
||||||
await ctx.send(_('Already running a REPL session in this channel. '
|
await ctx.send(
|
||||||
'Exit it with `quit`.'))
|
_("Already running a REPL session in this channel. " "Exit it with `quit`.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.sessions.add(ctx.channel.id)
|
self.sessions.add(ctx.channel.id)
|
||||||
await ctx.send(_('Enter code to execute or evaluate.'
|
await ctx.send(_("Enter code to execute or evaluate." " `exit()` or `quit` to exit."))
|
||||||
' `exit()` or `quit` to exit.'))
|
|
||||||
|
|
||||||
msg_check = lambda m: (m.author == ctx.author and
|
msg_check = lambda m: (
|
||||||
m.channel == ctx.channel and
|
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
|
||||||
m.content.startswith('`'))
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
response = await ctx.bot.wait_for("message", check=msg_check)
|
response = await ctx.bot.wait_for("message", check=msg_check)
|
||||||
|
|
||||||
cleaned = self.cleanup_code(response.content)
|
cleaned = self.cleanup_code(response.content)
|
||||||
|
|
||||||
if cleaned in ('quit', 'exit', 'exit()'):
|
if cleaned in ("quit", "exit", "exit()"):
|
||||||
await ctx.send('Exiting.')
|
await ctx.send("Exiting.")
|
||||||
self.sessions.remove(ctx.channel.id)
|
self.sessions.remove(ctx.channel.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
executor = exec
|
executor = exec
|
||||||
if cleaned.count('\n') == 0:
|
if cleaned.count("\n") == 0:
|
||||||
# single statement, potentially 'eval'
|
# single statement, potentially 'eval'
|
||||||
try:
|
try:
|
||||||
code = compile(cleaned, '<repl session>', 'eval')
|
code = compile(cleaned, "<repl session>", "eval")
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -243,12 +242,12 @@ class Dev:
|
|||||||
|
|
||||||
if executor is exec:
|
if executor is exec:
|
||||||
try:
|
try:
|
||||||
code = compile(cleaned, '<repl session>', 'exec')
|
code = compile(cleaned, "<repl session>", "exec")
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
await ctx.send(self.get_syntax_error(e))
|
await ctx.send(self.get_syntax_error(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
variables['message'] = response
|
variables["message"] = response
|
||||||
|
|
||||||
stdout = io.StringIO()
|
stdout = io.StringIO()
|
||||||
|
|
||||||
@@ -266,7 +265,7 @@ class Dev:
|
|||||||
value = stdout.getvalue()
|
value = stdout.getvalue()
|
||||||
if result is not None:
|
if result is not None:
|
||||||
msg = "{}{}".format(value, result)
|
msg = "{}{}".format(value, result)
|
||||||
variables['_'] = result
|
variables["_"] = result
|
||||||
elif value:
|
elif value:
|
||||||
msg = "{}".format(value)
|
msg = "{}".format(value)
|
||||||
|
|
||||||
@@ -277,7 +276,7 @@ class Dev:
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
pass
|
pass
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
await ctx.send(_('Unexpected error: `{}`').format(e))
|
await ctx.send(_("Unexpected error: `{}`").format(e))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -290,7 +289,7 @@ class Dev:
|
|||||||
msg.author = user
|
msg.author = user
|
||||||
msg.content = ctx.prefix + command
|
msg.content = ctx.prefix + command
|
||||||
|
|
||||||
ctx.bot.dispatch('message', msg)
|
ctx.bot.dispatch("message", msg)
|
||||||
|
|
||||||
@commands.command(name="mockmsg")
|
@commands.command(name="mockmsg")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ def get_driver(type, *args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
if type == "JSON":
|
if type == "JSON":
|
||||||
from .red_json import JSON
|
from .red_json import JSON
|
||||||
|
|
||||||
return JSON(*args, **kwargs)
|
return JSON(*args, **kwargs)
|
||||||
elif type == "MongoDB":
|
elif type == "MongoDB":
|
||||||
from .red_mongo import Mongo
|
from .red_mongo import Mongo
|
||||||
|
|
||||||
return Mongo(*args, **kwargs)
|
return Mongo(*args, **kwargs)
|
||||||
raise RuntimeError("Invalid driver type: '{}'".format(type))
|
raise RuntimeError("Invalid driver type: '{}'".format(type))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ __all__ = ["BaseDriver"]
|
|||||||
|
|
||||||
|
|
||||||
class BaseDriver:
|
class BaseDriver:
|
||||||
|
|
||||||
def __init__(self, cog_name, identifier):
|
def __init__(self, cog_name, identifier):
|
||||||
self.cog_name = cog_name
|
self.cog_name = cog_name
|
||||||
self.unique_cog_identifier = identifier
|
self.unique_cog_identifier = identifier
|
||||||
|
|||||||
@@ -44,14 +44,21 @@ class JSON(BaseDriver):
|
|||||||
|
|
||||||
The path in which to store the file indicated by :py:attr:`file_name`.
|
The path in which to store the file indicated by :py:attr:`file_name`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, cog_name, identifier, *, data_path_override: Path=None,
|
|
||||||
file_name_override: str="settings.json"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
cog_name,
|
||||||
|
identifier,
|
||||||
|
*,
|
||||||
|
data_path_override: Path = None,
|
||||||
|
file_name_override: str = "settings.json"
|
||||||
|
):
|
||||||
super().__init__(cog_name, identifier)
|
super().__init__(cog_name, identifier)
|
||||||
self.file_name = file_name_override
|
self.file_name = file_name_override
|
||||||
if data_path_override:
|
if data_path_override:
|
||||||
self.data_path = data_path_override
|
self.data_path = data_path_override
|
||||||
else:
|
else:
|
||||||
self.data_path = Path.cwd() / 'cogs' / '.data' / self.cog_name
|
self.data_path = Path.cwd() / "cogs" / ".data" / self.cog_name
|
||||||
|
|
||||||
self.data_path.mkdir(parents=True, exist_ok=True)
|
self.data_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user