mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
326 Commits
3.0.0b12
...
3.0.0rc1.p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c706e8c45 | ||
|
|
91029b73e5 | ||
|
|
de4b42a11e | ||
|
|
03230b6386 | ||
|
|
4dbf2796c0 | ||
|
|
03892f5ef1 | ||
|
|
fdf3f86ab0 | ||
|
|
7b15ad5989 | ||
|
|
443f2ca556 | ||
|
|
fa692ccc0b | ||
|
|
0c3d8af8f4 | ||
|
|
3a20c11331 | ||
|
|
aa8c9c350e | ||
|
|
139329233a | ||
|
|
d79996aeea | ||
|
|
fb839084fe | ||
|
|
dea9dde637 | ||
|
|
ebc657dcc6 | ||
|
|
80506856fb | ||
|
|
93a0e45c34 | ||
|
|
3cb2b95121 | ||
|
|
a04869ab27 | ||
|
|
c69b1185d3 | ||
|
|
ad7466a026 | ||
|
|
54dad2a604 | ||
|
|
d5899fae83 | ||
|
|
5d44bfabed | ||
|
|
b6c8be5f43 | ||
|
|
b2abfc5710 | ||
|
|
a9b328ff3c | ||
|
|
0870403299 | ||
|
|
f07b78bd0d | ||
|
|
b2497386bb | ||
|
|
f8558b98c1 | ||
|
|
84ac5f3952 | ||
|
|
404800c556 | ||
|
|
415385b747 | ||
|
|
f7dbaca340 | ||
|
|
32b4c6ce86 | ||
|
|
a3c36d4bde | ||
|
|
fc4703ef80 | ||
|
|
a301b2c758 | ||
|
|
e27682abd3 | ||
|
|
df922a0e3e | ||
|
|
51c83d958c | ||
|
|
17139ce439 | ||
|
|
61652a0306 | ||
|
|
113b97b9c9 | ||
|
|
2784730f7f | ||
|
|
1a9216b522 | ||
|
|
08fc732b7b | ||
|
|
54baf687ec | ||
|
|
8096cd803e | ||
|
|
27b0d606c8 | ||
|
|
af220e497f | ||
|
|
892b2487f5 | ||
|
|
7971c02dc5 | ||
|
|
c1d8272b49 | ||
|
|
bce5dd26f3 | ||
|
|
04e97f3516 | ||
|
|
7eed033c9e | ||
|
|
a2fe1a8757 | ||
|
|
9ee860c3f0 | ||
|
|
1dbe9537e9 | ||
|
|
775f4ce69a | ||
|
|
e83beeef34 | ||
|
|
e77cfff892 | ||
|
|
9495432b8f | ||
|
|
d71c334a34 | ||
|
|
aa8cc90ad0 | ||
|
|
589041556e | ||
|
|
85354f2722 | ||
|
|
c0c5535005 | ||
|
|
e126cf9f4e | ||
|
|
c2d23b37a7 | ||
|
|
3a968d707f | ||
|
|
b07c44c8b4 | ||
|
|
43b0a58649 | ||
|
|
f258e93cf7 | ||
|
|
93138b04cb | ||
|
|
0cf54ec9c2 | ||
|
|
ce031cf7bd | ||
|
|
e6495bc7c0 | ||
|
|
1b196bf0fb | ||
|
|
dbed24aaca | ||
|
|
48a7a21aca | ||
|
|
f595afab18 | ||
|
|
0aca00b245 | ||
|
|
9af58d3abf | ||
|
|
dd5ef3696f | ||
|
|
03d49bac53 | ||
|
|
6c082a10b1 | ||
|
|
77944e195a | ||
|
|
6ebfdef025 | ||
|
|
bc39a6741c | ||
|
|
bda7e08208 | ||
|
|
aa69dd381f | ||
|
|
1fd8a8e0a6 | ||
|
|
1329fa1b09 | ||
|
|
b550f38eed | ||
|
|
ae7b912ac8 | ||
|
|
af9478922e | ||
|
|
7acea29cdb | ||
|
|
6082eb21e3 | ||
|
|
a91cda4995 | ||
|
|
7959654dc8 | ||
|
|
dc9a85ca98 | ||
|
|
591ed50ac3 | ||
|
|
47350328e6 | ||
|
|
75ed749cb3 | ||
|
|
f44ea8b749 | ||
|
|
76c0071f57 | ||
|
|
2a396b4438 | ||
|
|
51a54863c5 | ||
|
|
06f986b92e | ||
|
|
652ceba845 | ||
|
|
16d0f54d8f | ||
|
|
872cce784a | ||
|
|
aec3ad382a | ||
|
|
9d4f9ef73c | ||
|
|
cf7cafc261 | ||
|
|
e3bff7e87c | ||
|
|
4b19421075 | ||
|
|
cf371e8093 | ||
|
|
5eeadc6399 | ||
|
|
f6823ea3d1 | ||
|
|
f24290c423 | ||
|
|
f8a36885fe | ||
|
|
a555eff2cc | ||
|
|
05c389623c | ||
|
|
bf00f5e9a2 | ||
|
|
7685c4d5d5 | ||
|
|
e701ec9617 | ||
|
|
6c1ee096a1 | ||
|
|
2df282222f | ||
|
|
43c7bd48c7 | ||
|
|
86579068d9 | ||
|
|
8e6ab9aa35 | ||
|
|
77566a887a | ||
|
|
9d0eca1914 | ||
|
|
79a3164d9d | ||
|
|
eb73e48192 | ||
|
|
cd6af7f185 | ||
|
|
3d6020b9cf | ||
|
|
461f03aac0 | ||
|
|
35149f8837 | ||
|
|
c0d01f32a6 | ||
|
|
83a0459b6a | ||
|
|
50f6dcef2f | ||
|
|
5c514fd663 | ||
|
|
1c2196f78f | ||
|
|
43cc3c40f3 | ||
|
|
7a6a4cf59d | ||
|
|
3bcf375204 | ||
|
|
a175bdc1c7 | ||
|
|
b557b437a3 | ||
|
|
d1f0b59b5d | ||
|
|
3ece3a1f2b | ||
|
|
1f1a85de18 | ||
|
|
e08c9dafa6 | ||
|
|
ad27607ccc | ||
|
|
c1bcca4432 | ||
|
|
9f2ed694ce | ||
|
|
edadd8f2fd | ||
|
|
afa08713e0 | ||
|
|
d23620727e | ||
|
|
b456c6ad3b | ||
|
|
0298b53803 | ||
|
|
bfd6e4af3f | ||
|
|
31612aae4a | ||
|
|
219367e7c1 | ||
|
|
7b64f10fc7 | ||
|
|
1ad1744054 | ||
|
|
7b825f2cd7 | ||
|
|
3759fce090 | ||
|
|
470521f7c8 | ||
|
|
a070dffb93 | ||
|
|
9e7bc94aab | ||
|
|
033d0113a5 | ||
|
|
d0a53ed2df | ||
|
|
49b80e9fe3 | ||
|
|
d5f5ddbec5 | ||
|
|
17c7dd658d | ||
|
|
ca19ecaefc | ||
|
|
c149f00f82 | ||
|
|
b041d59fc7 | ||
|
|
b983d5904b | ||
|
|
8b15053dd4 | ||
|
|
e15815cd97 | ||
|
|
94a64d8fae | ||
|
|
fd7088de1a | ||
|
|
7d4946560d | ||
|
|
b7c9647e1a | ||
|
|
36b9f64aae | ||
|
|
60a72b2ba4 | ||
|
|
f830f73ae6 | ||
|
|
95f51e1126 | ||
|
|
8916f55d52 | ||
|
|
4aaef9558a | ||
|
|
0b78664792 | ||
|
|
db5d4d5158 | ||
|
|
0dfd8b6453 | ||
|
|
11a2fb1088 | ||
|
|
40feeff442 | ||
|
|
a0a2976e0a | ||
|
|
741f3cbdcc | ||
|
|
a6965c4b5a | ||
|
|
19b05e632c | ||
|
|
8610b47a68 | ||
|
|
2ab8890540 | ||
|
|
5de5a519c3 | ||
|
|
0d193d3e9e | ||
|
|
622382f425 | ||
|
|
c1f09326cc | ||
|
|
ddbbba4aaa | ||
|
|
bcf7ea30c5 | ||
|
|
35e9fab701 | ||
|
|
864b6d313e | ||
|
|
d47d12e961 | ||
|
|
9f0e752318 | ||
|
|
34bd5ead15 | ||
|
|
1fd5dffdc7 | ||
|
|
6d7a900bbb | ||
|
|
fb4f921159 | ||
|
|
14cc701b25 | ||
|
|
d8c4113d24 | ||
|
|
9eb6bb7738 | ||
|
|
de96f8b9f9 | ||
|
|
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 | ||
|
|
501aff41ea | ||
|
|
449b1bfe9e | ||
|
|
4a8358ecb4 | ||
|
|
8f74e4dd31 | ||
|
|
2b35d9f012 | ||
|
|
35001107e0 | ||
|
|
a7d7b90ae8 | ||
|
|
119ba7ef8b | ||
|
|
28bbe9c646 | ||
|
|
8739c04024 | ||
|
|
57240d25b9 | ||
|
|
15ea5440a3 | ||
|
|
1e60d1c265 | ||
|
|
b7cd097c43 | ||
|
|
6c934b02e6 | ||
|
|
fcb9b40b43 | ||
|
|
7a6884e4b1 | ||
|
|
e86698cfeb | ||
|
|
53650aefa6 | ||
|
|
1d80a0cad1 | ||
|
|
f6d27a0f43 | ||
|
|
f71aa9dd21 | ||
|
|
1cb5394e96 | ||
|
|
2b2dbd25f7 | ||
|
|
dd4cd0eeb1 | ||
|
|
ee7b0cf730 | ||
|
|
95ef5d6348 | ||
|
|
23192b9ef6 | ||
|
|
7cd98c8a63 | ||
|
|
fca7686701 | ||
|
|
be767478f4 | ||
|
|
b3ad5d90ed | ||
|
|
fb093b7411 | ||
|
|
e4ea3110e3 | ||
|
|
79676c4f72 | ||
|
|
d61827b92c | ||
|
|
1f1f46c70f | ||
|
|
9188e4a7ec | ||
|
|
e5a780eb0c | ||
|
|
d8c85a2b15 | ||
|
|
83080bc5a2 | ||
|
|
233bfc59ac | ||
|
|
c606caf3a3 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -24,6 +24,8 @@ redbot/core/utils/mod.py @palmtree5
|
|||||||
redbot/core/utils/data_converter.py @mikeshardmind
|
redbot/core/utils/data_converter.py @mikeshardmind
|
||||||
redbot/core/utils/antispam.py @mikeshardmind
|
redbot/core/utils/antispam.py @mikeshardmind
|
||||||
redbot/core/utils/tunnel.py @mikeshardmind
|
redbot/core/utils/tunnel.py @mikeshardmind
|
||||||
|
redbot/core/utils/caching.py @mikeshardmind
|
||||||
|
redbot/core/utils/common_filters.py @mikeshardmind
|
||||||
|
|
||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
@@ -43,6 +45,8 @@ 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
|
||||||
|
redbot/cogs/warnings/* @palmtree5
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|||||||
116
.github/CONTRIBUTING.md
vendored
116
.github/CONTRIBUTING.md
vendored
@@ -1,23 +1,43 @@
|
|||||||
# 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.6 and above.
|
||||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||||
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,87 @@ 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.2 or greater (3.6.6 or greater on Windows)
|
||||||
|
- 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 python 3.6 and 3.7 (test environments `py36` and `py37`)
|
||||||
|
- 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 -N <src>`.
|
||||||
|
|
||||||
|
### 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 +138,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).
|
||||||
|
|||||||
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Command bugs
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Did you find a bug with a command? Fill out the following:
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Command name
|
||||||
|
|
||||||
|
<!-- Replace this line with the name of the command -->
|
||||||
|
|
||||||
|
#### What cog is this command from?
|
||||||
|
|
||||||
|
<!-- Replace this line with the name of the cog -->
|
||||||
|
|
||||||
|
#### What were you expecting to happen?
|
||||||
|
|
||||||
|
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||||
|
|
||||||
|
#### What actually happened?
|
||||||
|
|
||||||
|
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||||
|
|
||||||
|
#### How can we reproduce this issue?
|
||||||
|
|
||||||
|
<!-- Replace with numbered steps to reproduce the issue -->
|
||||||
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Feature request
|
||||||
|
|
||||||
|
<!-- This template is for feature requests. Please fill out the following: -->
|
||||||
|
|
||||||
|
|
||||||
|
#### Select the type of feature you are requesting:
|
||||||
|
|
||||||
|
<!-- To check a box, replace the space between the [] with a x -->
|
||||||
|
|
||||||
|
- [ ] Cog
|
||||||
|
- [ ] Command
|
||||||
|
- [ ] API functionality
|
||||||
|
|
||||||
|
#### Describe your requested feature
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Feel free to describe in as much detail as you wish.
|
||||||
|
|
||||||
|
If you are requesting a cog to be included in core:
|
||||||
|
- Describe the functionality in as much detail as possible
|
||||||
|
- Include the command structure, if possible
|
||||||
|
- Please note that unless it's something that should be core functionality,
|
||||||
|
we reserve the right to reject your suggestion and point you to our cog
|
||||||
|
board to request it for a third-party cog
|
||||||
|
|
||||||
|
If you are requesting a command:
|
||||||
|
- Include what cog it should be in and a name for the command
|
||||||
|
- Describe the intended functionality for the command
|
||||||
|
- Note any restrictions on who can use the command or where it can be used
|
||||||
|
|
||||||
|
If you are requesting API functionality:
|
||||||
|
- Describe what it should do
|
||||||
|
- Note whether it is to extend existing functionality or introduce new functionality
|
||||||
|
|
||||||
|
-->
|
||||||
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Other bugs
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Did you find a bug with something other than a command? Fill out the following:
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### What were you trying to do?
|
||||||
|
|
||||||
|
<!-- Replace this line with a description of what you were trying to do -->
|
||||||
|
|
||||||
|
#### What were you expecting to happen?
|
||||||
|
|
||||||
|
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||||
|
|
||||||
|
#### What actually happened?
|
||||||
|
|
||||||
|
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||||
|
|
||||||
|
#### How can we reproduce this issue?
|
||||||
|
|
||||||
|
<!-- Replace with numbered steps to reproduce the issue -->
|
||||||
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Bugfix request
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To be used for pull requests that fix a bug
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Describe the bug being fixed
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If an issue exists for the bug, mention
|
||||||
|
that this PR fixes that issue
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Anything we need to know about this fix?
|
||||||
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Enhancement request
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To be used for PRs which enhance existing features
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Describe the enhancement
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe what your changes do.
|
||||||
|
If adding commands, describe any restrictions on their usage.
|
||||||
|
- For example, who can use the command? Where can it be used?
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Does this enhancement break existing functionality?
|
||||||
|
|
||||||
|
<!-- To check a box, replace the space between the [] with a x -->
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# New feature addition
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To be used for PRs which add a new feature
|
||||||
|
Examples of this include new APIs, new core cogs, etc.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### What type of feature is this?
|
||||||
|
|
||||||
|
<!-- To check a box, replace the space between the [] with a x -->
|
||||||
|
|
||||||
|
- [ ] New core cog
|
||||||
|
- [ ] New API
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
#### Describe the feature
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||||
|
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||||
|
-->
|
||||||
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# New release
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To be used by collaborators for doing releases.
|
||||||
|
Most contributors will not need to use this.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Has a draft release been created for this?
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Translations update
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Used for PRs updating translations from Crowdin
|
||||||
|
-->
|
||||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,40 +1,16 @@
|
|||||||
# Trivia list repo injection
|
|
||||||
redbot/trivia/
|
|
||||||
|
|
||||||
*.json
|
*.json
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.dll
|
||||||
|
*.pot
|
||||||
.data
|
.data
|
||||||
|
!/tests/cogs/dataconverter/data/**/*.json
|
||||||
|
|
||||||
### JetBrains template
|
### JetBrains template
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
# User-specific stuff:
|
# User-specific stuff:
|
||||||
.idea/**/workspace.xml
|
.idea/
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
|
|
||||||
# Sensitive or high-churn files:
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.xml
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
|
|
||||||
# Gradle:
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-debug/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin:
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
## File-based project format:
|
|
||||||
*.iws
|
*.iws
|
||||||
|
|
||||||
## Plugin-specific files:
|
## Plugin-specific files:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ formats:
|
|||||||
build:
|
build:
|
||||||
image: latest
|
image: latest
|
||||||
|
|
||||||
requirements_file: docs/requirements.txt
|
requirements_file: dependency_links.txt
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.6
|
version: 3.6
|
||||||
|
|||||||
43
.travis.yml
43
.travis.yml
@@ -1,25 +1,37 @@
|
|||||||
dist: trusty
|
dist: xenial
|
||||||
language: python
|
language: python
|
||||||
cache: pip
|
cache: pip
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
|
sudo: true
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 3.5.3
|
- 3.6.6
|
||||||
- 3.6.1
|
- 3.7
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
PIPENV_IGNORE_VIRTUALENVS=1
|
||||||
|
matrix:
|
||||||
|
TOXENV=py
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]" >> requirements.txt
|
- pip install --upgrade pip tox
|
||||||
- pip install -r requirements.txt
|
|
||||||
- pip install .[test]
|
|
||||||
script:
|
script:
|
||||||
- python -m compileall ./redbot/cogs
|
- tox
|
||||||
- python -m pytest
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
|
|
||||||
|
- python: 3.6.6
|
||||||
|
env: TOXENV=docs
|
||||||
|
- python: 3.6.6
|
||||||
|
env: TOXENV=style
|
||||||
|
|
||||||
|
# These jobs only occur on tag creation 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.6
|
||||||
env:
|
env:
|
||||||
- DEPLOYING=true
|
- DEPLOYING=true
|
||||||
deploy:
|
deploy:
|
||||||
@@ -30,25 +42,24 @@ jobs:
|
|||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
python: 3.6.6
|
||||||
python: 3.5.3
|
|
||||||
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.6
|
||||||
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
|
||||||
- sudo apt-get install -y crowdin
|
- sudo apt-get install -y crowdin
|
||||||
|
- pip install redgettext==2.1
|
||||||
deploy:
|
deploy:
|
||||||
- provider: script
|
- provider: script
|
||||||
script: python3 ./generate_strings.py
|
script: make gettext
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
python: 3.6.6
|
||||||
python: 3.5.3
|
|
||||||
tags: true
|
tags: true
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
include README.rst
|
include README.md
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include requirements.txt
|
include dependency_links.txt
|
||||||
include discord/bin/*.dll
|
|
||||||
include redbot/cogs/audio/application.yml
|
|
||||||
|
|||||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
reformat:
|
||||||
|
black -l 99 -N `git ls-files "*.py"`
|
||||||
|
stylecheck:
|
||||||
|
black --check -l 99 -N `git ls-files "*.py"`
|
||||||
|
gettext:
|
||||||
|
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||||
|
crowdin upload
|
||||||
12
Pipfile
Normal file
12
Pipfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[[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, extras = ['mongo', 'voice'] }
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
tox = "*"
|
||||||
|
"e1839a9" = { path = ".", editable = true, extras = ['docs', 'test', 'style'] }
|
||||||
748
Pipfile.lock
generated
Normal file
748
Pipfile.lock
generated
Normal file
@@ -0,0 +1,748 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "edd35f353e1fadc20094e40de6627db77fd61303da01794214c44d748e99838b"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||||
|
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||||
|
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||||
|
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||||
|
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||||
|
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||||
|
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||||
|
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||||
|
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||||
|
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||||
|
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||||
|
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||||
|
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||||
|
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||||
|
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||||
|
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||||
|
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||||
|
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||||
|
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||||
|
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||||
|
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||||
|
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||||
|
],
|
||||||
|
"version": "==3.4.4"
|
||||||
|
},
|
||||||
|
"aiohttp-json-rpc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||||
|
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||||
|
],
|
||||||
|
"version": "==0.11.2"
|
||||||
|
},
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
|
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||||
|
],
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"async-timeout": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||||
|
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5.3'",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||||
|
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||||
|
],
|
||||||
|
"version": "==18.2.0"
|
||||||
|
},
|
||||||
|
"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": "836ae730401ea370aa10127bb9c86854c8b516ac"
|
||||||
|
},
|
||||||
|
"distro": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||||
|
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||||
|
],
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"dnspython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c",
|
||||||
|
"sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31"
|
||||||
|
],
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
|
"e1839a8": {
|
||||||
|
"editable": true,
|
||||||
|
"extras": [
|
||||||
|
"mongo",
|
||||||
|
"voice"
|
||||||
|
],
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
"fuzzywuzzy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||||
|
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||||
|
],
|
||||||
|
"version": "==0.17.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||||
|
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||||
|
],
|
||||||
|
"version": "==2.7"
|
||||||
|
},
|
||||||
|
"idna-ssl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
|
||||||
|
"sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
|
||||||
|
],
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||||
|
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||||
|
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||||
|
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||||
|
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||||
|
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||||
|
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||||
|
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||||
|
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||||
|
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||||
|
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||||
|
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||||
|
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||||
|
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||||
|
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||||
|
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||||
|
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||||
|
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||||
|
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||||
|
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||||
|
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||||
|
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||||
|
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||||
|
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||||
|
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||||
|
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||||
|
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||||
|
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||||
|
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||||
|
],
|
||||||
|
"version": "==4.4.2"
|
||||||
|
},
|
||||||
|
"pymongo": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08dea6dbff33363419af7af3bf2e9a373ff71eb22833dd7063f9b953f09a0bdf",
|
||||||
|
"sha256:0949110db76eb1b54cecfc0c0f8468a8b9a7fd42ba23fd0d4a37d97e0b4ca203",
|
||||||
|
"sha256:0c31a39f440801cc8603547ccaacf4cb1f02b81af6ba656621c13677b27f4426",
|
||||||
|
"sha256:1e10b3fda5677d360440ebd12a1185944dc81d9ea9acf0c6b0681013b3fb9bc2",
|
||||||
|
"sha256:1f59440b993666a417ba1954cfb1b7fb11cb4dea1a1d2777897009f688d000ee",
|
||||||
|
"sha256:2b5a3806d9f656c14e9d9b693a344fc5684fdd045155594be0c505c6e9410a94",
|
||||||
|
"sha256:4a14e2d7c2c0e07b5affcfbfc5c395d767f94bb1a822934a41a3b5371cde1458",
|
||||||
|
"sha256:4cb50541225208b37786fdb0de632e475c4f00ec4792579df551ef48d6999d69",
|
||||||
|
"sha256:52999666ad01de885653e1f74a86c2a6520d1004afec475180bebf3d7393a8fc",
|
||||||
|
"sha256:562c353079e8ce7e2ad611fd7436a72f5df97be72bca59ae9ebf789a724afd5c",
|
||||||
|
"sha256:5ce2a71f473f4703daa8d6c61a00b35ce625a7f5015b4371e3af728dafca296a",
|
||||||
|
"sha256:6613e633676168a4500e5e6bb6e3e64d3fdb96d2dc472eb4b99235fb4141adb1",
|
||||||
|
"sha256:8330406f294df118399c721f80979f2516447bcc73e4262826687872c864751e",
|
||||||
|
"sha256:8e939dfa7d16609b99eb4d1fd2fc74f7a90f4fd0aaf31d611822daaff456236f",
|
||||||
|
"sha256:8fa4303e1f50d9f0c8f2f7833b5a370a94d19d41449def62b34ae072126b4dfd",
|
||||||
|
"sha256:966d987975aa3b4cfcdf1495930ff6ecb152fafe8e544e40633e41b24ca3e1c5",
|
||||||
|
"sha256:aec4ea43a1b8e9782246a259410f66692f2d3aa0f03c54477e506193b0781cb6",
|
||||||
|
"sha256:b73f889f032fbef05863f5056b46468a8262ae83628898e20b10bbbb79a3617e",
|
||||||
|
"sha256:b752088a2f819f163d11dfdbbe627b27eef9d8478c7e57d42c5e7c600fee434e",
|
||||||
|
"sha256:c8669f96277f140797e0ff99f80bd706271674942672a38ed694e2bfa66f3900",
|
||||||
|
"sha256:ccf00549efaf6f8d5b35b654beb9aed2b788a5b33b05606eb818ddaa4e924ea3",
|
||||||
|
"sha256:ce7c91463ad21ac72fc795188292b01c8366cf625e2d1e5ed473ce127b844f60",
|
||||||
|
"sha256:d776d8d47884e6ad39ff8a301f1ae6b7d2186f209218cf024f43334dbba79c64",
|
||||||
|
"sha256:dab0f63841aebb2b421fadb31f3c7eef27898f21274a8e5b45c4f2bccb40f9ed",
|
||||||
|
"sha256:daedcfbf3b24b2b687e35b33252a9315425c2dd06a085a36906d516135bdd60e",
|
||||||
|
"sha256:e7ad1ec621db2c5ad47924f63561f75abfd4fff669c62c8cc99c169c90432f59",
|
||||||
|
"sha256:f14fb6c4058772a0d74d82874d3b89d7264d89b4ed7fa0413ea0ef8112b268b9",
|
||||||
|
"sha256:f16c7b6b98bc400d180f05e65e2236ef4ee9d71f3815280558582670e1e67536",
|
||||||
|
"sha256:f2d9eb92b26600ae6e8092f66da4bcede1b61a647c9080d6b44c148aff3a8ea4",
|
||||||
|
"sha256:ffe94f9d17800610dda5282d7f6facfc216d79a93dd728a03d2f21cff3af7cc6"
|
||||||
|
],
|
||||||
|
"version": "==3.7.1"
|
||||||
|
},
|
||||||
|
"python-levenshtein": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||||
|
],
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||||
|
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||||
|
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||||
|
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||||
|
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||||
|
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||||
|
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||||
|
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||||
|
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||||
|
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||||
|
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||||
|
],
|
||||||
|
"version": "==3.13"
|
||||||
|
},
|
||||||
|
"raven": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
|
||||||
|
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
|
||||||
|
],
|
||||||
|
"version": "==6.9.0"
|
||||||
|
},
|
||||||
|
"raven-aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
||||||
|
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
||||||
|
],
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"red-lavalink": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a1a34471ccf4630eee537049568dd87e8e93614f1d1ce355dd74e5b10079782"
|
||||||
|
],
|
||||||
|
"version": "==0.1.2"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||||
|
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||||
|
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||||
|
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||||
|
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||||
|
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||||
|
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||||
|
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||||
|
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||||
|
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||||
|
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||||
|
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||||
|
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||||
|
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||||
|
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||||
|
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||||
|
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||||
|
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||||
|
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||||
|
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||||
|
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
|
"version": "==6.0"
|
||||||
|
},
|
||||||
|
"yarl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
|
||||||
|
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
|
||||||
|
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
|
||||||
|
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
|
||||||
|
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
|
||||||
|
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
|
||||||
|
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
|
||||||
|
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
|
||||||
|
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.4.1'",
|
||||||
|
"version": "==1.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||||
|
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||||
|
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||||
|
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||||
|
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||||
|
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||||
|
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||||
|
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||||
|
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||||
|
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||||
|
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||||
|
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||||
|
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||||
|
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||||
|
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||||
|
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||||
|
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||||
|
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||||
|
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||||
|
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||||
|
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||||
|
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||||
|
],
|
||||||
|
"version": "==3.4.4"
|
||||||
|
},
|
||||||
|
"aiohttp-json-rpc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||||
|
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||||
|
],
|
||||||
|
"version": "==0.11.2"
|
||||||
|
},
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
|
||||||
|
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
|
||||||
|
],
|
||||||
|
"version": "==0.7.11"
|
||||||
|
},
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
|
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||||
|
],
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"async-timeout": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||||
|
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5.3'",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"atomicwrites": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||||
|
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||||
|
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||||
|
],
|
||||||
|
"version": "==18.2.0"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||||
|
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||||
|
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||||
|
],
|
||||||
|
"version": "==18.9b0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||||
|
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||||
|
],
|
||||||
|
"version": "==2018.8.24"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||||
|
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||||
|
],
|
||||||
|
"version": "==7.0"
|
||||||
|
},
|
||||||
|
"colorama": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||||
|
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||||
|
],
|
||||||
|
"version": "==0.3.9"
|
||||||
|
},
|
||||||
|
"distro": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||||
|
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||||
|
],
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||||
|
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||||
|
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||||
|
],
|
||||||
|
"version": "==0.14"
|
||||||
|
},
|
||||||
|
"e1839a9": {
|
||||||
|
"editable": true,
|
||||||
|
"extras": [
|
||||||
|
"docs",
|
||||||
|
"test",
|
||||||
|
"style"
|
||||||
|
],
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
"fuzzywuzzy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||||
|
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||||
|
],
|
||||||
|
"version": "==0.17.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||||
|
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||||
|
],
|
||||||
|
"version": "==2.7"
|
||||||
|
},
|
||||||
|
"idna-ssl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||||
|
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||||
|
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||||
|
],
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||||
|
],
|
||||||
|
"version": "==1.0"
|
||||||
|
},
|
||||||
|
"more-itertools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||||
|
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||||
|
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||||
|
],
|
||||||
|
"version": "==4.3.0"
|
||||||
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||||
|
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||||
|
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||||
|
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||||
|
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||||
|
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||||
|
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||||
|
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||||
|
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||||
|
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||||
|
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||||
|
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||||
|
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||||
|
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||||
|
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||||
|
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||||
|
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||||
|
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||||
|
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||||
|
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||||
|
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||||
|
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||||
|
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||||
|
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||||
|
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||||
|
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||||
|
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||||
|
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||||
|
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||||
|
],
|
||||||
|
"version": "==4.4.2"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||||
|
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||||
|
],
|
||||||
|
"version": "==18.0"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||||
|
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==0.7.1"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
|
||||||
|
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==1.6.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||||
|
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||||
|
],
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||||
|
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||||
|
],
|
||||||
|
"version": "==2.2.2"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||||
|
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||||
|
],
|
||||||
|
"version": "==3.8.2"
|
||||||
|
},
|
||||||
|
"pytest-asyncio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
|
||||||
|
"sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
|
"python-levenshtein": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||||
|
],
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||||
|
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||||
|
],
|
||||||
|
"version": "==2018.5"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||||
|
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||||
|
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||||
|
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||||
|
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||||
|
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||||
|
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||||
|
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||||
|
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||||
|
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||||
|
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||||
|
],
|
||||||
|
"version": "==3.13"
|
||||||
|
},
|
||||||
|
"raven": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
|
||||||
|
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
|
||||||
|
],
|
||||||
|
"version": "==6.9.0"
|
||||||
|
},
|
||||||
|
"raven-aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
||||||
|
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
||||||
|
],
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||||
|
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||||
|
],
|
||||||
|
"version": "==2.19.1"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||||
|
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
|
||||||
|
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
|
||||||
|
],
|
||||||
|
"version": "==1.7.9"
|
||||||
|
},
|
||||||
|
"sphinx-rtd-theme": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
|
||||||
|
"sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
|
||||||
|
],
|
||||||
|
"version": "==0.4.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-asyncio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-websupport": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||||
|
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42",
|
||||||
|
"sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957"
|
||||||
|
],
|
||||||
|
"version": "==0.9.6"
|
||||||
|
},
|
||||||
|
"tox": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7f802b37fffd3b5ef2aab104943fa5dad24bf9564bb7e732e54b8d0cfec2fca0",
|
||||||
|
"sha256:cc97859bd7f38aa5b3b8ba55ffe7ee9952e7050faad1aedc0829cd3db2fb61d6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.4.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||||
|
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.1.*'",
|
||||||
|
"version": "==1.23"
|
||||||
|
},
|
||||||
|
"virtualenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
|
||||||
|
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
|
||||||
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||||
|
"version": "==16.0.0"
|
||||||
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||||
|
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||||
|
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||||
|
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||||
|
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||||
|
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||||
|
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||||
|
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||||
|
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||||
|
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||||
|
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||||
|
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||||
|
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||||
|
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||||
|
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||||
|
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||||
|
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||||
|
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||||
|
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||||
|
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||||
|
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
|
"version": "==6.0"
|
||||||
|
},
|
||||||
|
"yarl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
|
||||||
|
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
|
||||||
|
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
|
||||||
|
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
|
||||||
|
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
|
||||||
|
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
|
||||||
|
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
|
||||||
|
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
|
||||||
|
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.4.1'",
|
||||||
|
"version": "==1.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
README.md
Normal file
135
README.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<br>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and Fully Modular.</h4>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://discord.gg/red">
|
||||||
|
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield" alt="Discord Server">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.patreon.com/Red_Devs">
|
||||||
|
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.python.org/downloads/">
|
||||||
|
<img src="https://img.shields.io/badge/Made%20With-Python%203-blue.svg?style=for-the-badge" alt="Made with Python 3">
|
||||||
|
</a>
|
||||||
|
<a href="https://crowdin.com/project/red-discordbot">
|
||||||
|
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
|
||||||
|
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://travis-ci.org/Cog-Creators/Red-DiscordBot">
|
||||||
|
<img src="https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
|
||||||
|
</a>
|
||||||
|
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop">
|
||||||
|
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop" alt="Red on readthedocs.org">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ambv/black">
|
||||||
|
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
|
||||||
|
</a>
|
||||||
|
<a href="http://makeapullrequest.com">
|
||||||
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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">Plugins</a>
|
||||||
|
•
|
||||||
|
<a href="#join-the-community">Community</a>
|
||||||
|
•
|
||||||
|
<a href="#license">License</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
[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.
|
||||||
|
|
||||||
|
**The default set of modules includes and is not limited to:**
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
- Bank (slot machine, user credits)
|
||||||
|
- Custom commands
|
||||||
|
- Imgur/gif search
|
||||||
|
- Admin automation (self-role assignment, cross-server announcements, mod-mail reports)
|
||||||
|
- Customisable command permissions
|
||||||
|
|
||||||
|
**Additionally, other [plugins](#plugins) (cogs) can be easily found and added from our growing
|
||||||
|
community of cog repositories.**
|
||||||
|
|
||||||
|
# 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_linux_mac.html)
|
||||||
|
- [Ubuntu](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||||
|
- [Debian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||||
|
- [CentOS 7](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||||
|
- [Arch Linux](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||||
|
- [Raspbian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.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 guide you are still experiencing issues, feel free to join the
|
||||||
|
[Official Discord Server](https://discord.gg/red) and ask in the **#v3-support** channel for help.
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
|
||||||
|
Red is fully modular, allowing you to load and unload plugins of your choice, and install 3rd party
|
||||||
|
plugins directly from Discord! A few examples are:
|
||||||
|
|
||||||
|
- 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) at a list of
|
||||||
|
available 3rd party cogs!
|
||||||
|
|
||||||
|
# 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) the cog 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](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
||||||
|
|
||||||
|
Red is named after the main character of "Transistor", a video game by
|
||||||
|
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
||||||
|
|
||||||
|
Artwork created by [Sinlaire](https://sinlaire.deviantart.com/) on Deviant Art for the Red Discord
|
||||||
|
Bot Project.
|
||||||
42
README.rst
42
README.rst
@@ -1,42 +0,0 @@
|
|||||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
|
||||||
: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
|
|
||||||
:target: http://makeapullrequest.com
|
|
||||||
:alt: PRs Welcome
|
|
||||||
|
|
||||||
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
|
|
||||||
:target: https://crowdin.com/project/red-discordbot
|
|
||||||
:alt: Crowdin
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
|
||||||
:target: https://www.patreon.com/Red_Devs
|
|
||||||
:alt: Patreon
|
|
||||||
|
|
||||||
********************
|
|
||||||
Red - Discord Bot v3
|
|
||||||
********************
|
|
||||||
|
|
||||||
**This is in beta and very much a work in progress. Regular use is not recommended.
|
|
||||||
There will not be any effort made to prevent the breaking of current installations.**
|
|
||||||
|
|
||||||
How to install
|
|
||||||
^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Using python3 pip::
|
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot
|
|
||||||
redbot-setup
|
|
||||||
redbot <name>
|
|
||||||
|
|
||||||
To install requirements for voice::
|
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot[voice]
|
|
||||||
|
|
||||||
To install all requirements for docs and tests::
|
|
||||||
|
|
||||||
pip install --process-dependency-links -U Red-DiscordBot[test,docs]
|
|
||||||
|
|
||||||
For the latest git build, replace ``Red-DiscordBot`` in the above commands with
|
|
||||||
``git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop``.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
api_key_env: CROWDIN_API_KEY
|
api_key_env: CROWDIN_API_KEY
|
||||||
project_identifier_env: CROWDIN_PROJECT_ID
|
project_identifier_env: CROWDIN_PROJECT_ID
|
||||||
files:
|
files:
|
||||||
- source: /**/*.pot
|
- source: /redbot/**/*.pot
|
||||||
translation: /%original_path%/%locale%.po
|
translation: /%original_path%/%locale%.po
|
||||||
|
|||||||
1
dependency_links.txt
Normal file
1
dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/Rapptz/discord.py/tarball/836ae730401ea370aa10127bb9c86854c8b516ac#egg=discord.py-1.0.0a0
|
||||||
@@ -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,16 +1,16 @@
|
|||||||
.. 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:
|
||||||
|
|
||||||
:code:`sudo nano /etc/systemd/system/red@.service`
|
:code:`sudo -e /etc/systemd/system/red@.service`
|
||||||
|
|
||||||
Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root):
|
Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root):
|
||||||
|
|
||||||
@@ -27,15 +27,18 @@ 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 **@**
|
||||||
|
|
||||||
To start the bot, run the service and add the instance name after the **@**:
|
To start the bot, run the service and add the instance name after the **@**:
|
||||||
|
|
||||||
@@ -45,4 +48,6 @@ To set the bot to start on boot, you must enable the service, again adding the i
|
|||||||
|
|
||||||
:code:`sudo systemctl enable red@instancename`
|
:code:`sudo systemctl enable red@instancename`
|
||||||
|
|
||||||
.. 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 **@**
|
To view Red’s log, you can acccess through journalctl:
|
||||||
|
|
||||||
|
:code:`sudo journalctl -u red@instancename`
|
||||||
|
|||||||
109
docs/cog_customcom.rst
Normal file
109
docs/cog_customcom.rst
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.. CustomCommands Cog Reference
|
||||||
|
|
||||||
|
============================
|
||||||
|
CustomCommands Cog Reference
|
||||||
|
============================
|
||||||
|
|
||||||
|
------------
|
||||||
|
How it works
|
||||||
|
------------
|
||||||
|
|
||||||
|
CustomCommands allows you to create simple commands for your bot without requiring you to code your own cog for Red.
|
||||||
|
|
||||||
|
If the command you attempt to create shares a name with an already loaded command, you cannot overwrite it with this cog.
|
||||||
|
|
||||||
|
---------
|
||||||
|
Cooldowns
|
||||||
|
---------
|
||||||
|
|
||||||
|
You can set cooldowns for your custom commands. If a command is on cooldown, it will not be triggered.
|
||||||
|
|
||||||
|
You can set cooldowns per member or per channel, or set a cooldown guild-wide. You can also set multiple types of cooldown on a single custom command. All cooldowns must pass before the command will trigger.
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Context Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can enhance your custom command's response by leaving spaces for the bot to substitute.
|
||||||
|
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
| Argument | Substitute |
|
||||||
|
+===========+========================================+
|
||||||
|
| {message} | The message the bot is responding to. |
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
| {author} | The user who called the command. |
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
| {channel} | The channel the command was called in. |
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
| {server} | The server the command was called in. |
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
| {guild} | Same as with {server}. |
|
||||||
|
+-----------+----------------------------------------+
|
||||||
|
|
||||||
|
You can further refine the response with dot notation. For example, {author.mention} will mention the user who called the command.
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Command Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can further enhance your custom command's response by leaving spaces for the user to substitute.
|
||||||
|
|
||||||
|
To do this, simply put {#} in the response, replacing # with any number starting with 0. Each number will be replaced with what the user gave the command, in order.
|
||||||
|
|
||||||
|
You can refine the response with colon notation. For example, {0:Member} will accept members of the server, and {0:int} will accept a number. If no colon notation is provided, the argument will be returned unchanged.
|
||||||
|
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| Argument | Substitute |
|
||||||
|
+=================+================================+
|
||||||
|
| {#:Member} | A member of your server. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| {#:TextChannel} | A text channel in your server. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| {#:Role} | A role in your server. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| {#:int} | A whole number. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| {#:float} | A decimal number. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
| {#:bool} | True or False. |
|
||||||
|
+-----------------+--------------------------------+
|
||||||
|
|
||||||
|
You can specify more than the above with colon notation, but those are the most common.
|
||||||
|
|
||||||
|
As with context parameters, you can use dot notation to further refine the response. For example, {0.mention:Member} will mention the Member specified.
|
||||||
|
|
||||||
|
----------------
|
||||||
|
Example commands
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Showing your own avatar
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]customcom add simple avatar {author.avatar_url}
|
||||||
|
[p]avatar
|
||||||
|
https://cdn.discordapp.com/avatars/133801473317404673/be4c4a4fe47cb3e74c31a0504e7a295e.webp?size=1024
|
||||||
|
|
||||||
|
Repeating the user
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]customcom add simple say {0}
|
||||||
|
[p]say Pete and Repeat
|
||||||
|
Pete and Repeat
|
||||||
|
|
||||||
|
Greeting the specified member
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]customcom add simple greet Hello, {0.mention:Member}!
|
||||||
|
[p]greet Twentysix
|
||||||
|
Hello, @Twentysix!
|
||||||
|
|
||||||
|
Comparing two text channel's categories
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]customcom add simple comparecategory {0.category:TextChannel} | {1.category:TextChannel}
|
||||||
|
[p]comparecategory #support #general
|
||||||
|
Red | Community
|
||||||
97
docs/cog_permissions.rst
Normal file
97
docs/cog_permissions.rst
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.. 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 normally.
|
||||||
|
|
||||||
|
Rules can also be added to cogs, which will affect all commands from that cog. The cog name can be
|
||||||
|
found from the help menu.
|
||||||
|
|
||||||
|
-------------
|
||||||
|
Rule priority
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Rules set for subcommands will take precedence over rules set for the parent commands, which
|
||||||
|
lastly take precedence over rules set for the cog. So for example, if a user is denied the Core
|
||||||
|
cog, but allowed the ``[p]set token`` command, the user will not be able to use any command in the
|
||||||
|
Core cog except for ``[p]set token``.
|
||||||
|
|
||||||
|
In terms of scope, global rules will be checked first, then server rules.
|
||||||
|
|
||||||
|
For each of those, the first rule pertaining to one of the following models will be used:
|
||||||
|
|
||||||
|
1. User
|
||||||
|
2. Voice channel
|
||||||
|
3. Text channel
|
||||||
|
4. Channel category
|
||||||
|
5. Roles, highest to lowest
|
||||||
|
6. Server (can only be in global rules)
|
||||||
|
7. Default rules
|
||||||
|
|
||||||
|
In private messages, only global rules about a user will be checked.
|
||||||
|
|
||||||
|
-------------------------
|
||||||
|
Setting Rules From a File
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The permissions cog can also set, display or update rules with a YAML file with the
|
||||||
|
``[p]permissions yaml`` command. Models must be represented by ID. Rules must be ``true`` for
|
||||||
|
allow, or ``false`` for deny. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
COG:
|
||||||
|
Admin:
|
||||||
|
78631113035100160: true
|
||||||
|
96733288462286848: false
|
||||||
|
Audio:
|
||||||
|
133049272517001216: true
|
||||||
|
default: false
|
||||||
|
COMMAND:
|
||||||
|
cleanup bot:
|
||||||
|
78631113035100160: true
|
||||||
|
default: false
|
||||||
|
ping:
|
||||||
|
96733288462286848: false
|
||||||
|
default: true
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Example configurations
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Locking the ``[p]play`` command to approved server(s) as a bot owner:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions setglobaldefault play deny
|
||||||
|
[p]permissions addglobalrule allow play [server ID or name]
|
||||||
|
|
||||||
|
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions setserverdefault deny play
|
||||||
|
[p]permissions setserverdefault deny "playlist start"
|
||||||
|
[p]permissions addserverrule allow play [voice channel ID or name]
|
||||||
|
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
|
Allowing extra roles to use ``[p]cleanup``:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions addserverrule allow cleanup [role ID]
|
||||||
|
|
||||||
|
Preventing ``[p]cleanup`` from being used in channels where message history is important:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions addserverrule deny cleanup [channel ID or mention]
|
||||||
104
docs/conf.py
104
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,37 @@ 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'
|
"sphinx.ext.doctest",
|
||||||
|
"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 +81,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 +98,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 +108,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 +125,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 +138,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 +147,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 +162,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 +170,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 +179,35 @@ 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",
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for linkcheck builder ----------------------------------------
|
||||||
|
|
||||||
|
# A list of regular expressions that match URIs that should not be
|
||||||
|
# checked when doing a linkcheck build.
|
||||||
|
linkcheck_ignore = [r"https://java.com*"]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# -- Options for extensions -----------------------------------------------
|
||||||
intersphinx_mapping = {'python': ('https://docs.python.org/3.5', None),
|
|
||||||
'dpy': ('https://discordpy.readthedocs.io/en/rewrite/', None),
|
# Intersphinx
|
||||||
'motor': ('https://motor.readthedocs.io/en/stable/', None)}
|
intersphinx_mapping = {
|
||||||
|
"python": ("https://docs.python.org/3.6", None),
|
||||||
|
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
||||||
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Doctest
|
||||||
|
# If this string is non-empty, all blocks with ``>>>`` in them will be
|
||||||
|
# tested, not just the ones explicitly marked with ``.. doctest::``
|
||||||
|
doctest_test_doctest_blocks = ""
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ RedBase
|
|||||||
|
|
||||||
.. autoclass:: RedBase
|
.. autoclass:: RedBase
|
||||||
:members:
|
:members:
|
||||||
|
:exclude-members: get_context
|
||||||
|
|
||||||
|
.. automethod:: register_rpc_handler
|
||||||
|
.. automethod:: unregister_rpc_handler
|
||||||
|
|
||||||
Red
|
Red
|
||||||
^^^
|
^^^
|
||||||
|
|||||||
11
docs/framework_checks.rst
Normal file
11
docs/framework_checks.rst
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.. _checks:
|
||||||
|
|
||||||
|
========================
|
||||||
|
Command Check Decorators
|
||||||
|
========================
|
||||||
|
|
||||||
|
The following are all decorators for commands, which add restrictions to where and when they can be
|
||||||
|
run.
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.checks
|
||||||
|
:members:
|
||||||
26
docs/framework_commands.rst
Normal file
26
docs/framework_commands.rst
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.. red commands module documentation
|
||||||
|
|
||||||
|
================
|
||||||
|
Commands Package
|
||||||
|
================
|
||||||
|
|
||||||
|
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||||
|
all of the attributes from discord.py's are also in ours.
|
||||||
|
Some of these attributes, however, have been slightly modified, while others have been added to
|
||||||
|
extend functionlities used throughout the bot, as outlined below.
|
||||||
|
|
||||||
|
.. autofunction:: redbot.core.commands.command
|
||||||
|
|
||||||
|
.. autofunction:: redbot.core.commands.group
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.Command
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.Group
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: redbot.core.commands.Context
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.commands.requires
|
||||||
|
:members: PrivilegeLevel, PermState, Requires
|
||||||
@@ -29,7 +29,7 @@ Basic Usage
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def return_some_data(self, ctx):
|
async def return_some_data(self, ctx):
|
||||||
await ctx.send(await config.foo())
|
await ctx.send(await self.config.foo())
|
||||||
|
|
||||||
********
|
********
|
||||||
Tutorial
|
Tutorial
|
||||||
@@ -187,6 +187,7 @@ This usage guide will cover the following features:
|
|||||||
|
|
||||||
- :py:meth:`Group.get_raw`
|
- :py:meth:`Group.get_raw`
|
||||||
- :py:meth:`Group.set_raw`
|
- :py:meth:`Group.set_raw`
|
||||||
|
- :py:meth:`Group.clear_raw`
|
||||||
|
|
||||||
For this example let's suppose that we're creating a cog that allows users to buy and own multiple pets using
|
For this example let's suppose that we're creating a cog that allows users to buy and own multiple pets using
|
||||||
the built-in Economy credits::
|
the built-in Economy credits::
|
||||||
@@ -290,6 +291,37 @@ We're responsible pet owners here, so we've also got to have a way to feed our p
|
|||||||
|
|
||||||
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
|
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
|
||||||
|
|
||||||
|
Of course, if we're less than responsible pet owners, there are consequences::
|
||||||
|
|
||||||
|
#continued
|
||||||
|
@commands.command()
|
||||||
|
async def adopt(self, ctx, pet_name: str, *, member: discord.Member):
|
||||||
|
try:
|
||||||
|
pet = await self.conf.user(member).pets.get_raw(pet_name)
|
||||||
|
except KeyError:
|
||||||
|
await ctx.send("That person doesn't own that pet!")
|
||||||
|
return
|
||||||
|
|
||||||
|
hunger = pet.get("hunger")
|
||||||
|
if hunger < 80:
|
||||||
|
await ctx.send("That pet is too well taken care of to be adopted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.conf.user(member).pets.clear_raw(pet_name)
|
||||||
|
|
||||||
|
# this is equivalent to doing the following
|
||||||
|
|
||||||
|
pets = await self.conf.user(member).pets()
|
||||||
|
del pets[pet_name]
|
||||||
|
await self.conf.user(member).pets.set(pets)
|
||||||
|
|
||||||
|
await self.conf.user(ctx.author).pets.set_raw(pet_name, value=pet)
|
||||||
|
await ctx.send(
|
||||||
|
"Your request to adopt this pet has been granted due to "
|
||||||
|
"how poorly it was taken care of."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
*************
|
*************
|
||||||
V2 Data Usage
|
V2 Data Usage
|
||||||
*************
|
*************
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
.. red invocation context documentation
|
|
||||||
|
|
||||||
==========================
|
|
||||||
Command Invocation Context
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: redbot.core.context
|
|
||||||
|
|
||||||
.. autoclass:: redbot.core.RedContext
|
|
||||||
:members:
|
|
||||||
@@ -6,21 +6,35 @@ 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
|
||||||
|
|
||||||
|
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||||
|
used for installing.
|
||||||
|
|
||||||
|
- ``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 visible in the cog list for a repo.
|
||||||
|
|
||||||
- ``hidden`` (bool) - Determines if a cog is available for install.
|
- ``disabled`` (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 +43,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.
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ Basic Usage
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
|
||||||
_ = CogI18n("ExampleCog", __file__)
|
_ = Translator("ExampleCog", __file__)
|
||||||
|
|
||||||
|
@cog_i18n(_)
|
||||||
class ExampleCog:
|
class ExampleCog:
|
||||||
"""description"""
|
"""description"""
|
||||||
|
|
||||||
@@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is),
|
|||||||
create a directory called "locales".
|
create a directory called "locales".
|
||||||
Then do one of the following:
|
Then do one of the following:
|
||||||
|
|
||||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -n -p locales`
|
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -D -n -p locales`
|
||||||
|
|
||||||
Mac: ?
|
Mac: ?
|
||||||
|
|
||||||
Linux: :code:`pygettext3 -n -p locales`
|
Linux: :code:`pygettext3 -D -n -p locales`
|
||||||
|
|
||||||
This will generate a messages.pot file with strings to be translated
|
This will generate a messages.pot file with strings to be translated, including
|
||||||
|
docstrings.
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
API Reference
|
API Reference
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
.. automodule:: redbot.core.i18n
|
.. automodule:: redbot.core.i18n
|
||||||
|
:members:
|
||||||
|
:special-members: __call__
|
||||||
|
|||||||
@@ -4,5 +4,60 @@
|
|||||||
RPC
|
RPC
|
||||||
===
|
===
|
||||||
|
|
||||||
.. automodule:: redbot.core.rpc
|
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
||||||
:members:
|
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 enable the internal RPC server you must start the bot with the ``--rpc`` flag.
|
||||||
|
|
||||||
|
********
|
||||||
|
Examples
|
||||||
|
********
|
||||||
|
|
||||||
|
.. code-block:: Python
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
c = Cog()
|
||||||
|
bot.add_cog(c)
|
||||||
|
bot.register_rpc_handler(c.rpc_method)
|
||||||
|
|
||||||
|
*******************************
|
||||||
|
Interacting with the RPC Server
|
||||||
|
*******************************
|
||||||
|
|
||||||
|
The RPC server opens a websocket bound to port ``6133`` on ``127.0.0.1``.
|
||||||
|
This is not configurable for security reasons as broad access to this server gives anyone complete control over your bot.
|
||||||
|
To access the server you must find a library that implements websocket based JSONRPC in the language of your choice.
|
||||||
|
|
||||||
|
There are a few built-in RPC methods to note:
|
||||||
|
|
||||||
|
* ``GET_METHODS`` - Returns a list of available RPC methods.
|
||||||
|
* ``GET_METHOD_INFO`` - Will return the docstring for an available RPC method. Useful for finding information about the method's parameters and return values.
|
||||||
|
* ``GET_TOPIC`` - Returns a list of available RPC message topics.
|
||||||
|
* ``GET_SUBSCRIPTIONS`` - Returns a list of RPC subscriptions.
|
||||||
|
* ``SUBSCRIBE`` - Subscribes to an available RPC message topic.
|
||||||
|
* ``UNSUBSCRIBE`` - Unsubscribes from an RPC message topic.
|
||||||
|
|
||||||
|
All RPC methods accept a list of parameters.
|
||||||
|
The built-in methods above expect their parameters to be in list format.
|
||||||
|
|
||||||
|
All cog-based methods expect their parameter list to take one argument, a JSON object, in the following format::
|
||||||
|
|
||||||
|
params = [
|
||||||
|
{
|
||||||
|
"args": [], # A list of positional arguments
|
||||||
|
"kwargs": {}, # A dictionary of keyword arguments
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# As an example, here's a call to "get_method_info"
|
||||||
|
rpc_call("GET_METHOD_INFO", ["get_methods",])
|
||||||
|
|
||||||
|
# And here's a call to "core__load"
|
||||||
|
rpc_call("CORE__LOAD", {"args": [["general", "economy", "downloader"],], "kwargs": {}})
|
||||||
|
|
||||||
|
*************
|
||||||
|
API Reference
|
||||||
|
*************
|
||||||
|
|
||||||
|
Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
Utility Functions
|
Utility Functions
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
General Utility
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils
|
||||||
|
:members: deduplicate_iterables, bounded_gather, bounded_gather_iter
|
||||||
|
|
||||||
Chat Formatting
|
Chat Formatting
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -16,6 +22,18 @@ Embed Helpers
|
|||||||
.. automodule:: redbot.core.utils.embed
|
.. automodule:: redbot.core.utils.embed
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Reaction Menus
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.menus
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Event Predicates
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.predicates
|
||||||
|
:members:
|
||||||
|
|
||||||
Mod Helpers
|
Mod Helpers
|
||||||
===========
|
===========
|
||||||
|
|
||||||
@@ -33,3 +51,9 @@ Tunnel
|
|||||||
|
|
||||||
.. automodule:: redbot.core.utils.tunnel
|
.. automodule:: redbot.core.utils.tunnel
|
||||||
:members: Tunnel
|
:members: Tunnel
|
||||||
|
|
||||||
|
Common Filters
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.common_filters
|
||||||
|
:members:
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ you in the process.
|
|||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
To start off, be sure that you have installed Python 3.6.2 or higher (3.6.6 or higher on Windows).
|
||||||
are on Windows, stick with 3.5). Open a terminal or command prompt and type
|
Open a terminal or command prompt and type :code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||||
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
|
||||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||||
This will install the latest version of V3.
|
This will install the latest version of V3.
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@ In that file, place the following code:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
class Mycog:
|
class Mycog:
|
||||||
"""My custom cog"""
|
"""My custom cog"""
|
||||||
@@ -90,6 +89,6 @@ have successfully created a cog!
|
|||||||
Additional resources
|
Additional resources
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Be sure to check out the `migration guide </guide_migration>`_ for some resources
|
Be sure to check out the :doc:`/guide_migration` for some resources
|
||||||
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
||||||
those who developed cogs for V2.
|
those who developed cogs for V2.
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
:caption: Installation Guides:
|
:caption: Installation Guides:
|
||||||
|
|
||||||
install_windows
|
install_windows
|
||||||
install_mac
|
install_linux_mac
|
||||||
install_ubuntu
|
venv_guide
|
||||||
install_debian
|
|
||||||
install_centos
|
|
||||||
install_arch
|
|
||||||
install_raspbian
|
|
||||||
cog_dataconverter
|
cog_dataconverter
|
||||||
autostart_systemd
|
autostart_systemd
|
||||||
|
|
||||||
@@ -24,7 +20,9 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Cog Reference:
|
:caption: Cog Reference:
|
||||||
|
|
||||||
|
cog_customcom
|
||||||
cog_downloader
|
cog_downloader
|
||||||
|
cog_permissions
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
@@ -35,9 +33,10 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
guide_data_conversion
|
guide_data_conversion
|
||||||
framework_bank
|
framework_bank
|
||||||
framework_bot
|
framework_bot
|
||||||
|
framework_checks
|
||||||
framework_cogmanager
|
framework_cogmanager
|
||||||
|
framework_commands
|
||||||
framework_config
|
framework_config
|
||||||
framework_context
|
|
||||||
framework_datamanager
|
framework_datamanager
|
||||||
framework_downloader
|
framework_downloader
|
||||||
framework_events
|
framework_events
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
.. arch install guide
|
|
||||||
|
|
||||||
==============================
|
|
||||||
Installing Red on Arch Linux
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, make a new one.
|
|
||||||
|
|
||||||
:code:`https://wiki.archlinux.org/index.php/Users_and_groups`
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
Installing the pre-requirements
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
sudo pacman -Sy python-pip git base-devel jre8-openjdk
|
|
||||||
|
|
||||||
------------------
|
|
||||||
Installing the bot
|
|
||||||
------------------
|
|
||||||
|
|
||||||
To install without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
|
|
||||||
|
|
||||||
To install with audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 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 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.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
.. centos install guide
|
|
||||||
|
|
||||||
==========================
|
|
||||||
Installing Red on CentOS 7
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/Step_by_Step_Guide/s1-starting-create-account.html>`_.
|
|
||||||
|
|
||||||
---------------------------
|
|
||||||
Installing pre-requirements
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
yum -y groupinstall development
|
|
||||||
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
|
|
||||||
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
|
|
||||||
|
|
||||||
--------------
|
|
||||||
Installing Red
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
|
|
||||||
|
|
||||||
With audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 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 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
----------------------
|
|
||||||
Setting up an 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.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
.. debian install guide
|
|
||||||
|
|
||||||
================================
|
|
||||||
Installing Red on Debian Stretch
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://manpages.debian.org/stretch/adduser/adduser.8.en.html>`_.
|
|
||||||
|
|
||||||
---------------------------
|
|
||||||
Installing pre-requirements
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
echo "deb http://httpredir.debian.org/debian stretch-backports main contrib non-free" >> /etc/apt/sources.list
|
|
||||||
apt-get update
|
|
||||||
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
|
||||||
|
|
||||||
------------------
|
|
||||||
Installing the bot
|
|
||||||
------------------
|
|
||||||
|
|
||||||
To install without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot`
|
|
||||||
|
|
||||||
To install with audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
|
|
||||||
|
|
||||||
To install the development version (with audio):
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
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.
|
|
||||||
203
docs/install_linux_mac.rst
Normal file
203
docs/install_linux_mac.rst
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
.. _linux-mac-install-guide:
|
||||||
|
|
||||||
|
==============================
|
||||||
|
Installing Red on Linux or Mac
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
For safety reasons, DO NOT install Red with a root user. If you are unsure how to create
|
||||||
|
a new user, see the man page for the ``useradd`` command.
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
Installing the pre-requirements
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Please install the pre-requirements using the commands listed for your operating system.
|
||||||
|
|
||||||
|
The pre-requirements are:
|
||||||
|
- Python 3.6.2 or greater
|
||||||
|
- pip 9.0 or greater
|
||||||
|
- git
|
||||||
|
- Java Runtime Environment 8 or later (for audio support)
|
||||||
|
|
||||||
|
~~~~~~~~~~
|
||||||
|
Arch Linux
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
||||||
|
|
||||||
|
~~~~~~~~
|
||||||
|
CentOS 7
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
yum -y groupinstall development
|
||||||
|
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
||||||
|
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Debian and Raspbian Stretch
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Audio will not work on Raspberry Pi's **below** 2B. This is a CPU problem and
|
||||||
|
*cannot* be fixed.
|
||||||
|
|
||||||
|
We recommend installing pyenv as a method of installing non-native versions of python on
|
||||||
|
Debian/Raspbian Stretch. This guide will tell you how. First, run the following commands:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
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.7.0 -v
|
||||||
|
|
||||||
|
This may take a long time to complete.
|
||||||
|
|
||||||
|
After that is finished, run:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pyenv global 3.7.0
|
||||||
|
|
||||||
|
Pyenv is now installed and your system should be configured to run Python 3.7.
|
||||||
|
|
||||||
|
~~~
|
||||||
|
Mac
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Install Brew: in Finder or Spotlight, search for and open *Terminal*. In the terminal, paste the
|
||||||
|
following, then press Enter:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
|
|
||||||
|
After the installation, install the required packages by pasting the commands and pressing enter,
|
||||||
|
one-by-one:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
brew install python3 --with-brewed-openssl
|
||||||
|
brew install git
|
||||||
|
brew tap caskroom/versions
|
||||||
|
brew cask install java8
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Ubuntu 18.04 Bionic Beaver
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Ubuntu 16.04 Xenial Xerus
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We recommend adding the ``deadsnakes`` apt repository to install Python 3.6.2 or greater:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
sudo apt install software-properties-common
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
Now, install python, pip, git and java with the following commands:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
Creating a Virtual Environment
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
We **strongly** recommend installing Red into a virtual environment. See the section
|
||||||
|
`installing-in-virtual-environment`.
|
||||||
|
|
||||||
|
.. _installing-red-linux-mac:
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Installing Red
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Choose one of the following commands to install Red.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
|
``pip3`` commands.
|
||||||
|
|
||||||
|
To install without audio support:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot
|
||||||
|
|
||||||
|
Or, to install with audio support:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice]
|
||||||
|
|
||||||
|
Or, install with audio and MongoDB support:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice,mongo]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
||||||
|
following link:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
Setting Up and Running Red
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
After installation, set up your instance with the following command:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot-setup
|
||||||
|
|
||||||
|
This will set the location where data will be stored, as well as your
|
||||||
|
storage backend and the name of the instance (which will be used for
|
||||||
|
running the bot).
|
||||||
|
|
||||||
|
Once done setting up the instance, run the following command to run Red:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot <your instance name>
|
||||||
|
|
||||||
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
|
|
||||||
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
|
from discord, and enable auto-restart. You may also update the bot from the
|
||||||
|
launcher menu. Use the following command to run the launcher:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot-launcher
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
.. mac install guide
|
|
||||||
|
|
||||||
=====================
|
|
||||||
Installing Red on Mac
|
|
||||||
=====================
|
|
||||||
|
|
||||||
---------------------------
|
|
||||||
Installing pre-requirements
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
* Install Brew
|
|
||||||
* In Finder or Spotlight, search for and open terminal. In the window that will open, paste this:
|
|
||||||
:code:`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
|
|
||||||
and press enter.
|
|
||||||
* After the installation, install the required packages by pasting the commands and pressing enter, one-by-one:
|
|
||||||
* :code:`brew install python3 --with-brewed-openssl`
|
|
||||||
* :code:`brew install git`
|
|
||||||
* :code:`brew tap caskroom/versions`
|
|
||||||
* :code:`brew cask install java8`
|
|
||||||
|
|
||||||
--------------
|
|
||||||
Installing Red
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot`
|
|
||||||
|
|
||||||
With audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
|
|
||||||
|
|
||||||
To install the development version (with audio):
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
|
|
||||||
|
|
||||||
----------------------
|
|
||||||
Setting up an instance
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
To set up an instance, run :code:`redbot-setup` and follow the steps there, providing the requested information
|
|
||||||
or accepting the defaults. Keep in mind that the instance name will be the one you use when running the bot, so
|
|
||||||
make it something you can remember
|
|
||||||
|
|
||||||
-----------
|
|
||||||
Running Red
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Run :code:`redbot <your instance name>` and go through the initial setup (it will ask for the token and a prefix).
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
.. raspbian install guide
|
|
||||||
|
|
||||||
==================================
|
|
||||||
Installing Red on Raspbian Stretch
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://www.raspberrypi.org/documentation/linux/usage/users.md>`_.
|
|
||||||
|
|
||||||
---------------------------
|
|
||||||
Installing pre-requirements
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
|
||||||
|
|
||||||
|
|
||||||
--------------
|
|
||||||
Installing Red
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
|
|
||||||
|
|
||||||
With audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 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 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
----------------------
|
|
||||||
Setting up an 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.
|
|
||||||
|
|
||||||
.. warning:: Audio will not work on Raspberry Pi's **below** 2B. This is a CPU problem and *cannot* be fixed.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
.. ubuntu 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 python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
|
||||||
|
|
||||||
|
|
||||||
------------------
|
|
||||||
Installing the bot
|
|
||||||
------------------
|
|
||||||
|
|
||||||
To install without audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
|
|
||||||
|
|
||||||
To install with audio:
|
|
||||||
|
|
||||||
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
|
|
||||||
|
|
||||||
To install the development version (without audio):
|
|
||||||
|
|
||||||
:code:`pip3 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 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.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.. windows installation docs
|
.. _windows-install-guide:
|
||||||
|
|
||||||
=========================
|
=========================
|
||||||
Installing Red on Windows
|
Installing Red on Windows
|
||||||
@@ -8,11 +8,7 @@ Installing Red on Windows
|
|||||||
Needed Software
|
Needed Software
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5
|
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6.6 or greater on Windows
|
||||||
|
|
||||||
.. attention:: Please note that 3.6 has issues on some versions of Windows.
|
|
||||||
If you try using Red with 3.6 and experience issues, uninstall
|
|
||||||
Python 3.6 and install the latest version of Python 3.5
|
|
||||||
|
|
||||||
.. 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
|
||||||
@@ -25,23 +21,74 @@ Needed Software
|
|||||||
|
|
||||||
.. attention:: Please choose the "Windows Online" installer
|
.. attention:: Please choose the "Windows Online" installer
|
||||||
|
|
||||||
|
.. _installing-red-windows:
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
Installing Red
|
Installing Red
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
1. Open a command prompt (open Start, search for "command prompt", then click it)
|
1. Open a command prompt (open Start, search for "command prompt", then click it)
|
||||||
2. Run the appropriate command, depending on if you want audio or not
|
2. Create and activate a virtual environment (strongly recommended), see the section `using-venv`
|
||||||
|
3. Run **one** of the following commands, depending on what extras you want installed
|
||||||
|
|
||||||
* No audio: :code:`python -m pip install -U --process-dependency-links Red-DiscordBot`
|
.. note::
|
||||||
* Audio: :code:`python -m pip install -U --process-dependency-links Red-DiscordBot[voice]`
|
|
||||||
* Development version (without audio): :code:`python -m pip install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
|
|
||||||
* Development version (with audio): :code:`python -m pip install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
|
|
||||||
|
|
||||||
3. Once that has completed, run :code:`redbot-setup` to set up your instance
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
|
``pip`` commands.
|
||||||
|
|
||||||
* This will set the location where data will be stored, as well as your
|
* No audio:
|
||||||
storage backend and the name of the instance (which will be used for
|
|
||||||
running the bot)
|
|
||||||
|
|
||||||
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red.
|
.. code-block:: none
|
||||||
It will walk through the initial setup, asking for your token and a prefix
|
|
||||||
|
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot
|
||||||
|
|
||||||
|
* With audio:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice]
|
||||||
|
|
||||||
|
* With audio and MongoDB support:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice,mongo]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
|
||||||
|
following link:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
Setting Up and Running Red
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
After installation, set up your instance with the following command:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot-setup
|
||||||
|
|
||||||
|
This will set the location where data will be stored, as well as your
|
||||||
|
storage backend and the name of the instance (which will be used for
|
||||||
|
running the bot).
|
||||||
|
|
||||||
|
Once done setting up the instance, run the following command to run Red:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot <your instance name>
|
||||||
|
|
||||||
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
|
|
||||||
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
|
from discord, and enable auto-restart. You may also update the bot from the
|
||||||
|
launcher menu. Use the following command to run the launcher:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
redbot-launcher
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
sphinx==1.6.5
|
|
||||||
sphinxcontrib-asyncio
|
|
||||||
sphinx_rtd_theme
|
|
||||||
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]
|
|
||||||
132
docs/venv_guide.rst
Normal file
132
docs/venv_guide.rst
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
.. _installing-in-virtual-environment:
|
||||||
|
|
||||||
|
=======================================
|
||||||
|
Installing Red in a Virtual Environment
|
||||||
|
=======================================
|
||||||
|
Virtual environments allow you to isolate red's library dependencies, cog dependencies and python
|
||||||
|
binaries from the rest of your system. It is strongly recommended you use this if you use python
|
||||||
|
for more than just Red.
|
||||||
|
|
||||||
|
.. _using-venv:
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Using ``venv``
|
||||||
|
--------------
|
||||||
|
This is the quickest way to get your virtual environment up and running, as `venv` is shipped with
|
||||||
|
python.
|
||||||
|
|
||||||
|
First, choose a directory where you would like to create your virtual environment. It's a good idea
|
||||||
|
to keep it in a location which is easy to type out the path to. From now, we'll call it
|
||||||
|
``path/to/venv/`` (or ``path\to\venv\`` on Windows).
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
``venv`` on Linux or Mac
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Create your virtual environment with the following command::
|
||||||
|
|
||||||
|
python3 -m venv path/to/venv/
|
||||||
|
|
||||||
|
And activate it with the following command::
|
||||||
|
|
||||||
|
source path/to/venv/bin/activate
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
You must activate the virtual environment with the above command every time you open a new
|
||||||
|
shell to run, install or update Red.
|
||||||
|
|
||||||
|
Continue reading `below <after-activating-virtual-environment>`.
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
``venv`` on Windows
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
Create your virtual environment with the following command::
|
||||||
|
|
||||||
|
python -m venv path\to\venv\
|
||||||
|
|
||||||
|
And activate it with the following command::
|
||||||
|
|
||||||
|
path\to\venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
You must activate the virtual environment with the above command every time you open a new
|
||||||
|
Command Prompt to run, install or update Red.
|
||||||
|
|
||||||
|
Continue reading `below <after-activating-virtual-environment>`.
|
||||||
|
|
||||||
|
.. _using-pyenv-virtualenv:
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
Using ``pyenv virtualenv``
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This is for non-Windows users only.
|
||||||
|
|
||||||
|
Using ``pyenv virtualenv`` saves you the headache of remembering where you installed your virtual
|
||||||
|
environments. If you haven't already, install pyenv with `pyenv-installer`_.
|
||||||
|
|
||||||
|
First, ensure your pyenv interpreter is set to python 3.6.2 or greater with the following command::
|
||||||
|
|
||||||
|
pyenv version
|
||||||
|
|
||||||
|
Now, create a virtual environment with the following command::
|
||||||
|
|
||||||
|
pyenv virtualenv <name>
|
||||||
|
|
||||||
|
Replace ``<name>`` with whatever you like. If you forget what you named it, use the command ``pyenv
|
||||||
|
versions``.
|
||||||
|
|
||||||
|
Now activate your virtualenv with the following command::
|
||||||
|
|
||||||
|
pyenv shell <name>
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
You must activate the virtual environment with the above command every time you open a new
|
||||||
|
shell to run, install or update Red.
|
||||||
|
|
||||||
|
Continue reading `below <after-activating-virtual-environment>`.
|
||||||
|
|
||||||
|
.. _pyenv-installer: https://github.com/pyenv/pyenv-installer/blob/master/README.rst
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
.. _after-activating-virtual-environment:
|
||||||
|
|
||||||
|
Once activated, your ``PATH`` environment variable will be modified to use the virtual
|
||||||
|
environment's python executables, as well as other executables like ``pip``.
|
||||||
|
|
||||||
|
From here, install Red using the commands listed on your installation guide (`Windows
|
||||||
|
<installing-red-windows>` or `Non-Windows <installing-red-linux-mac>`).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The alternative to activating the virtual environment each time you open a new shell is to
|
||||||
|
provide the full path to the executable. This will automatically use the virtual environment's
|
||||||
|
python interpreter and installed libraries.
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Virtual Environments with Multiple Instances
|
||||||
|
--------------------------------------------
|
||||||
|
If you are running multiple instances of Red on the same machine, you have the option of either
|
||||||
|
using the same virtual environment for all of them, or creating separate ones.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This only applies for multiple instances of V3. If you are running a V2 instance as well,
|
||||||
|
You **must** use separate virtual environments.
|
||||||
|
|
||||||
|
The advantages of using a *single* virtual environment for all of your V3 instances are:
|
||||||
|
|
||||||
|
- When updating Red, you will only need to update it once for all instances (however you will still need to restart all instances for the changes to take effect)
|
||||||
|
- It will save space on your hard drive
|
||||||
|
|
||||||
|
On the other hand, you may wish to update each of your instances individually.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
Windows users with multiple instances should create *separate* virtual environments, as
|
||||||
|
updating multiple running instances at once is likely to cause errors.
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
interpreter = sys.executable
|
|
||||||
print(interpreter)
|
|
||||||
root_dir = os.getcwd()
|
|
||||||
cogs = [i for i in os.listdir("redbot/cogs") if os.path.isdir(os.path.join("redbot/cogs", i))]
|
|
||||||
for d in cogs:
|
|
||||||
if "locales" in os.listdir(os.path.join("redbot/cogs", d)):
|
|
||||||
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
|
||||||
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
|
||||||
print("Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(d))
|
|
||||||
exit(1)
|
|
||||||
else:
|
|
||||||
print("Running 'regen_messages.py' for {}".format(d))
|
|
||||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
|
||||||
if retval.returncode != 0:
|
|
||||||
exit(1)
|
|
||||||
os.chdir(root_dir)
|
|
||||||
os.chdir("redbot/core/locales")
|
|
||||||
print("Running 'regen_messages.py' for core")
|
|
||||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
|
||||||
if retval.returncode != 0:
|
|
||||||
exit(1)
|
|
||||||
os.chdir(root_dir)
|
|
||||||
subprocess.run(["crowdin", "upload"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
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 -N !PYFILES!
|
||||||
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
|
:stylecheck
|
||||||
|
black -l 99 -N --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,34 @@
|
|||||||
import sys
|
import sys
|
||||||
import typing
|
import warnings
|
||||||
import discord
|
import discord
|
||||||
|
import colorama
|
||||||
|
|
||||||
# Let's do all the dumb version checking in one place.
|
# Let's do all the dumb version checking in one place.
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Due to issues with ProactorEventLoop prior to 3.6.6 (bpo-26819)
|
||||||
|
MIN_PYTHON_VERSION = (3, 6, 6)
|
||||||
|
else:
|
||||||
|
MIN_PYTHON_VERSION = (3, 6, 2)
|
||||||
|
|
||||||
|
if sys.version_info < MIN_PYTHON_VERSION:
|
||||||
|
print(
|
||||||
|
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
||||||
|
f"{sys.version}! Please update Python."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
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(
|
||||||
"In order to use Red v3 you MUST be running d.py version"
|
"You are not running the rewritten version of discord.py.\n\n"
|
||||||
" >= 1.0.0.")
|
"In order to use Red V3 you MUST be running d.py version "
|
||||||
|
"1.0.0 or greater."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
|
warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
|
||||||
|
# Prevent discord PyNaCl missing warning
|
||||||
|
discord.voice_client.VoiceClient.warn_nacl = False
|
||||||
|
|||||||
@@ -6,19 +6,31 @@ import sys
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core.bot import Red, ExitCodes
|
from redbot.core.bot import Red, ExitCodes
|
||||||
from redbot.core.cog_manager import CogManagerUI
|
from redbot.core.cog_manager import CogManagerUI
|
||||||
from redbot.core.data_manager import load_basic_configuration, config_file
|
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
||||||
from redbot.core.json_io import JsonIO
|
from redbot.core.json_io import JsonIO
|
||||||
from redbot.core.global_checks import init_global_checks
|
from redbot.core.global_checks import init_global_checks
|
||||||
from redbot.core.events import init_events
|
from redbot.core.events import init_events
|
||||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||||
from redbot.core.core_commands import Core
|
from redbot.core.core_commands import Core
|
||||||
from redbot.core.dev_commands import Dev
|
from redbot.core.dev_commands import Dev
|
||||||
from redbot.core import rpc, __version__
|
from redbot.core import __version__
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
|
if sys.implementation.name == "cpython":
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Red - Discord Bot v3
|
# Red - Discord Bot v3
|
||||||
@@ -40,24 +52,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.INFO)
|
||||||
|
|
||||||
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 +89,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()
|
||||||
@@ -103,12 +118,20 @@ def main():
|
|||||||
elif cli_flags.version:
|
elif cli_flags.version:
|
||||||
print(description)
|
print(description)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif not cli_flags.instance_name:
|
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||||
print("Error: No instance name was provided!")
|
print("Error: No instance name was provided!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if cli_flags.no_instance:
|
||||||
|
print(
|
||||||
|
"\033[1m"
|
||||||
|
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
||||||
|
"\033[0m"
|
||||||
|
)
|
||||||
|
cli_flags.instance_name = "temporary_red"
|
||||||
|
create_temp_config()
|
||||||
load_basic_configuration(cli_flags.instance_name)
|
load_basic_configuration(cli_flags.instance_name)
|
||||||
log, sentry_log = init_loggers(cli_flags)
|
log, sentry_log = init_loggers(cli_flags)
|
||||||
red = Red(cli_flags, description=description, pm_help=None)
|
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
red.add_cog(Core(red))
|
red.add_cog(Core(red))
|
||||||
@@ -118,30 +141,30 @@ 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']
|
if cli_flags.token:
|
||||||
if token is None or not prefix:
|
token = cli_flags.token
|
||||||
|
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||||
|
if not (token and 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
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
loop.run_until_complete(red.start(token, bot=True))
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
cleanup_tasks = False # No login happened, no need for this
|
log.critical("This token doesn't seem to be valid.")
|
||||||
log.critical("This token doesn't seem to be valid. If it belongs to "
|
db_token = loop.run_until_complete(red.db.token())
|
||||||
"a user account, remember that the --not-bot flag "
|
|
||||||
"must be used. For self-bot functionalities instead, "
|
|
||||||
"--self-bot")
|
|
||||||
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)")
|
||||||
if confirm("> "):
|
if confirm("> "):
|
||||||
@@ -156,15 +179,16 @@ 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()
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
if cleanup_tasks:
|
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
gathered.cancel()
|
||||||
gathered = asyncio.gather(
|
try:
|
||||||
*pending, loop=red.loop, return_exceptions=True)
|
red.rpc.server.close()
|
||||||
gathered.cancel()
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
sys.exit(red._shutdown_mode.value)
|
sys.exit(red._shutdown_mode.value)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,57 +1,60 @@
|
|||||||
|
import logging
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core import Config, checks
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
from redbot.core import Config, checks, commands
|
||||||
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
from .announcer import Announcer
|
from .announcer import Announcer
|
||||||
from .converters import MemberDefaultAuthor, SelfRole
|
from .converters import MemberDefaultAuthor, SelfRole
|
||||||
|
|
||||||
log = logging.getLogger("red.admin")
|
log = logging.getLogger("red.admin")
|
||||||
|
|
||||||
GENERIC_FORBIDDEN = (
|
T_ = Translator("Admin", __file__)
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
|
GENERIC_FORBIDDEN = _(
|
||||||
"I attempted to do something that Discord denied me permissions for."
|
"I attempted to do something that Discord denied me permissions for."
|
||||||
" Your command failed to successfully complete."
|
" Your command failed to successfully complete."
|
||||||
)
|
)
|
||||||
|
|
||||||
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."
|
||||||
)
|
)
|
||||||
|
|
||||||
RUNNING_ANNOUNCEMENT = (
|
RUNNING_ANNOUNCEMENT = _(
|
||||||
"I am already announcing something. If you would like to make a"
|
"I am already announcing something. If you would like to make a"
|
||||||
" different announcement please use `{prefix}announce cancel`"
|
" different announcement please use `{prefix}announce cancel`"
|
||||||
" first."
|
" first."
|
||||||
)
|
)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
class Admin:
|
@cog_i18n(_)
|
||||||
|
class Admin(commands.Cog):
|
||||||
|
"""A collection of server administration utilities."""
|
||||||
|
|
||||||
def __init__(self, config=Config):
|
def __init__(self, config=Config):
|
||||||
self.conf = config.get_conf(self, 8237492837454039,
|
super().__init__()
|
||||||
force_registration=True)
|
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||||
|
|
||||||
self.conf.register_global(
|
self.conf.register_global(serverlocked=False)
|
||||||
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 +66,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 +80,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 +90,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,197 +99,193 @@ 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, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||||
member=member)
|
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(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 {member.display_name}").format(
|
||||||
role=role, member=member
|
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, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||||
member=member)
|
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(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 {member.display_name}").format(
|
||||||
role=role, member=member
|
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
|
"""Add a role to a user.
|
||||||
author of the command.
|
|
||||||
|
If user is left blank it defaults to the 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:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE, member=ctx.author)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE), member=ctx.author, role=rolename)
|
||||||
|
|
||||||
@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
|
"""Remove a role from a user.
|
||||||
author of the command.
|
|
||||||
|
If user is left blank it defaults to the 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:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def editrole(self, ctx: commands.Context):
|
async def editrole(self, ctx: commands.Context):
|
||||||
"""Edits roles settings"""
|
"""Edit role settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
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
|
):
|
||||||
|
"""Edit a role's colour.
|
||||||
|
|
||||||
Use double quotes if the role contains spaces.
|
Use double quotes if the role contains spaces.
|
||||||
Colour must be in hexadecimal format.
|
Colour must be in hexadecimal format.
|
||||||
\"http://www.w3schools.com/colors/colors_picker.asp\"
|
[Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)
|
||||||
Examples:
|
|
||||||
!editrole colour \"The Transistor\" #ff0000
|
|
||||||
!editrole colour Test #ff9900"""
|
|
||||||
author = ctx.author
|
|
||||||
reason = "{}({}) changed the colour of role '{}'".format(
|
|
||||||
author.name, author.id, role.name)
|
|
||||||
|
|
||||||
if not self.pass_user_heirarchy_check(ctx, role):
|
Examples:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
`[p]editrole colour "The Transistor" #ff0000`
|
||||||
|
`[p]editrole colour Test #ff9900`
|
||||||
|
"""
|
||||||
|
author = ctx.author
|
||||||
|
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
|
||||||
|
|
||||||
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await role.edit(reason=reason, color=value)
|
await role.edit(reason=reason, color=value)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@editrole.command(name="name")
|
@editrole.command(name="name")
|
||||||
@checks.admin_or_permissions(administrator=True)
|
@checks.admin_or_permissions(administrator=True)
|
||||||
async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str):
|
async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str):
|
||||||
"""Edits a role's name
|
"""Edit a role's name.
|
||||||
|
|
||||||
Use double quotes if the role or the name contain spaces.
|
Use double quotes if the role or the name contain spaces.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
!editrole name \"The Transistor\" Test"""
|
`[p]editrole name \"The Transistor\" Test`
|
||||||
|
"""
|
||||||
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, T_(USER_HIERARCHY_ISSUE))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await role.edit(reason=reason, name=name)
|
await role.edit(reason=reason, name=name)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def announce(self, ctx: commands.Context, *, message: str):
|
async def announce(self, ctx: commands.Context, *, message: str):
|
||||||
"""
|
"""Announce a message to all servers the bot is in."""
|
||||||
Announces a message to all servers the bot is in.
|
|
||||||
"""
|
|
||||||
if not self.is_announcing():
|
if not self.is_announcing():
|
||||||
announcer = Announcer(ctx, message, config=self.conf)
|
announcer = Announcer(ctx, message, config=self.conf)
|
||||||
announcer.start()
|
announcer.start()
|
||||||
|
|
||||||
self.__current_announcer = announcer
|
self.__current_announcer = announcer
|
||||||
|
|
||||||
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, T_(RUNNING_ANNOUNCEMENT), prefix=prefix)
|
||||||
prefix=prefix)
|
|
||||||
|
|
||||||
@announce.command(name="cancel")
|
@announce.command(name="cancel")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def announce_cancel(self, ctx):
|
async def announce_cancel(self, ctx):
|
||||||
"""
|
"""Cancel a running announce."""
|
||||||
Cancels a running announce.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
self.__current_announcer.cancel()
|
self.__current_announcer.cancel()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await ctx.send("The current announcement has been cancelled.")
|
await ctx.send(_("The current announcement has been cancelled."))
|
||||||
|
|
||||||
@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):
|
||||||
"""
|
"""Change the channel to which the bot makes announcements."""
|
||||||
Changes the channel on which the bot makes announcements.
|
|
||||||
"""
|
|
||||||
if channel is None:
|
if channel is None:
|
||||||
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(
|
||||||
channel.mention
|
_("The announcement channel has been set to {channel.mention}").format(channel=channel)
|
||||||
))
|
)
|
||||||
|
|
||||||
@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):
|
||||||
"""
|
"""Toggle announcements being enabled this server."""
|
||||||
Toggles whether the announcements will ignore the given server.
|
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||||
Defaults to the current server if none is provided.
|
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||||
"""
|
|
||||||
if guild is None:
|
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
ignored = await self.conf.guild(guild).announce_ignore()
|
if ignored: # Keeping original logic....
|
||||||
await self.conf.guild(guild).announce_ignore.set(not ignored)
|
await ctx.send(
|
||||||
|
_("The server {guild.name} will receive announcements.").format(guild=ctx.guild)
|
||||||
verb = "will" if ignored else "will not"
|
)
|
||||||
|
else:
|
||||||
await ctx.send("The server {} {} receive announcements.".format(
|
await ctx.send(
|
||||||
guild.name, verb
|
_("The server {guild.name} will not receive announcements.").format(
|
||||||
))
|
guild=ctx.guild
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||||
"""
|
"""
|
||||||
@@ -309,45 +305,51 @@ class Admin:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return valid_roles
|
return valid_roles
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""Add a role to yourself.
|
||||||
Add a role to yourself that server admins have configured as
|
|
||||||
user settable.
|
Server admins must have configured the role as user settable.
|
||||||
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, ctx.author, selfrole)
|
await self._addrole(ctx, ctx.author, selfrole)
|
||||||
|
|
||||||
@selfrole.command(name="remove")
|
@selfrole.command(name="remove")
|
||||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""Remove a selfrole from yourself.
|
||||||
Removes a selfrole from yourself.
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, ctx.author, selfrole)
|
await self._removerole(ctx, ctx.author, selfrole)
|
||||||
|
|
||||||
@selfrole.command(name="add")
|
@selfrole.command(name="add")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""
|
"""Add a role to the list of available selfroles.
|
||||||
Add a role to the list of available selfroles.
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
if role.id not in curr_selfroles:
|
if role.id not in curr_selfroles:
|
||||||
curr_selfroles.append(role.id)
|
curr_selfroles.append(role.id)
|
||||||
|
|
||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send(_("The selfroles list has been successfully modified."))
|
||||||
|
|
||||||
@selfrole.command(name="delete")
|
@selfrole.command(name="delete")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||||
"""
|
"""Remove a role from the list of available selfroles.
|
||||||
Removes a role from the list of available selfroles.
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
curr_selfroles.remove(role.id)
|
curr_selfroles.remove(role.id)
|
||||||
|
|
||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send(_("The selfroles list has been successfully modified."))
|
||||||
|
|
||||||
@selfrole.command(name="list")
|
@selfrole.command(name="list")
|
||||||
async def selfrole_list(self, ctx: commands.Context):
|
async def selfrole_list(self, ctx: commands.Context):
|
||||||
@@ -357,7 +359,7 @@ class Admin:
|
|||||||
selfroles = await self._valid_selfroles(ctx.guild)
|
selfroles = await self._valid_selfroles(ctx.guild)
|
||||||
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
|
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
|
||||||
|
|
||||||
msg = "Available Selfroles:\n{}".format(fmt_selfroles)
|
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles)
|
||||||
await ctx.send(box(msg, "diff"))
|
await ctx.send(box(msg, "diff"))
|
||||||
|
|
||||||
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
||||||
@@ -374,18 +376,19 @@ class Admin:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def serverlock(self, ctx: commands.Context):
|
async def serverlock(self, ctx: commands.Context):
|
||||||
"""
|
"""Lock a bot to its current servers only."""
|
||||||
Locks a bot to its current servers only.
|
|
||||||
"""
|
|
||||||
serverlocked = await self.conf.serverlocked()
|
serverlocked = await self.conf.serverlocked()
|
||||||
await self.conf.serverlocked.set(not serverlocked)
|
await self.conf.serverlocked.set(not serverlocked)
|
||||||
|
|
||||||
verb = "is now" if not serverlocked else "is no longer"
|
if serverlocked:
|
||||||
|
await ctx.send(_("The bot is no longer serverlocked."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("The bot is now serverlocked."))
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("Announcer", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Announcer:
|
class Announcer:
|
||||||
def __init__(self, ctx: commands.Context,
|
def __init__(self, ctx: commands.Context, message: str, config=None):
|
||||||
message: str,
|
|
||||||
config=None):
|
|
||||||
"""
|
"""
|
||||||
:param ctx:
|
:param ctx:
|
||||||
:param message:
|
:param message:
|
||||||
@@ -65,10 +66,9 @@ 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(
|
||||||
g.id
|
_("I could not announce to server: {server.id}").format(server=g)
|
||||||
))
|
)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("AdminConverters", __file__)
|
||||||
|
|
||||||
|
|
||||||
class MemberDefaultAuthor(commands.Converter):
|
class MemberDefaultAuthor(commands.Converter):
|
||||||
@@ -19,7 +22,7 @@ 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:
|
||||||
raise commands.BadArgument("Admin is not loaded.")
|
raise commands.BadArgument(_("The Admin cog is not loaded."))
|
||||||
|
|
||||||
conf = admin.conf
|
conf = admin.conf
|
||||||
selfroles = await conf.guild(ctx.guild).selfroles()
|
selfroles = await conf.guild(ctx.guild).selfroles()
|
||||||
@@ -28,6 +31,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,17 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../admin.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from .alias import Alias
|
from .alias import Alias
|
||||||
from discord.ext import commands
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: Red):
|
||||||
bot.add_cog(Alias(bot))
|
bot.add_cog(Alias(bot))
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from re import search
|
from re import search
|
||||||
from typing import Generator, Tuple, Iterable
|
from typing import Generator, Tuple, Iterable, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config
|
from redbot.core import Config, commands, checks
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from .alias_entry import AliasEntry
|
from .alias_entry import AliasEntry
|
||||||
|
|
||||||
_ = CogI18n("Alias", __file__)
|
_ = Translator("Alias", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Alias:
|
@cog_i18n(_)
|
||||||
"""
|
class Alias(commands.Cog):
|
||||||
Alias
|
"""Create aliases for commands.
|
||||||
|
|
||||||
Aliases are per server shortcuts for commands. They
|
Aliases are alternative names shortcuts for commands. They
|
||||||
can act as both a lambda (storing arguments for repeated use)
|
can act as both a lambda (storing arguments for repeated use)
|
||||||
or as simply a shortcut to saying "x y z".
|
or as simply a shortcut to saying "x y z".
|
||||||
|
|
||||||
When run, aliases will accept any additional arguments
|
When run, aliases will accept any additional arguments
|
||||||
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):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._aliases = Config.get_conf(self, 8927348724)
|
self._aliases = Config.get_conf(self, 8927348724)
|
||||||
|
|
||||||
@@ -49,16 +44,22 @@ 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: Optional[discord.Guild],
|
||||||
|
alias_name: str,
|
||||||
|
server_aliases: Iterable[AliasEntry] = (),
|
||||||
|
) -> Tuple[bool, Optional[AliasEntry]]:
|
||||||
|
|
||||||
if not server_aliases:
|
if not server_aliases and guild is not None:
|
||||||
server_aliases = await self.unloaded_aliases(guild)
|
server_aliases = await self.unloaded_aliases(guild)
|
||||||
|
|
||||||
global_aliases = await self.unloaded_global_aliases()
|
global_aliases = await self.unloaded_global_aliases()
|
||||||
@@ -76,10 +77,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 +95,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 +123,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 +145,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)
|
||||||
|
|
||||||
@@ -172,143 +176,166 @@ class Alias:
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def alias(self, ctx: commands.Context):
|
async def alias(self, ctx: commands.Context):
|
||||||
"""Manage per-server aliases for commands"""
|
"""Manage command aliases."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@alias.group(name="global")
|
@alias.group(name="global")
|
||||||
async def global_(self, ctx: commands.Context):
|
async def global_(self, ctx: commands.Context):
|
||||||
"""
|
"""Manage global aliases."""
|
||||||
Manage global aliases.
|
pass
|
||||||
"""
|
|
||||||
if ctx.invoked_subcommand is None or \
|
|
||||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
|
||||||
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."""
|
||||||
"""
|
# region Alias Add Validity Checking
|
||||||
Add an alias for a command.
|
|
||||||
"""
|
|
||||||
# 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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" name is already a command on this bot.").format(alias_name))
|
"You attempted to create a new alias"
|
||||||
|
" with the name {name} but that"
|
||||||
|
" name is already a command on this bot."
|
||||||
|
).format(name=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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" alias already exists on this server.").format(alias_name))
|
"You attempted to create a new alias"
|
||||||
|
" with the name {name} but that"
|
||||||
|
" alias already exists on this server."
|
||||||
|
).format(name=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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" name is an invalid alias name. Alias"
|
"You attempted to create a new alias"
|
||||||
" names may not contain spaces.").format(alias_name))
|
" with the name {name} but that"
|
||||||
|
" name is an invalid alias name. Alias"
|
||||||
|
" names may not contain spaces."
|
||||||
|
).format(name=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 `{name}` has been created.").format(name=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."""
|
||||||
"""
|
# region Alias Add Validity Checking
|
||||||
Add a global alias for a command.
|
|
||||||
"""
|
|
||||||
# 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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" name is already a command on this bot.").format(alias_name))
|
"You attempted to create a new global alias"
|
||||||
|
" with the name {name} but that"
|
||||||
|
" name is already a command on this bot."
|
||||||
|
).format(name=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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" alias already exists on this server.").format(alias_name))
|
"You attempted to create a new global alias"
|
||||||
|
" with the name {name} but that"
|
||||||
|
" alias already exists on this server."
|
||||||
|
).format(name=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(
|
||||||
" with the name {} but that"
|
_(
|
||||||
" name is an invalid alias name. Alias"
|
"You attempted to create a new global alias"
|
||||||
" names may not contain spaces.").format(alias_name))
|
" with the name {name} but that"
|
||||||
|
" name is an invalid alias name. Alias"
|
||||||
|
" names may not contain spaces."
|
||||||
|
).format(name=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 `{name}` has been created.").format(
|
||||||
|
name=alias_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@alias.command(name="help")
|
@alias.command(name="help")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Tries to execute help for the base command of the alias"""
|
"""Try to execute help for the base command of the alias."""
|
||||||
is_alias, alias = self.is_alias(ctx.guild, alias_name=alias_name)
|
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||||
if is_alias:
|
if is_alias:
|
||||||
base_cmd = alias.command[0]
|
base_cmd = alias.command[0]
|
||||||
|
|
||||||
new_msg = copy(ctx.message)
|
new_msg = copy(ctx.message)
|
||||||
new_msg.content = "{}help {}".format(ctx.prefix, base_cmd)
|
new_msg.content = _("{prefix}help {command}").format(
|
||||||
|
prefix=ctx.prefix, command=base_cmd
|
||||||
|
)
|
||||||
await self.bot.process_commands(new_msg)
|
await self.bot.process_commands(new_msg)
|
||||||
else:
|
else:
|
||||||
ctx.send(_("No such alias exists."))
|
await ctx.send(_("No such alias exists."))
|
||||||
|
|
||||||
@alias.command(name="show")
|
@alias.command(name="show")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _show_alias(self, ctx: commands.Context, alias_name: str):
|
async def _show_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Shows what command the alias executes."""
|
"""Show what command the alias executes."""
|
||||||
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_name}` alias will execute the command `{command}`").format(
|
||||||
|
alias_name=alias_name, command=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 `{name}`").format(name=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):
|
||||||
"""
|
"""Delete an existing alias on this server."""
|
||||||
Deletes an existing alias on this server.
|
|
||||||
"""
|
|
||||||
aliases = await self.unloaded_aliases(ctx.guild)
|
aliases = await self.unloaded_aliases(ctx.guild)
|
||||||
try:
|
try:
|
||||||
next(aliases)
|
next(aliases)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
await ctx.send(_("There are no aliases on this guild."))
|
await ctx.send(_("There are no aliases on this server."))
|
||||||
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 `{name}` was successfully deleted.").format(name=alias_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{name}` was not found.").format(name=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):
|
||||||
"""
|
"""Delete an existing global alias."""
|
||||||
Deletes an existing global alias.
|
|
||||||
"""
|
|
||||||
aliases = await self.unloaded_global_aliases()
|
aliases = await self.unloaded_global_aliases()
|
||||||
try:
|
try:
|
||||||
next(aliases)
|
next(aliases)
|
||||||
@@ -317,18 +344,19 @@ 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 `{name}` was successfully deleted.").format(name=alias_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
|
||||||
|
|
||||||
@alias.command(name="list")
|
@alias.command(name="list")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _list_alias(self, ctx: commands.Context):
|
async def _list_alias(self, ctx: commands.Context):
|
||||||
"""
|
"""List 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:
|
||||||
@@ -336,10 +364,10 @@ class Alias:
|
|||||||
|
|
||||||
@global_.command(name="list")
|
@global_.command(name="list")
|
||||||
async def _list_global_alias(self, ctx: commands.Context):
|
async def _list_global_alias(self, ctx: commands.Context):
|
||||||
"""
|
"""List 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:
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from redbot.core import commands
|
||||||
|
|
||||||
|
|
||||||
class AliasEntry:
|
class AliasEntry:
|
||||||
def __init__(self, name: str, command: Tuple[str],
|
def __init__(
|
||||||
creator: discord.Member, global_: bool=False):
|
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 +44,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,89 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../alias.py:129
|
|
||||||
msgid "No prefix found."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:198
|
|
||||||
msgid "You attempted to create a new alias with the name {} but that name is already a command on this bot."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:205
|
|
||||||
msgid "You attempted to create a new alias with the name {} but that alias already exists on this server."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:212
|
|
||||||
msgid "You attempted to create a new alias with the name {} but that name is an invalid alias name. Alias names may not contain spaces."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:224
|
|
||||||
msgid "A new alias with the trigger `{}` has been created."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:236
|
|
||||||
msgid "You attempted to create a new global alias with the name {} but that name is already a command on this bot."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:243
|
|
||||||
msgid "You attempted to create a new global alias with the name {} but that alias already exists on this server."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:250
|
|
||||||
msgid "You attempted to create a new global alias with the name {} but that name is an invalid alias name. Alias names may not contain spaces."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:259
|
|
||||||
msgid "A new global alias with the trigger `{}` has been created."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:274
|
|
||||||
msgid "No such alias exists."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:283
|
|
||||||
msgid "The `{}` alias will execute the command `{}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:286
|
|
||||||
msgid "There is no alias with the name `{}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:298
|
|
||||||
msgid "There are no aliases on this guild."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:302 ../alias.py:320
|
|
||||||
msgid "Alias with the name `{}` was successfully deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:305 ../alias.py:323
|
|
||||||
msgid "Alias with name `{}` was not found."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:316
|
|
||||||
msgid "There are no aliases on this bot."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:331 ../alias.py:342
|
|
||||||
msgid "Aliases:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../alias.py:333 ../alias.py:344
|
|
||||||
msgid "There are no aliases on this server."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../alias.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,27 +1,29 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
|
|
||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
from .manager import start_lavalink_server
|
from .manager import start_lavalink_server
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
import redbot.core
|
import redbot.core
|
||||||
|
|
||||||
|
log = logging.getLogger("red.audio")
|
||||||
|
|
||||||
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")
|
||||||
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
||||||
|
|
||||||
APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
|
APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
|
||||||
BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
|
BUNDLED_APP_YML_FILE = Path(__file__).parent / "data/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)
|
||||||
@@ -32,17 +34,15 @@ async def download_lavalink(session):
|
|||||||
|
|
||||||
async def maybe_download_lavalink(loop, cog):
|
async def maybe_download_lavalink(loop, cog):
|
||||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
jar_exists = LAVALINK_JAR_FILE.exists()
|
||||||
current_build = redbot.core.VersionInfo(*await cog.config.current_build())
|
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_build())
|
||||||
|
|
||||||
session = ClientSession(loop=loop)
|
|
||||||
|
|
||||||
if not jar_exists or current_build < redbot.core.version_info:
|
if not jar_exists or current_build < redbot.core.version_info:
|
||||||
|
log.info("Downloading Lavalink.jar")
|
||||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
await download_lavalink(session)
|
async with ClientSession(loop=loop) as session:
|
||||||
|
await download_lavalink(session)
|
||||||
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
||||||
|
|
||||||
|
|
||||||
@@ -52,5 +52,6 @@ async def setup(bot: commands.Bot):
|
|||||||
await maybe_download_lavalink(bot.loop, cog)
|
await maybe_download_lavalink(bot.loop, cog)
|
||||||
await start_lavalink_server(bot.loop)
|
await start_lavalink_server(bot.loop)
|
||||||
|
|
||||||
|
await cog.initialize()
|
||||||
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
bot.loop.create_task(cog.init_config())
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ lavalink:
|
|||||||
vimeo: true
|
vimeo: true
|
||||||
mixer: true
|
mixer: true
|
||||||
http: true
|
http: true
|
||||||
local: false
|
local: true
|
||||||
sentryDsn: ""
|
sentryDsn: ""
|
||||||
bufferDurationMs: 400
|
bufferDurationMs: 400
|
||||||
youtubePlaylistLoadLimit: 10000
|
youtubePlaylistLoadLimit: 10000
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../audio.py:25 ../audio.py:45
|
|
||||||
msgid "Join a voice channel first!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../audio.py:33
|
|
||||||
msgid "Let's play a file that exists pls"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../audio.py:38 ../audio.py:58
|
|
||||||
msgid "{} is playing a song..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../audio.py:48
|
|
||||||
msgid "Youtube links pls"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../audio.py:67 ../audio.py:77 ../audio.py:87 ../audio.py:97
|
|
||||||
msgid "I'm not even connected to a voice channel!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../audio.py:95
|
|
||||||
msgid "Volume set."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../audio.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
from subprocess import Popen, DEVNULL, PIPE
|
import asyncio.subprocess
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
from subprocess import Popen, DEVNULL
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
log = logging.getLogger('red.audio.manager')
|
_JavaVersion = Tuple[int, int]
|
||||||
|
|
||||||
|
log = logging.getLogger("red.audio.manager")
|
||||||
|
|
||||||
proc = None
|
proc = None
|
||||||
SHUTDOWN = asyncio.Event()
|
SHUTDOWN = asyncio.Event()
|
||||||
@@ -13,7 +18,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,38 +35,55 @@ 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) -> Tuple[bool, Optional[_JavaVersion]]:
|
||||||
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, None
|
||||||
|
|
||||||
version = await get_java_version(loop)
|
version = await get_java_version(loop)
|
||||||
return version >= (1, 8), version
|
return (2, 0) > version >= (1, 8) or version >= (8, 0), version
|
||||||
|
|
||||||
|
|
||||||
async def get_java_version(loop):
|
async def get_java_version(loop) -> _JavaVersion:
|
||||||
"""
|
"""
|
||||||
This assumes we've already checked that java exists.
|
This assumes we've already checked that java exists.
|
||||||
"""
|
"""
|
||||||
proc = Popen(
|
_proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
|
||||||
shlex.split("java -version", posix=os.name == 'posix'),
|
"java",
|
||||||
stdout=PIPE, stderr=PIPE
|
"-version",
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
loop=loop,
|
||||||
)
|
)
|
||||||
_, err = proc.communicate()
|
# java -version outputs to stderr
|
||||||
|
_, err = await _proc.communicate()
|
||||||
|
|
||||||
version_info = str(err, encoding='utf-8')
|
version_info: str = err.decode("utf-8")
|
||||||
|
# We expect the output to look something like:
|
||||||
|
# $ java -version
|
||||||
|
# ...
|
||||||
|
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
||||||
|
# ...
|
||||||
|
# We only care about the major and minor parts though.
|
||||||
|
version_line_re = re.compile(r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?"')
|
||||||
|
|
||||||
|
lines = version_info.splitlines()
|
||||||
|
for line in lines:
|
||||||
|
match = version_line_re.search(line)
|
||||||
|
if match:
|
||||||
|
return int(match["major"]), int(match["minor"])
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
||||||
|
"issue tracker."
|
||||||
|
)
|
||||||
|
|
||||||
version_line = version_info.split('\n')[0]
|
|
||||||
version_start = version_line.find('"')
|
|
||||||
version_string = version_line[version_start + 1:-1]
|
|
||||||
major, minor = version_string.split('.')[:2]
|
|
||||||
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)
|
||||||
@@ -72,13 +95,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))
|
||||||
@@ -92,4 +117,5 @@ def shutdown_lavalink_server():
|
|||||||
global proc
|
global proc
|
||||||
if proc is not None:
|
if proc is not None:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
|
proc.wait()
|
||||||
proc = None
|
proc = None
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
|
||||||
from redbot.core import checks, bank
|
from redbot.core import checks, bank, commands
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core.bot import Red # Only used for type hints
|
from redbot.core.bot import Red # Only used for type hints
|
||||||
|
|
||||||
_ = CogI18n('Bank', __file__)
|
_ = Translator("Bank", __file__)
|
||||||
|
|
||||||
|
|
||||||
def check_global_setting_guildowner():
|
def check_global_setting_guildowner():
|
||||||
@@ -15,15 +14,18 @@ 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):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
return author == ctx.guild.owner or permissions.administrator
|
return author == ctx.guild.owner or permissions.administrator
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
@@ -33,33 +35,39 @@ 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):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
is_guild_owner = author == ctx.guild.owner
|
is_guild_owner = author == ctx.guild.owner
|
||||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
class Bank:
|
@cog_i18n(_)
|
||||||
|
class Bank(commands.Cog):
|
||||||
"""Bank"""
|
"""Bank"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
# SECTION commands
|
# SECTION commands
|
||||||
|
|
||||||
@commands.group()
|
@check_global_setting_guildowner()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.group(autohelp=True)
|
||||||
async def bankset(self, ctx: commands.Context):
|
async def bankset(self, ctx: commands.Context):
|
||||||
"""Base command for bank settings"""
|
"""Base command for bank settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
bank_name = await bank._conf.bank_name()
|
bank_name = await bank._conf.bank_name()
|
||||||
@@ -67,54 +75,53 @@ class Bank:
|
|||||||
default_balance = await bank._conf.default_balance()
|
default_balance = await bank._conf.default_balance()
|
||||||
else:
|
else:
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
await ctx.send_help()
|
|
||||||
return
|
return
|
||||||
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
||||||
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\nBank name: {bank_name}\nCurrency: {currency_name}\n"
|
||||||
"Bank name: {}\n"
|
"Default balance: {default_balance}"
|
||||||
"Currency: {}\n"
|
).format(
|
||||||
"Default balance: {}"
|
bank_name=bank_name, currency_name=currency_name, default_balance=default_balance
|
||||||
"").format(bank_name, currency_name, default_balance)
|
|
||||||
)
|
)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
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
|
"""Toggle whether the bank is global or not.
|
||||||
If the bank is global, it will become per-server
|
|
||||||
If the bank is per-server, it will become global"""
|
If the bank is global, it will become per-server.
|
||||||
|
If the bank is per-server, it will become global.
|
||||||
|
"""
|
||||||
cur_setting = await bank.is_global()
|
cur_setting = await bank.is_global()
|
||||||
|
|
||||||
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 {banktype}, deleting all accounts "
|
||||||
word, "{}bankset toggleglobal yes".format(ctx.prefix)
|
"in the process! If you're sure, type `{command}`"
|
||||||
)
|
).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.set_global(not cur_setting)
|
await bank.set_global(not cur_setting)
|
||||||
await ctx.send(_("The bank is now {}.").format(word))
|
await ctx.send(_("The bank is now {banktype}.").format(banktype=word))
|
||||||
|
|
||||||
@bankset.command(name="bankname")
|
@bankset.command(name="bankname")
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
|
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
|
||||||
"""Set the bank's name"""
|
"""Set the bank's name."""
|
||||||
await bank.set_bank_name(name, ctx.guild)
|
await bank.set_bank_name(name, ctx.guild)
|
||||||
await ctx.send(_("Bank's name has been set to {}").format(name))
|
await ctx.send(_("Bank name has been set to: {name}").format(name=name))
|
||||||
|
|
||||||
@bankset.command(name="creditsname")
|
@bankset.command(name="creditsname")
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
|
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
|
||||||
"""Set the name for the bank's currency"""
|
"""Set the name for the bank's currency."""
|
||||||
await bank.set_currency_name(name, ctx.guild)
|
await bank.set_currency_name(name, ctx.guild)
|
||||||
await ctx.send(_("Currency name has been set to {}").format(name))
|
await ctx.send(_("Currency name has been set to: {name}").format(name=name))
|
||||||
|
|
||||||
# ENDSECTION
|
# ENDSECTION
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class BankError(Exception):
|
class BankError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BankNotGlobal(BankError):
|
class BankNotGlobal(BankError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../bank.py:68
|
|
||||||
msgid "global"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../bank.py:68
|
|
||||||
msgid "per-guild"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../bank.py:70
|
|
||||||
msgid "The bank is now {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../bank.py:77
|
|
||||||
msgid "Bank's name has been set to {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../bank.py:84
|
|
||||||
msgid "Currency name has been set to {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../bank.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,99 +1,129 @@
|
|||||||
import re
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Union, List, Callable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core import checks, RedContext
|
from redbot.core import checks, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||||
from redbot.cogs.mod.log import log
|
from redbot.cogs.mod.log import log
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = CogI18n("Cleanup", __file__)
|
_ = Translator("Cleanup", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Cleanup:
|
@cog_i18n(_)
|
||||||
"""Commands for cleaning messages"""
|
class Cleanup(commands.Cog):
|
||||||
|
"""Commands for cleaning up messages."""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def check_100_plus(ctx: RedContext, number: int) -> bool:
|
async def check_100_plus(ctx: commands.Context, number: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Called when trying to delete more than 100 messages at once
|
Called when trying to delete more than 100 messages at once.
|
||||||
|
|
||||||
Prompts the user to choose whether they want to continue or not
|
Prompts the user to choose whether they want to continue or not.
|
||||||
|
|
||||||
|
Tries its best to cleanup after itself if the response is positive.
|
||||||
"""
|
"""
|
||||||
def author_check(message):
|
|
||||||
return message.author == ctx.author
|
|
||||||
|
|
||||||
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 {number} messages? (y/n)").format(number=number)
|
||||||
|
)
|
||||||
|
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
|
||||||
|
|
||||||
if response.content.lower().startswith('y'):
|
if response.content.lower().startswith("y"):
|
||||||
|
await prompt.delete()
|
||||||
|
try:
|
||||||
|
await response.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
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: RedContext, channel: discord.TextChannel, number,
|
*,
|
||||||
check=lambda x: True, limit=100, before=None, after=None
|
channel: discord.TextChannel,
|
||||||
) -> list:
|
number: int = None,
|
||||||
|
check: Callable[[discord.Message], bool] = lambda x: True,
|
||||||
|
before: Union[discord.Message, datetime] = None,
|
||||||
|
after: Union[discord.Message, datetime] = None,
|
||||||
|
delete_pinned: bool = False,
|
||||||
|
) -> List[discord.Message]:
|
||||||
"""
|
"""
|
||||||
Gets a list of messages meeting the requirements to be deleted.
|
Gets a list of messages meeting the requirements to be deleted.
|
||||||
|
|
||||||
Generally, the requirements are:
|
Generally, the requirements are:
|
||||||
- We don't have the number of messages to be deleted already
|
- We don't have the number of messages to be deleted already
|
||||||
- The message passes a provided check (if no check is provided,
|
- The message passes a provided check (if no check is provided,
|
||||||
this is automatically true)
|
this is automatically true)
|
||||||
- The message is less than 14 days old
|
- The message is less than 14 days old
|
||||||
"""
|
- The message is not pinned
|
||||||
to_delete = []
|
|
||||||
too_old = False
|
|
||||||
|
|
||||||
while not too_old and len(to_delete) - 1 < number:
|
Warning: Due to the way the API hands messages back in chunks,
|
||||||
message = None
|
passing after and a number together is not advisable.
|
||||||
async for message in channel.history(limit=limit,
|
If you need to accomplish this, you should filter messages on
|
||||||
before=before,
|
the entire applicable range, rather than use this utility.
|
||||||
after=after):
|
"""
|
||||||
if (not number or len(to_delete) - 1 < number) and check(message) \
|
|
||||||
and (ctx.message.created_at - message.created_at).days < 14:
|
# This isn't actually two weeks ago to allow some wiggle room on API limits
|
||||||
to_delete.append(message)
|
two_weeks_ago = datetime.utcnow() - timedelta(days=14, minutes=-5)
|
||||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
|
||||||
too_old = True
|
def message_filter(message):
|
||||||
break
|
return (
|
||||||
elif number and len(to_delete) >= number:
|
check(message)
|
||||||
break
|
and message.created_at > two_weeks_ago
|
||||||
if message is None:
|
and (delete_pinned or not message.pinned)
|
||||||
|
)
|
||||||
|
|
||||||
|
if after:
|
||||||
|
if isinstance(after, discord.Message):
|
||||||
|
after = after.created_at
|
||||||
|
after = max(after, two_weeks_ago)
|
||||||
|
|
||||||
|
collected = []
|
||||||
|
async for message in channel.history(
|
||||||
|
limit=None, before=before, after=after, reverse=False
|
||||||
|
):
|
||||||
|
if message.created_at < two_weeks_ago:
|
||||||
break
|
break
|
||||||
else:
|
if check(message):
|
||||||
before = message
|
collected.append(message)
|
||||||
return to_delete
|
if number and number <= len(collected):
|
||||||
|
break
|
||||||
|
|
||||||
|
return collected
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def cleanup(self, ctx: RedContext):
|
async def cleanup(self, ctx: commands.Context):
|
||||||
"""Deletes messages."""
|
"""Delete messages."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@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: RedContext, text: str, number: int):
|
async def text(
|
||||||
"""Deletes last X messages matching the specified text.
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
|
"""Delete the last X messages matching the specified text.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
cleanup text \"test\" 5
|
`[p]cleanup text "test" 5`
|
||||||
|
|
||||||
Remember to use double quotes."""
|
Remember to use double quotes.
|
||||||
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
cont = await self.check_100_plus(ctx, number)
|
cont = await self.check_100_plus(ctx, number)
|
||||||
@@ -109,97 +139,16 @@ 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)
|
channel=channel,
|
||||||
|
number=number,
|
||||||
reason = "{}({}) deleted {} messages "\
|
check=check,
|
||||||
" containing '{}' in channel {}.".format(author.name,
|
before=ctx.message,
|
||||||
author.id, len(to_delete), text, channel.id)
|
delete_pinned=delete_pinned,
|
||||||
log.info(reason)
|
|
||||||
|
|
||||||
if is_bot:
|
|
||||||
await mass_purge(to_delete, channel)
|
|
||||||
else:
|
|
||||||
await slow_deletion(to_delete)
|
|
||||||
|
|
||||||
@cleanup.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def user(self, ctx: RedContext, user: discord.Member or int, number: int):
|
|
||||||
"""Deletes last X messages from specified user.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
cleanup user @\u200bTwentysix 2
|
|
||||||
cleanup user Red 6"""
|
|
||||||
|
|
||||||
channel = ctx.channel
|
|
||||||
author = ctx.author
|
|
||||||
is_bot = self.bot.user.bot
|
|
||||||
|
|
||||||
if number > 100:
|
|
||||||
cont = await self.check_100_plus(ctx, number)
|
|
||||||
if not cont:
|
|
||||||
return
|
|
||||||
|
|
||||||
def check(m):
|
|
||||||
if isinstance(user, discord.Member) and m.author == user:
|
|
||||||
return True
|
|
||||||
elif m.author.id == user: # Allow finding messages based on an ID
|
|
||||||
return True
|
|
||||||
elif m == ctx.message:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
|
||||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
|
||||||
)
|
|
||||||
reason = "{}({}) deleted {} messages "\
|
|
||||||
" made by {}({}) in channel {}."\
|
|
||||||
"".format(author.name, author.id, len(to_delete),
|
|
||||||
user.name, user.id, channel.name)
|
|
||||||
log.info(reason)
|
|
||||||
|
|
||||||
if is_bot:
|
|
||||||
# For whatever reason the purge endpoint requires manage_messages
|
|
||||||
await mass_purge(to_delete, channel)
|
|
||||||
else:
|
|
||||||
await slow_deletion(to_delete)
|
|
||||||
|
|
||||||
@cleanup.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def after(self, ctx: RedContext, message_id: int):
|
|
||||||
"""Deletes all messages after specified message.
|
|
||||||
|
|
||||||
To get a message id, enable developer mode in Discord's
|
|
||||||
settings, 'appearance' tab. Then right click a message
|
|
||||||
and copy its id.
|
|
||||||
|
|
||||||
This command only works on bots running as bot accounts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
channel = ctx.channel
|
|
||||||
author = ctx.author
|
|
||||||
is_bot = self.bot.user.bot
|
|
||||||
|
|
||||||
if not is_bot:
|
|
||||||
await ctx.send(_("This command can only be used on bots with "
|
|
||||||
"bot accounts."))
|
|
||||||
return
|
|
||||||
|
|
||||||
after = await channel.get_message(message_id)
|
|
||||||
|
|
||||||
if not after:
|
|
||||||
await ctx.send(_("Message not found."))
|
|
||||||
return
|
|
||||||
|
|
||||||
to_delete = await self.get_messages_for_deletion(
|
|
||||||
ctx, channel, 0, limit=None, after=after
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}."\
|
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
||||||
"".format(author.name, author.id,
|
author.name, author.id, len(to_delete), text, channel.id
|
||||||
len(to_delete), channel.name)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
|
|
||||||
await mass_purge(to_delete, channel)
|
await mass_purge(to_delete, channel)
|
||||||
@@ -207,16 +156,133 @@ 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: RedContext, number: int):
|
async def user(
|
||||||
"""Deletes last X messages.
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
|
"""Delete the last X messages from a specified user.
|
||||||
|
|
||||||
Example:
|
Examples:
|
||||||
cleanup messages 26"""
|
`[p]cleanup user @\u200bTwentysix 2`
|
||||||
|
`[p]cleanup user Red 6`
|
||||||
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
|
|
||||||
|
member = None
|
||||||
|
try:
|
||||||
|
member = await commands.converter.MemberConverter().convert(ctx, user)
|
||||||
|
except commands.BadArgument:
|
||||||
|
try:
|
||||||
|
_id = int(user)
|
||||||
|
except ValueError:
|
||||||
|
raise commands.BadArgument()
|
||||||
|
else:
|
||||||
|
_id = member.id
|
||||||
|
|
||||||
|
author = ctx.author
|
||||||
|
|
||||||
|
if number > 100:
|
||||||
|
cont = await self.check_100_plus(ctx, number)
|
||||||
|
if not cont:
|
||||||
|
return
|
||||||
|
|
||||||
|
def check(m):
|
||||||
|
if m.author.id == _id:
|
||||||
|
return True
|
||||||
|
elif m == ctx.message:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
to_delete = await self.get_messages_for_deletion(
|
||||||
|
channel=channel,
|
||||||
|
number=number,
|
||||||
|
check=check,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||||
|
"""Delete all messages after a specified message.
|
||||||
|
|
||||||
|
To get a message id, enable developer mode in Discord's
|
||||||
|
settings, 'appearance' tab. Then right click a message
|
||||||
|
and copy its id.
|
||||||
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
is_bot = self.bot.user.bot
|
try:
|
||||||
|
after = await channel.get_message(message_id)
|
||||||
|
except discord.NotFound:
|
||||||
|
return await ctx.send(_("Message not found."))
|
||||||
|
|
||||||
|
to_delete = await self.get_messages_for_deletion(
|
||||||
|
channel=channel, number=None, after=after, delete_pinned=delete_pinned
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
|
author.name, author.id, len(to_delete), channel.name
|
||||||
|
)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def before(
|
||||||
|
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
|
"""Deletes X messages before specified message.
|
||||||
|
|
||||||
|
To get a message id, enable developer mode in Discord's
|
||||||
|
settings, 'appearance' tab. Then right click a message
|
||||||
|
and copy its id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
|
||||||
|
try:
|
||||||
|
before = await channel.get_message(message_id)
|
||||||
|
except discord.NotFound:
|
||||||
|
return await ctx.send(_("Message not found."))
|
||||||
|
|
||||||
|
to_delete = await self.get_messages_for_deletion(
|
||||||
|
channel=channel, number=number, before=before, delete_pinned=delete_pinned
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
|
author.name, author.id, len(to_delete), channel.name
|
||||||
|
)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
|
"""Delete the last X messages.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`[p]cleanup messages 26`
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
cont = await self.check_100_plus(ctx, number)
|
cont = await self.check_100_plus(ctx, number)
|
||||||
@@ -224,41 +290,38 @@ 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
|
channel=channel, number=number, before=ctx.message, delete_pinned=delete_pinned
|
||||||
)
|
)
|
||||||
|
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:
|
await mass_purge(to_delete, channel)
|
||||||
await mass_purge(to_delete, channel)
|
|
||||||
else:
|
|
||||||
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: RedContext, number: int):
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Cleans up command messages and messages from the bot."""
|
"""Clean up command messages and messages from the bot."""
|
||||||
|
|
||||||
channel = ctx.message.channel
|
channel = ctx.channel
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
is_bot = self.bot.user.bot
|
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
cont = await self.check_100_plus(ctx, number)
|
cont = await self.check_100_plus(ctx, number)
|
||||||
if not cont:
|
if not cont:
|
||||||
return
|
return
|
||||||
|
|
||||||
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
|
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
|
||||||
if isinstance(prefixes, str):
|
if isinstance(prefixes, str):
|
||||||
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:
|
||||||
@@ -267,28 +330,37 @@ 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
|
channel=channel,
|
||||||
|
number=number,
|
||||||
|
check=check,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
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:
|
await mass_purge(to_delete, channel)
|
||||||
await mass_purge(to_delete, channel)
|
|
||||||
else:
|
|
||||||
await slow_deletion(to_delete)
|
|
||||||
|
|
||||||
@cleanup.command(name='self')
|
@cleanup.command(name="self")
|
||||||
async def cleanup_self(self, ctx: RedContext, number: int, match_pattern: str = None):
|
async def cleanup_self(
|
||||||
"""Cleans up messages owned by the bot.
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
number: int,
|
||||||
|
match_pattern: str = None,
|
||||||
|
delete_pinned: bool = False,
|
||||||
|
):
|
||||||
|
"""Clean 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,
|
||||||
it is used for pattern matching: If it begins with r( and ends with ),
|
it is used for pattern matching: If it begins with r( and ends with ),
|
||||||
@@ -300,7 +372,6 @@ class Cleanup:
|
|||||||
"""
|
"""
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
is_bot = self.bot.user.bot
|
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
cont = await self.check_100_plus(ctx, number)
|
cont = await self.check_100_plus(ctx, number)
|
||||||
@@ -313,8 +384,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'
|
||||||
@@ -322,10 +392,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
|
||||||
|
|
||||||
@@ -337,25 +411,26 @@ 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
|
channel=channel,
|
||||||
|
number=number,
|
||||||
|
check=check,
|
||||||
|
before=ctx.message,
|
||||||
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Selfbot convenience, delete trigger message
|
if ctx.guild:
|
||||||
if author == self.bot.user:
|
channel_name = "channel " + channel.name
|
||||||
to_delete.append(ctx.message)
|
|
||||||
|
|
||||||
if 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 can_mass_purge:
|
||||||
await mass_purge(to_delete, channel)
|
await mass_purge(to_delete, channel)
|
||||||
else:
|
else:
|
||||||
await slow_deletion(to_delete)
|
await slow_deletion(to_delete)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../cleanup.py:150
|
|
||||||
msgid "This command can only be used on bots with bot accounts."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../cleanup.py:157
|
|
||||||
msgid "Message not found."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../cleanup.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,35 +1,44 @@
|
|||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
from inspect import Parameter
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Mapping, Tuple, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core import Config, checks
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.utils.chat_formatting import box, pagify
|
from redbot.core.utils.chat_formatting import box, pagify
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = CogI18n("CustomCommands", __file__)
|
_ = Translator("CustomCommands", __file__)
|
||||||
|
|
||||||
|
|
||||||
class CCError(Exception):
|
class CCError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotFound(CCError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExists(CCError):
|
class AlreadyExists(CCError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandObj:
|
class ArgParseError(CCError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(CCError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OnCooldown(CCError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -41,275 +50,308 @@ 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 = _(
|
||||||
"Every message you send will be added as one of the random "
|
"Welcome to the interactive random {cc} maker!\n"
|
||||||
"response to choose from once this {} is "
|
"Every message you send will be added as one of the random "
|
||||||
"triggered. To exit this interactive menu, type `{}`").format(
|
"responses to choose from once this {cc} is "
|
||||||
"customcommand", "customcommand", "exit()"
|
"triggered. To exit this interactive menu, type `{quit}`"
|
||||||
))
|
).format(cc="customcommand", quit="exit()")
|
||||||
await ctx.send(intro)
|
await ctx.send(intro)
|
||||||
|
|
||||||
def check(m):
|
|
||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
|
||||||
responses = []
|
responses = []
|
||||||
|
args = None
|
||||||
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=MessagePredicate.same_context(ctx))
|
||||||
|
|
||||||
if msg.content.lower() == 'exit()':
|
if msg.content.lower() == "exit()":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
this_args = ctx.cog.prepare_args(msg.content)
|
||||||
|
except ArgParseError as e:
|
||||||
|
await ctx.send(e.args[0])
|
||||||
|
continue
|
||||||
|
if args and args != this_args:
|
||||||
|
await ctx.send(_("Random responses must take the same arguments!"))
|
||||||
|
continue
|
||||||
|
args = args or this_args
|
||||||
responses.append(msg.content)
|
responses.append(msg.content)
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
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) -> Tuple[str, Dict]:
|
||||||
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"], ccinfo.get("cooldowns", {})
|
||||||
|
|
||||||
async def create(self,
|
async def create(self, ctx: commands.Context, command: str, *, response):
|
||||||
ctx: commands.Context,
|
"""Create a custom command"""
|
||||||
command: str,
|
|
||||||
response):
|
|
||||||
"""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()
|
||||||
|
# test to raise
|
||||||
|
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
||||||
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
|
"cooldowns": {},
|
||||||
},
|
"created_at": self.get_now(),
|
||||||
'command': command,
|
"editors": [],
|
||||||
'created_at': self.get_now(),
|
"response": response,
|
||||||
'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(
|
||||||
ctx: commands.Context,
|
self,
|
||||||
command: str,
|
ctx: commands.Context,
|
||||||
response: None):
|
command: str,
|
||||||
|
*,
|
||||||
|
response=None,
|
||||||
|
cooldowns: Mapping[str, int] = None,
|
||||||
|
ask_for: bool = True
|
||||||
|
):
|
||||||
"""Edit an already existing custom command"""
|
"""Edit an already existing custom command"""
|
||||||
|
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
||||||
|
|
||||||
# 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 ccinfo:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
|
||||||
|
|
||||||
def check(m):
|
if ask_for and not response:
|
||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
await ctx.send(_("Do you want to create a 'randomized' custom command? (y/n)"))
|
||||||
|
|
||||||
if not response:
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
await ctx.send(
|
try:
|
||||||
_("Do you want to create a 'randomized' cc? {}").format("y/n")
|
await self.bot.wait_for("message", check=pred, timeout=30)
|
||||||
)
|
except TimeoutError:
|
||||||
|
await ctx.send(_("Response timed out, please try again later."))
|
||||||
msg = await self.bot.wait_for('message', check=check)
|
return
|
||||||
if msg.content.lower() == 'y':
|
if pred.result is True:
|
||||||
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(
|
try:
|
||||||
'message', check=check)
|
resp = await self.bot.wait_for(
|
||||||
).content
|
"message", check=MessagePredicate.same_context(ctx), timeout=180
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
await ctx.send(_("Response timed out, please try again later."))
|
||||||
|
return
|
||||||
|
response = resp.content
|
||||||
|
|
||||||
ccinfo['response'] = response
|
if response:
|
||||||
ccinfo['edited_at'] = self.get_now()
|
# test to raise
|
||||||
|
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
||||||
|
ccinfo["response"] = response
|
||||||
|
|
||||||
if author.id not in ccinfo['editors']:
|
if cooldowns:
|
||||||
|
ccinfo.setdefault("cooldowns", {}).update(cooldowns)
|
||||||
|
for key, value in ccinfo["cooldowns"].copy().items():
|
||||||
|
if value <= 0:
|
||||||
|
del ccinfo["cooldowns"][key]
|
||||||
|
|
||||||
|
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(
|
ccinfo["edited_at"] = self.get_now()
|
||||||
command, value=ccinfo)
|
|
||||||
|
|
||||||
async def delete(self,
|
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||||
ctx: commands.Context,
|
|
||||||
command: str):
|
async def delete(self, 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)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomCommands:
|
@cog_i18n(_)
|
||||||
"""Custom commands
|
class CustomCommands(commands.Cog):
|
||||||
Creates commands used to display text"""
|
"""Creates commands used to display text."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
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)
|
self.cooldowns = {}
|
||||||
|
|
||||||
@commands.group(aliases=["cc"], no_pm=True)
|
@commands.group(aliases=["cc"])
|
||||||
@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"""
|
pass
|
||||||
if not ctx.invoked_subcommand:
|
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@customcom.group(name="add")
|
@customcom.group(name="create", aliases=["add"])
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add(self,
|
async def cc_create(self, ctx: commands.Context):
|
||||||
ctx: commands.Context):
|
"""Create custom commands.
|
||||||
"""
|
|
||||||
CCs can be enhanced with arguments:
|
|
||||||
https: // twentysix26.github.io / Red - Docs / red_guide_command_args/
|
|
||||||
"""
|
|
||||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
|
|
||||||
commands.Group):
|
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@cc_add.command(name='random')
|
CCs can be enhanced with arguments, see the guide
|
||||||
|
[here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cc_create.command(name="random")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_random(self,
|
async def cc_create_random(self, ctx: commands.Context, command: str.lower):
|
||||||
ctx: commands.Context,
|
"""Create a CC where it will randomly choose a response!
|
||||||
command: str):
|
|
||||||
"""
|
|
||||||
Create a CC where it will randomly choose a response!
|
|
||||||
Note: This is interactive
|
|
||||||
"""
|
|
||||||
channel = ctx.channel
|
|
||||||
responses = []
|
|
||||||
|
|
||||||
|
Note: This command is interactive.
|
||||||
|
"""
|
||||||
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 `{command}` to edit it.").format(
|
||||||
"`{}` to edit it.").format(
|
command="{}customcom edit".format(ctx.prefix)
|
||||||
"{}customcom edit".format(ctx.prefix)
|
)
|
||||||
))
|
)
|
||||||
|
|
||||||
# await ctx.send(str(responses))
|
@cc_create.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_create_simple(self, ctx, command: str.lower, *, text: str):
|
||||||
ctx,
|
"""Add a simple custom command.
|
||||||
command: str,
|
|
||||||
*,
|
|
||||||
text):
|
|
||||||
"""Adds a simple custom command
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom add simple yourcommand Text you want
|
- `[p]customcom create simple yourcommand Text you want`
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
|
||||||
command = command.lower()
|
|
||||||
if command in self.bot.all_commands:
|
if command in self.bot.all_commands:
|
||||||
await ctx.send(_("That command is already a standard command."))
|
await ctx.send(_("There already exists a bot command with the same name."))
|
||||||
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 `{command}` to edit it.").format(
|
||||||
"`{}` to edit it.").format(
|
command="{}customcom edit".format(ctx.prefix)
|
||||||
"{}customcom edit".format(ctx.prefix)
|
)
|
||||||
))
|
)
|
||||||
|
except ArgParseError as e:
|
||||||
|
await ctx.send(e.args[0])
|
||||||
|
|
||||||
@customcom.command(name="edit")
|
@customcom.command(name="cooldown")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_edit(self,
|
async def cc_cooldown(
|
||||||
ctx,
|
self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
|
||||||
command: str,
|
):
|
||||||
*,
|
"""Set, edit, or view the cooldown for a custom command.
|
||||||
text=None):
|
|
||||||
"""Edits a custom command
|
|
||||||
Example:
|
|
||||||
[p]customcom edit yourcommand Text you want
|
|
||||||
"""
|
|
||||||
guild = ctx.message.guild
|
|
||||||
command = command.lower()
|
|
||||||
|
|
||||||
|
You may set cooldowns per member, channel, or guild. Multiple
|
||||||
|
cooldowns may be set. All cooldowns must be cooled to call the
|
||||||
|
custom command.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- `[p]customcom cooldown yourcommand 30`
|
||||||
|
"""
|
||||||
|
if cooldown is None:
|
||||||
|
try:
|
||||||
|
cooldowns = (await self.commandobj.get(ctx.message, command))[1]
|
||||||
|
except NotFound:
|
||||||
|
return await ctx.send(_("That command doesn't exist."))
|
||||||
|
if cooldowns:
|
||||||
|
cooldown = []
|
||||||
|
for per, rate in cooldowns.items():
|
||||||
|
cooldown.append(
|
||||||
|
_("A {} may call this command every {} seconds").format(per, rate)
|
||||||
|
)
|
||||||
|
return await ctx.send("\n".join(cooldown))
|
||||||
|
else:
|
||||||
|
return await ctx.send(_("This command has no cooldown."))
|
||||||
|
per = {"server": "guild", "user": "member"}.get(per, per)
|
||||||
|
allowed = ("guild", "member", "channel")
|
||||||
|
if per not in allowed:
|
||||||
|
return await ctx.send(_("{} must be one of {}").format("per", ", ".join(allowed)))
|
||||||
|
cooldown = {per: cooldown}
|
||||||
try:
|
try:
|
||||||
await self.commandobj.edit(ctx=ctx,
|
await self.commandobj.edit(ctx=ctx, command=command, cooldowns=cooldown, ask_for=False)
|
||||||
command=command,
|
await ctx.send(_("Custom command cooldown successfully edited."))
|
||||||
response=text)
|
|
||||||
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 `{command}` to add it.").format(
|
||||||
"`{}` to add it.").format(
|
command="{}customcom create".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.lower):
|
||||||
ctx,
|
"""Delete a custom command
|
||||||
command: str):
|
.
|
||||||
"""Deletes a custom command
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom delete yourcommand"""
|
- `[p]customcom delete yourcommand`
|
||||||
guild = ctx.message.guild
|
"""
|
||||||
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="edit")
|
||||||
|
@checks.mod_or_permissions(administrator=True)
|
||||||
|
async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
|
||||||
|
"""Edit a custom command.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- `[p]customcom edit yourcommand Text you want`
|
||||||
|
"""
|
||||||
|
command = command.lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.commandobj.edit(ctx=ctx, command=command, response=text)
|
||||||
|
await ctx.send(_("Custom command successfully edited."))
|
||||||
|
except NotFound:
|
||||||
|
await ctx.send(
|
||||||
|
_("That command doesn't exist. Use `{command}` to add it.").format(
|
||||||
|
command="{}customcom create".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ArgParseError as e:
|
||||||
|
await ctx.send(e.args[0])
|
||||||
|
|
||||||
@customcom.command(name="list")
|
@customcom.command(name="list")
|
||||||
async def cc_list(self,
|
async def cc_list(self, ctx):
|
||||||
ctx):
|
"""List all available custom commands."""
|
||||||
"""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."
|
_(
|
||||||
" Use `{}` to start adding some.").format(
|
"There are no custom commands in this server."
|
||||||
"{}customcom add".format(ctx.prefix)
|
" Use `{command}` to start adding some."
|
||||||
))
|
).format(command="{}customcom create".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)
|
||||||
|
|
||||||
@@ -319,59 +361,176 @@ 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:
|
|
||||||
return
|
|
||||||
|
|
||||||
guild = message.guild
|
|
||||||
prefixes = await self.bot.db.guild(guild).get_raw('prefix', default=[])
|
|
||||||
|
|
||||||
if len(prefixes) < 1:
|
|
||||||
def_prefixes = await self.bot.get_prefix(message)
|
|
||||||
for prefix in def_prefixes:
|
|
||||||
prefixes.append(prefix)
|
|
||||||
|
|
||||||
# user_allowed check, will be replaced with self.bot.user_allowed or
|
# user_allowed check, will be replaced with self.bot.user_allowed or
|
||||||
# something similar once it's added
|
# something similar once it's added
|
||||||
|
|
||||||
user_allowed = True
|
user_allowed = True
|
||||||
|
|
||||||
for prefix in prefixes:
|
if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
|
||||||
if message.content.startswith(prefix):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if user_allowed:
|
ctx = await self.bot.get_context(message)
|
||||||
cmd = message.content[len(prefix):]
|
|
||||||
try:
|
|
||||||
c = await self.commandobj.get(message=message,
|
|
||||||
command=cmd)
|
|
||||||
if isinstance(c, list):
|
|
||||||
command = random.choice(c)
|
|
||||||
elif isinstance(c, str):
|
|
||||||
command = c
|
|
||||||
else:
|
|
||||||
raise NotFound()
|
|
||||||
except NotFound:
|
|
||||||
return
|
|
||||||
response = self.format_cc(command, message)
|
|
||||||
await message.channel.send(response)
|
|
||||||
|
|
||||||
def format_cc(self,
|
if ctx.prefix is None or ctx.valid:
|
||||||
command,
|
return
|
||||||
message) -> str:
|
|
||||||
results = re.findall("\{([^}]+)\}", command)
|
try:
|
||||||
|
raw_response, cooldowns = await self.commandobj.get(
|
||||||
|
message=message, command=ctx.invoked_with
|
||||||
|
)
|
||||||
|
if isinstance(raw_response, list):
|
||||||
|
raw_response = random.choice(raw_response)
|
||||||
|
elif isinstance(raw_response, str):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise NotFound()
|
||||||
|
if cooldowns:
|
||||||
|
self.test_cooldowns(ctx, ctx.invoked_with, cooldowns)
|
||||||
|
except CCError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# wrap the command here so it won't register with the bot
|
||||||
|
fake_cc = commands.Command(ctx.invoked_with, self.cc_callback)
|
||||||
|
fake_cc.params = self.prepare_args(raw_response)
|
||||||
|
ctx.command = fake_cc
|
||||||
|
|
||||||
|
await self.bot.invoke(ctx)
|
||||||
|
if not ctx.command_failed:
|
||||||
|
await self.cc_command(*ctx.args, **ctx.kwargs, raw_response=raw_response)
|
||||||
|
|
||||||
|
async def cc_callback(self, *args, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Custom command.
|
||||||
|
|
||||||
|
Created via the CustomCom cog. See `[p]customcom` for more details.
|
||||||
|
"""
|
||||||
|
# fake command to take advantage of discord.py's parsing and events
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cc_command(self, ctx, *cc_args, raw_response, **cc_kwargs) -> None:
|
||||||
|
cc_args = (*cc_args, *cc_kwargs.values())
|
||||||
|
results = re.findall(r"\{([^}]+)\}", raw_response)
|
||||||
for result in results:
|
for result in results:
|
||||||
param = self.transform_parameter(result, message)
|
param = self.transform_parameter(result, ctx.message)
|
||||||
command = command.replace("{" + result + "}", param)
|
raw_response = raw_response.replace("{" + result + "}", param)
|
||||||
return command
|
results = re.findall(r"\{((\d+)[^\.}]*(\.[^:}]+)?[^}]*)\}", raw_response)
|
||||||
|
if results:
|
||||||
|
low = min(int(result[1]) for result in results)
|
||||||
|
for result in results:
|
||||||
|
index = int(result[1]) - low
|
||||||
|
arg = self.transform_arg(result[0], result[2], cc_args[index])
|
||||||
|
raw_response = raw_response.replace("{" + result[0] + "}", arg)
|
||||||
|
await ctx.send(raw_response)
|
||||||
|
|
||||||
def transform_parameter(self,
|
def prepare_args(self, raw_response) -> Mapping[str, Parameter]:
|
||||||
result,
|
args = re.findall(r"\{(\d+)[^:}]*(:[^\.}]*)?[^}]*\}", raw_response)
|
||||||
message) -> str:
|
default = [["ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD)]]
|
||||||
|
if not args:
|
||||||
|
return OrderedDict(default)
|
||||||
|
allowed_builtins = {
|
||||||
|
"bool": bool,
|
||||||
|
"complex": complex,
|
||||||
|
"float": float,
|
||||||
|
"frozenset": frozenset,
|
||||||
|
"int": int,
|
||||||
|
"list": list,
|
||||||
|
"set": set,
|
||||||
|
"str": str,
|
||||||
|
"tuple": tuple,
|
||||||
|
}
|
||||||
|
indices = [int(a[0]) for a in args]
|
||||||
|
low = min(indices)
|
||||||
|
indices = [a - low for a in indices]
|
||||||
|
high = max(indices)
|
||||||
|
if high > 9:
|
||||||
|
raise ArgParseError(_("Too many arguments!"))
|
||||||
|
gaps = set(indices).symmetric_difference(range(high + 1))
|
||||||
|
if gaps:
|
||||||
|
raise ArgParseError(
|
||||||
|
_("Arguments must be sequential. Missing arguments: ")
|
||||||
|
+ ", ".join(str(i + low) for i in gaps)
|
||||||
|
)
|
||||||
|
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
|
||||||
|
for arg in args:
|
||||||
|
index = int(arg[0]) - low
|
||||||
|
anno = arg[1][1:] # strip initial colon
|
||||||
|
if anno.lower().endswith("converter"):
|
||||||
|
anno = anno[:-9]
|
||||||
|
if not anno or anno.startswith("_"): # public types only
|
||||||
|
name = "{}_{}".format("text", index if index < high else "final")
|
||||||
|
fin[index] = fin[index].replace(name=name)
|
||||||
|
continue
|
||||||
|
# allow type hinting only for discord.py and builtin types
|
||||||
|
try:
|
||||||
|
anno = getattr(discord, anno)
|
||||||
|
# force an AttributeError if there's no discord.py converter
|
||||||
|
getattr(commands.converter, anno.__name__ + "Converter")
|
||||||
|
except AttributeError:
|
||||||
|
anno = allowed_builtins.get(anno.lower(), Parameter.empty)
|
||||||
|
if (
|
||||||
|
anno is not Parameter.empty
|
||||||
|
and fin[index].annotation is not Parameter.empty
|
||||||
|
and anno != fin[index].annotation
|
||||||
|
):
|
||||||
|
raise ArgParseError(
|
||||||
|
_(
|
||||||
|
'Conflicting colon notation for argument {index}: "{name1}" and "{name2}".'
|
||||||
|
).format(
|
||||||
|
index=index + low,
|
||||||
|
name1=fin[index].annotation.__name__,
|
||||||
|
name2=anno.__name__,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if anno is not Parameter.empty:
|
||||||
|
fin[index] = fin[index].replace(annotation=anno)
|
||||||
|
# consume rest
|
||||||
|
fin[-1] = fin[-1].replace(kind=Parameter.KEYWORD_ONLY)
|
||||||
|
# name the parameters for the help text
|
||||||
|
for i, param in enumerate(fin):
|
||||||
|
anno = param.annotation
|
||||||
|
name = "{}_{}".format(
|
||||||
|
"text" if anno is Parameter.empty else anno.__name__.lower(),
|
||||||
|
i if i < high else "final",
|
||||||
|
)
|
||||||
|
fin[i] = fin[i].replace(name=name)
|
||||||
|
# insert ctx parameter for discord.py parsing
|
||||||
|
fin = default + [(p.name, p) for p in fin]
|
||||||
|
return OrderedDict(fin)
|
||||||
|
|
||||||
|
def test_cooldowns(self, ctx, command, cooldowns):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
new_cooldowns = {}
|
||||||
|
for per, rate in cooldowns.items():
|
||||||
|
if per == "guild":
|
||||||
|
key = (command, ctx.guild)
|
||||||
|
elif per == "channel":
|
||||||
|
key = (command, ctx.guild, ctx.channel)
|
||||||
|
elif per == "member":
|
||||||
|
key = (command, ctx.guild, ctx.author)
|
||||||
|
else:
|
||||||
|
raise ValueError(per)
|
||||||
|
cooldown = self.cooldowns.get(key)
|
||||||
|
if cooldown:
|
||||||
|
cooldown += timedelta(seconds=rate)
|
||||||
|
if cooldown > now:
|
||||||
|
raise OnCooldown()
|
||||||
|
new_cooldowns[key] = now
|
||||||
|
# only update cooldowns if the command isn't on cooldown
|
||||||
|
self.cooldowns.update(new_cooldowns)
|
||||||
|
|
||||||
|
def transform_arg(self, result, attr, obj) -> str:
|
||||||
|
attr = attr[1:] # strip initial dot
|
||||||
|
if not attr:
|
||||||
|
return str(obj)
|
||||||
|
raw_result = "{" + result + "}"
|
||||||
|
# forbid private members and nested attr lookups
|
||||||
|
if attr.startswith("_") or "." in attr:
|
||||||
|
return raw_result
|
||||||
|
return str(getattr(obj, attr, raw_result))
|
||||||
|
|
||||||
|
def transform_parameter(self, 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
|
||||||
@@ -382,7 +541,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,67 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../customcom.py:44
|
|
||||||
msgid ""
|
|
||||||
"Welcome to the interactive random {} maker!\n"
|
|
||||||
"Every message you send will be added as one of the random response to choose from once this {} is triggered. To exit this interactive menu, type `{}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:56
|
|
||||||
msgid "Add a random response:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:119
|
|
||||||
msgid "Do you want to create a 'randomized' cc? {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:126
|
|
||||||
msgid "What response do you want?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:205 ../customcom.py:235
|
|
||||||
msgid "Custom command successfully added."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:207 ../customcom.py:237
|
|
||||||
msgid "This command already exists. Use `{}` to edit it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:229
|
|
||||||
msgid "That command is already a standard command."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:261
|
|
||||||
msgid "Custom command successfully edited."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:263
|
|
||||||
msgid "That command doesn't exist. Use `{}` to add it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:282
|
|
||||||
msgid "Custom command successfully deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:284
|
|
||||||
msgid "That command doesn't exist."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../customcom.py:294
|
|
||||||
msgid "There are no custom commands in this guild. Use `{}` to start adding some."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../customcom.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -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,60 +111,60 @@ 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))
|
||||||
ret = {}
|
ret = {}
|
||||||
for k, v in flatscoped.items():
|
for config_identifiers, v2data in flatscoped.items():
|
||||||
outerkey, innerkey = (*k[:-1],), (k[-1],)
|
if config_identifiers not in ret:
|
||||||
if outerkey not in ret:
|
ret[config_identifiers] = {}
|
||||||
ret[outerkey] = {}
|
ret[config_identifiers].update({("past_nicks",): v2data})
|
||||||
ret[outerkey].update({innerkey: v})
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def customcom_conv_spec(self, data: dict):
|
def customcom_conv_spec(self, data: dict):
|
||||||
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
|
||||||
|
|
||||||
async def convert(self, bot: Red, prettyname: str):
|
def get_config_object(self, bot, cogname, attr, _id):
|
||||||
if prettyname not in self.available:
|
|
||||||
raise NotImplementedError("No Conversion Specs for this")
|
|
||||||
|
|
||||||
info = self.available_core_conversions[prettyname]
|
|
||||||
filepath, converter = info['file'], info['converter']
|
|
||||||
(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):
|
||||||
config = Config.get_conf(None, _id, cog_name=cogname)
|
config = Config.get_conf(None, _id, cog_name=cogname)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def get_conversion_info(self, prettyname: str):
|
||||||
|
info = self.available_core_conversions[prettyname]
|
||||||
|
filepath, converter = info["file"], info["converter"]
|
||||||
|
(cogname, attr, _id) = info["cfg"]
|
||||||
|
return filepath, converter, cogname, attr, _id
|
||||||
|
|
||||||
|
async def convert(self, bot: Red, prettyname: str, config=None):
|
||||||
|
if prettyname not in self.available:
|
||||||
|
raise NotImplementedError("No Conversion Specs for this")
|
||||||
|
|
||||||
|
filepath, converter, cogname, attr, _id = self.get_conversion_info(prettyname)
|
||||||
|
if config is None:
|
||||||
|
config = self.get_config_object(bot, cogname, attr, _id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
items = converter(dc.json_load(filepath))
|
items = converter(dc.json_load(filepath))
|
||||||
await dc(config).dict_import(items)
|
await dc(config).dict_import(items)
|
||||||
|
|||||||
@@ -1,74 +1,63 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from discord.ext import commands
|
from redbot.core import checks, commands
|
||||||
|
|
||||||
from redbot.core import checks, RedContext
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import CogI18n
|
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
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = CogI18n('DataConverter', __file__)
|
_ = Translator("DataConverter", __file__)
|
||||||
|
|
||||||
|
|
||||||
class DataConverter:
|
@cog_i18n(_)
|
||||||
"""
|
class DataConverter(commands.Cog):
|
||||||
Cog for importing Red v2 Data
|
"""Import Red V2 data to your V3 instance."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.command(name="convertdata")
|
@commands.command(name="convertdata")
|
||||||
async def dataconversioncommand(self, ctx: RedContext, v2path: str):
|
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
|
||||||
"""
|
"""Interactive prompt for importing data from Red V2.
|
||||||
Interactive prompt for importing data from Red v2
|
|
||||||
|
|
||||||
Takes the path where the v2 install is
|
Takes the path where the V2 install is, and overwrites
|
||||||
|
values which have entries in both V2 and v3; use with caution.
|
||||||
Overwrites values which have entries in both v2 and v3,
|
|
||||||
use with caution.
|
|
||||||
"""
|
"""
|
||||||
resolver = SpecResolver(Path(v2path.strip()))
|
resolver = SpecResolver(Path(v2path.strip()))
|
||||||
|
|
||||||
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 "
|
_(
|
||||||
"handle here. Are you sure you gave me the base "
|
"There don't seem to be any data files I know how to "
|
||||||
"installation path?")
|
"handle here. Are you sure you gave me the base "
|
||||||
|
"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)
|
||||||
|
|
||||||
menu_message = await ctx.send(box(menu))
|
menu_message = await ctx.send(box(menu))
|
||||||
|
|
||||||
def pred(m):
|
|
||||||
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=MessagePredicate.same_context(ctx), timeout=60
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return await ctx.send(
|
return await ctx.send(_("Try this again when you are 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():
|
||||||
@@ -77,6 +66,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.\n"
|
||||||
|
"There might be more things I can convert in the future."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-03-12 04:35+EDT\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../dataconverter.py:38
|
|
||||||
msgid "There don't seem to be any data files I know how to handle here. Are you sure you gave me the base installation path?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../dataconverter.py:43
|
|
||||||
msgid "Please select a set of data to import by number, or 'exit' to exit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../dataconverter.py:59
|
|
||||||
msgid "Try this again when you are more ready"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../dataconverter.py:70
|
|
||||||
msgid "That wasn't a valid choice."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../dataconverter.py:76
|
|
||||||
msgid "{} converted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../dataconverter.py:80
|
|
||||||
msgid ""
|
|
||||||
"There isn't anything else I know how to convert here.\n"
|
|
||||||
"There might be more things I can convert in the future."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../dataconverter.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from redbot.core.bot import Red
|
|
||||||
from .downloader import Downloader
|
from .downloader import Downloader
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Red):
|
async def setup(bot):
|
||||||
bot.add_cog(Downloader(bot))
|
cog = Downloader(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
from redbot.core import commands
|
||||||
from discord.ext import commands
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
__all__ = ["install_agreement", ]
|
__all__ = ["do_install_agreement"]
|
||||||
|
|
||||||
REPO_INSTALL_MSG = (
|
T_ = Translator("DownloaderChecks", __file__)
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
|
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"
|
||||||
" and its community have no responsibility for any potential "
|
" and its community have no responsibility for any potential "
|
||||||
"damage that the content of 3rd party repositories might cause."
|
"damage that the content of 3rd party repositories might cause."
|
||||||
@@ -14,32 +18,23 @@ REPO_INSTALL_MSG = (
|
|||||||
"shown again until the next reboot.\n\nYou have **30** seconds"
|
"shown again until the next reboot.\n\nYou have **30** seconds"
|
||||||
" to reply to this message."
|
" to reply to this message."
|
||||||
)
|
)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
def install_agreement():
|
async def do_install_agreement(ctx: commands.Context):
|
||||||
async def pred(ctx: commands.Context):
|
downloader = ctx.cog
|
||||||
downloader = ctx.command.instance
|
if downloader is None or downloader.already_agreed:
|
||||||
if downloader is None:
|
|
||||||
return True
|
|
||||||
elif downloader.already_agreed:
|
|
||||||
return True
|
|
||||||
elif ctx.invoked_subcommand is None or \
|
|
||||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def does_agree(msg: discord.Message):
|
|
||||||
return ctx.author == msg.author and \
|
|
||||||
ctx.channel == msg.channel and \
|
|
||||||
msg.content == "I agree"
|
|
||||||
|
|
||||||
await ctx.send(REPO_INSTALL_MSG)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await ctx.bot.wait_for('message', check=does_agree, timeout=30)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await ctx.send("Your response has timed out, please try again.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
downloader.already_agreed = True
|
|
||||||
return True
|
return True
|
||||||
return commands.check(pred)
|
|
||||||
|
await ctx.send(T_(REPO_INSTALL_MSG))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.lower_equal_to("i agree", ctx), timeout=30
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send(_("Your response has timed out, please try again."))
|
||||||
|
return False
|
||||||
|
|
||||||
|
downloader.already_agreed = True
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from .repo_manager import RepoManager
|
|
||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
|
|
||||||
|
|
||||||
class InstalledCog(commands.Converter):
|
class InstalledCog(Installable):
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
@classmethod
|
||||||
|
async def convert(cls, 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:
|
||||||
raise commands.CommandError("Downloader not loaded.")
|
raise commands.CommandError(_("No Downloader cog found."))
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import path as syspath
|
from sys import path as syspath
|
||||||
from typing import Tuple, Union
|
from typing import Tuple, Union, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import sys
|
from redbot.core import checks, commands, Config
|
||||||
|
|
||||||
from redbot.core import Config
|
|
||||||
from redbot.core import checks
|
|
||||||
from redbot.core.data_manager import cog_data_path
|
|
||||||
from redbot.core.i18n import CogI18n
|
|
||||||
from redbot.core.utils.chat_formatting import box, pagify
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from .checks import install_agreement
|
from redbot.core.data_manager import cog_data_path
|
||||||
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.chat_formatting import box, pagify, humanize_list, inline
|
||||||
|
from redbot.core.utils.menus import start_adding_reactions
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
|
|
||||||
|
from . import errors
|
||||||
|
from .checks import do_install_agreement
|
||||||
from .converters import InstalledCog
|
from .converters import InstalledCog
|
||||||
from .errors import CloningError, ExistingGitRepo
|
|
||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
from .log import log
|
from .log import log
|
||||||
from .repo_manager import RepoManager, Repo
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
_ = CogI18n('Downloader', __file__)
|
_ = Translator("Downloader", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Downloader:
|
@cog_i18n(_)
|
||||||
|
class Downloader(commands.Cog):
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
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
|
||||||
|
|
||||||
@@ -45,13 +45,16 @@ 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 initialize(self):
|
||||||
|
await self._repo_manager.initialize()
|
||||||
|
|
||||||
async def cog_install_path(self):
|
async def cog_install_path(self):
|
||||||
"""Get the current cog install path.
|
"""Get the current cog install path.
|
||||||
@@ -109,7 +112,7 @@ class Downloader:
|
|||||||
installed.remove(cog_json)
|
installed.remove(cog_json)
|
||||||
await self.conf.installed.set(installed)
|
await self.conf.installed.set(installed)
|
||||||
|
|
||||||
async def _reinstall_cogs(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
async def _reinstall_cogs(self, cogs: Iterable[Installable]) -> Tuple[Installable]:
|
||||||
"""
|
"""
|
||||||
Installs a list of cogs, used when updating.
|
Installs a list of cogs, used when updating.
|
||||||
:param cogs:
|
:param cogs:
|
||||||
@@ -123,7 +126,7 @@ class Downloader:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(failed)
|
return tuple(failed)
|
||||||
|
|
||||||
async def _reinstall_libraries(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
async def _reinstall_libraries(self, cogs: Iterable[Installable]) -> Tuple[Installable]:
|
||||||
"""
|
"""
|
||||||
Reinstalls any shared libraries from the repos of cogs that
|
Reinstalls any shared libraries from the repos of cogs that
|
||||||
were updated.
|
were updated.
|
||||||
@@ -143,7 +146,7 @@ class Downloader:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(failed)
|
return tuple(failed)
|
||||||
|
|
||||||
async def _reinstall_requirements(self, cogs: Tuple[Installable]) -> bool:
|
async def _reinstall_requirements(self, cogs: Iterable[Installable]) -> bool:
|
||||||
"""
|
"""
|
||||||
Reinstalls requirements for given cogs that have been updated.
|
Reinstalls requirements for given cogs that have been updated.
|
||||||
Returns a bool that indicates if all requirement installations
|
Returns a bool that indicates if all requirement installations
|
||||||
@@ -169,7 +172,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
|
||||||
@@ -190,193 +193,272 @@ class Downloader:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def pipinstall(self, ctx, *deps: str):
|
async def pipinstall(self, ctx, *deps: str):
|
||||||
"""
|
"""Install a group of dependencies using pip."""
|
||||||
Installs a group of dependencies using pip.
|
|
||||||
"""
|
|
||||||
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
||||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||||
|
|
||||||
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()
|
||||||
async def repo(self, ctx):
|
async def repo(self, ctx):
|
||||||
"""
|
"""Repo management commands."""
|
||||||
Command group for managing Downloader repos.
|
pass
|
||||||
"""
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@repo.command(name="add")
|
@repo.command(name="add")
|
||||||
@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.
|
||||||
"""
|
|
||||||
Add a new repo to Downloader.
|
|
||||||
|
|
||||||
Name can only contain characters A-z, numbers and underscore
|
The name can only contain characters A-z, numbers and underscores.
|
||||||
Branch will default to master if not specified
|
The branch will be the default branch if not specified.
|
||||||
"""
|
"""
|
||||||
|
agreed = await do_install_agreement(ctx)
|
||||||
|
if not agreed:
|
||||||
|
return
|
||||||
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,
|
except errors.ExistingGitRepo:
|
||||||
url=repo_url,
|
|
||||||
branch=branch
|
|
||||||
)
|
|
||||||
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 errors.CloningError as err:
|
||||||
await ctx.send(_("Something went wrong during the cloning process."))
|
await ctx.send(_("Something went wrong during the cloning process."))
|
||||||
log.exception(_("Something went wrong during the cloning process."))
|
log.exception(
|
||||||
|
"Something went wrong whilst cloning %s (to revision: %s)",
|
||||||
|
repo_url,
|
||||||
|
branch,
|
||||||
|
exc_info=err,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
await ctx.send(_("Repo `{name}` successfully added.").format(name=name))
|
||||||
if repo.install_msg is not None:
|
if repo.install_msg is not None:
|
||||||
await ctx.send(repo.install_msg)
|
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@repo.command(name="delete")
|
@repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
|
||||||
async def _repo_del(self, ctx, repo_name: Repo):
|
async def _repo_del(self, ctx, repo: Repo):
|
||||||
"""
|
"""Remove a repo and its files."""
|
||||||
Removes a repo from Downloader and its' files.
|
await self._repo_manager.delete_repo(repo.name)
|
||||||
"""
|
|
||||||
await self._repo_manager.delete_repo(repo_name.name)
|
|
||||||
|
|
||||||
await ctx.send(_("The repo `{}` has been deleted successfully.").format(repo_name.name))
|
await ctx.send(
|
||||||
|
_("The repo `{repo.name}` has been deleted successfully.").format(repo=repo)
|
||||||
|
)
|
||||||
|
|
||||||
@repo.command(name="list")
|
@repo.command(name="list")
|
||||||
async def _repo_list(self, ctx):
|
async def _repo_list(self, ctx):
|
||||||
"""
|
"""List all installed repos."""
|
||||||
Lists all installed repos.
|
|
||||||
"""
|
|
||||||
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", usage="<repo_name>")
|
||||||
|
async def _repo_info(self, ctx, repo: Repo):
|
||||||
|
"""Show information about a repo."""
|
||||||
|
if repo is None:
|
||||||
|
await ctx.send(_("Repo `{repo.name}` not found.").format(repo=repo))
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = _("Information on {repo.name}:\n{description}").format(
|
||||||
|
repo=repo, description=repo.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):
|
||||||
"""
|
"""Cog installation management commands."""
|
||||||
Command group for managing installable Cogs.
|
pass
|
||||||
"""
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@cog.command(name="install")
|
@cog.command(name="install", usage="<repo_name> <cog_name>")
|
||||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
async def _cog_install(self, ctx, repo: Repo, cog_name: str):
|
||||||
"""
|
"""Install a cog from the given repo."""
|
||||||
Installs a cog from the given repo.
|
cog: Installable = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
"""
|
|
||||||
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 `{cog_name}` in the `{repo.name}` repo."
|
||||||
|
).format(cog_name=cog_name, repo=repo)
|
||||||
|
)
|
||||||
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 {version}, aborting install.").format(
|
||||||
'.'.join([str(n) for n in cog.min_python_version])
|
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.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 `{cog_name}`: `{libraries}`"
|
||||||
|
).format(cog_name=cog.name, libraries=cog.requirements)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await repo_name.install_cog(cog, await self.cog_install_path())
|
await repo.install_cog(cog, await self.cog_install_path())
|
||||||
|
|
||||||
await self._add_to_installed(cog)
|
await self._add_to_installed(cog)
|
||||||
|
|
||||||
await repo_name.install_libraries(self.SHAREDLIB_PATH)
|
await repo.install_libraries(self.SHAREDLIB_PATH)
|
||||||
|
|
||||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name))
|
||||||
if cog.install_msg is not None:
|
if cog.install_msg is not None:
|
||||||
await ctx.send(cog.install_msg)
|
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@cog.command(name="uninstall")
|
@cog.command(name="uninstall", usage="<cog_name>")
|
||||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
async def _cog_uninstall(self, ctx, cog: InstalledCog):
|
||||||
"""
|
"""Uninstall a cog.
|
||||||
Allows you to uninstall cogs that were previously installed
|
|
||||||
through Downloader.
|
You may only uninstall cogs which were previously installed
|
||||||
|
by Downloader.
|
||||||
"""
|
"""
|
||||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||||
real_name = cog_name.name
|
real_name = cog.name
|
||||||
|
|
||||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||||
if poss_installed_path.exists():
|
if poss_installed_path.exists():
|
||||||
await self._delete_cog(poss_installed_path)
|
await self._delete_cog(poss_installed_path)
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._remove_from_installed(cog_name)
|
await self._remove_from_installed(cog)
|
||||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
await ctx.send(
|
||||||
|
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That cog was installed but can no longer"
|
await ctx.send(
|
||||||
" be located. You may need to remove it's"
|
_(
|
||||||
" files manually if it is still usable."))
|
"That cog was installed but can no longer"
|
||||||
|
" be located. You may need to remove it's"
|
||||||
|
" files manually if it is still usable."
|
||||||
|
" Also make sure you've unloaded the cog"
|
||||||
|
" with `{prefix}unload {cog_name}`."
|
||||||
|
).format(cog_name=real_name)
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
||||||
"""
|
"""Update all cogs, or one of your choosing."""
|
||||||
Updates all cogs or one of your choosing.
|
|
||||||
"""
|
|
||||||
installed_cogs = set(await self.installed_cogs())
|
installed_cogs = set(await self.installed_cogs())
|
||||||
|
|
||||||
if cog_name is None:
|
async with ctx.typing():
|
||||||
updated = await self._repo_manager.update_all_repos()
|
if cog_name is None:
|
||||||
|
updated = await self._repo_manager.update_all_repos()
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
updated = await self._repo_manager.update_repo(cog_name.repo_name)
|
||||||
|
except KeyError:
|
||||||
|
# Thrown if the repo no longer exists
|
||||||
|
updated = {}
|
||||||
|
|
||||||
|
updated_cogs = set(cog for repo in updated for cog in repo.available_cogs)
|
||||||
|
installed_and_updated = updated_cogs & installed_cogs
|
||||||
|
|
||||||
|
if installed_and_updated:
|
||||||
|
await self._reinstall_requirements(installed_and_updated)
|
||||||
|
await self._reinstall_cogs(installed_and_updated)
|
||||||
|
await self._reinstall_libraries(installed_and_updated)
|
||||||
|
message = _("Cog update completed successfully.")
|
||||||
|
|
||||||
|
cognames = [c.name for c in installed_and_updated]
|
||||||
|
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames)))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("All installed cogs are already up to date."))
|
||||||
|
return
|
||||||
|
await ctx.send(message)
|
||||||
|
|
||||||
|
message = _("Would you like to reload the updated cogs?")
|
||||||
|
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
|
||||||
|
if not can_react:
|
||||||
|
message += " (y/n)"
|
||||||
|
query: discord.Message = await ctx.send(message)
|
||||||
|
if can_react:
|
||||||
|
# noinspection PyAsyncCall
|
||||||
|
start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
|
||||||
|
pred = ReactionPredicate.yes_or_no(query, ctx.author)
|
||||||
|
event = "reaction_add"
|
||||||
else:
|
else:
|
||||||
try:
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
updated = await self._repo_manager.update_repo(cog_name.repo_name)
|
event = "message"
|
||||||
except KeyError:
|
try:
|
||||||
# Thrown if the repo no longer exists
|
await ctx.bot.wait_for(event, check=pred, timeout=30)
|
||||||
updated = {}
|
except asyncio.TimeoutError:
|
||||||
|
await query.delete()
|
||||||
updated_cogs = set(cog for repo in updated.keys() for cog in repo.available_cogs)
|
|
||||||
installed_and_updated = updated_cogs & installed_cogs
|
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._reinstall_requirements(installed_and_updated)
|
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._reinstall_cogs(installed_and_updated)
|
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._reinstall_libraries(installed_and_updated)
|
|
||||||
await ctx.send(_("Cog update completed successfully."))
|
|
||||||
|
|
||||||
@cog.command(name="list")
|
|
||||||
async def _cog_list(self, ctx, repo_name: Repo):
|
|
||||||
"""
|
|
||||||
Lists all available cogs from a single repo.
|
|
||||||
"""
|
|
||||||
cogs = repo_name.available_cogs
|
|
||||||
cogs = _("Available Cogs:\n") + "\n".join(
|
|
||||||
["+ {}: {}".format(c.name, c.short or "") for c in cogs])
|
|
||||||
|
|
||||||
await ctx.send(box(cogs, lang="diff"))
|
|
||||||
|
|
||||||
@cog.command(name="info")
|
|
||||||
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
|
||||||
"""
|
|
||||||
Lists information about a single cog.
|
|
||||||
"""
|
|
||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
|
||||||
if cog is None:
|
|
||||||
await ctx.send(_("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 "")
|
if pred.result is True:
|
||||||
|
if can_react:
|
||||||
|
with contextlib.suppress(discord.Forbidden):
|
||||||
|
await query.clear_reactions()
|
||||||
|
|
||||||
|
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||||
|
else:
|
||||||
|
if can_react:
|
||||||
|
await query.delete()
|
||||||
|
else:
|
||||||
|
await ctx.send(_("OK then."))
|
||||||
|
|
||||||
|
@cog.command(name="list", usage="<repo_name>")
|
||||||
|
async def _cog_list(self, ctx, repo: Repo):
|
||||||
|
"""List all available cogs from a single repo."""
|
||||||
|
installed = await self.installed_cogs()
|
||||||
|
installed_str = ""
|
||||||
|
if installed:
|
||||||
|
installed_str = _("Installed Cogs:\n") + "\n".join(
|
||||||
|
[
|
||||||
|
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
||||||
|
for i in installed
|
||||||
|
if i.repo_name == repo.name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
cogs = repo.available_cogs
|
||||||
|
cogs = _("Available Cogs:\n") + "\n".join(
|
||||||
|
[
|
||||||
|
"+ {}: {}".format(c.name, c.short or "")
|
||||||
|
for c in cogs
|
||||||
|
if not (c.hidden or c in installed)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
cogs = cogs + "\n\n" + installed_str
|
||||||
|
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||||
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
|
@cog.command(name="info", usage="<repo_name> <cog_name>")
|
||||||
|
async def _cog_info(self, ctx, repo: Repo, cog_name: str):
|
||||||
|
"""List information about a single cog."""
|
||||||
|
cog = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
|
if cog is None:
|
||||||
|
await ctx.send(
|
||||||
|
_("There is no cog `{cog_name}` in the repo `{repo.name}`").format(
|
||||||
|
cog_name=cog_name, repo=repo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = _(
|
||||||
|
"Information on {cog_name}:\n{description}\n\nRequirements: {requirements}"
|
||||||
|
).format(
|
||||||
|
cog_name=cog.name,
|
||||||
|
description=cog.description or "",
|
||||||
|
requirements=", ".join(cog.requirements) or "None",
|
||||||
|
)
|
||||||
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
|
||||||
@@ -396,8 +478,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
|
||||||
@@ -420,12 +503,12 @@ class Downloader:
|
|||||||
cog_name = cog_installable.name
|
cog_name = cog_installable.name
|
||||||
else:
|
else:
|
||||||
made_by = "26 & co."
|
made_by = "26 & co."
|
||||||
repo_url = "https://github.com/Twentysix26/Red-DiscordBot"
|
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
||||||
cog_name = cog_installable.__class__.__name__
|
cog_name = cog_installable.__class__.__name__
|
||||||
|
|
||||||
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
|
msg = _("Command: {command}\nMade by: {author}\nRepo: {repo}\nCog name: {cog}")
|
||||||
|
|
||||||
return msg.format(command_name, made_by, repo_url, cog_name)
|
return msg.format(command=command_name, author=made_by, repo=repo_url, cog=cog_name)
|
||||||
|
|
||||||
def cog_name_from_instance(self, instance: object) -> str:
|
def cog_name_from_instance(self, instance: object) -> str:
|
||||||
"""Determines the cog name that Downloader knows from the cog instance.
|
"""Determines the cog name that Downloader knows from the cog instance.
|
||||||
@@ -443,14 +526,14 @@ 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()
|
||||||
async def findcog(self, ctx: commands.Context, command_name: str):
|
async def findcog(self, ctx: commands.Context, command_name: str):
|
||||||
"""
|
"""Find which cog a command comes from.
|
||||||
Figures out which cog a command comes from. Only works with loaded
|
|
||||||
cogs.
|
This will only work with loaded cogs.
|
||||||
"""
|
"""
|
||||||
command = ctx.bot.all_commands.get(command_name)
|
command = ctx.bot.all_commands.get(command_name)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo",
|
__all__ = [
|
||||||
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError",
|
"DownloaderException",
|
||||||
"UpdateError", "GitDiffError", "PipError"]
|
"GitException",
|
||||||
|
"InvalidRepoName",
|
||||||
|
"ExistingGitRepo",
|
||||||
|
"MissingGitRepo",
|
||||||
|
"CloningError",
|
||||||
|
"CurrentHashError",
|
||||||
|
"HardResetError",
|
||||||
|
"UpdateError",
|
||||||
|
"GitDiffError",
|
||||||
|
"NoRemoteURL",
|
||||||
|
"PipError",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DownloaderException(Exception):
|
class DownloaderException(Exception):
|
||||||
"""
|
"""
|
||||||
Base class for Downloader exceptions.
|
Base class for Downloader exceptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +33,7 @@ class InvalidRepoName(DownloaderException):
|
|||||||
Throw when a repo name is invalid. Check
|
Throw when a repo name is invalid. Check
|
||||||
the message for a more detailed reason.
|
the message for a more detailed reason.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +42,7 @@ class ExistingGitRepo(DownloaderException):
|
|||||||
Thrown when trying to clone into a folder where a
|
Thrown when trying to clone into a folder where a
|
||||||
git repo already exists.
|
git repo already exists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +51,7 @@ class MissingGitRepo(DownloaderException):
|
|||||||
Thrown when a git repo is expected to exist but
|
Thrown when a git repo is expected to exist but
|
||||||
does not.
|
does not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +59,7 @@ class CloningError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when git clone returns a non zero exit code.
|
Thrown when git clone returns a non zero exit code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -52,6 +68,7 @@ class CurrentHashError(GitException):
|
|||||||
Thrown when git returns a non zero exit code attempting
|
Thrown when git returns a non zero exit code attempting
|
||||||
to determine the current commit hash.
|
to determine the current commit hash.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +77,7 @@ class HardResetError(GitException):
|
|||||||
Thrown when there is an issue trying to execute a hard reset
|
Thrown when there is an issue trying to execute a hard reset
|
||||||
(usually prior to a repo update).
|
(usually prior to a repo update).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +85,7 @@ class UpdateError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when git pull returns a non zero error code.
|
Thrown when git pull returns a non zero error code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +93,15 @@ class GitDiffError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when a git diff fails.
|
Thrown when a git diff fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoRemoteURL(GitException):
|
||||||
|
"""
|
||||||
|
Thrown when no remote URL exists for a repo.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -81,4 +109,5 @@ class PipError(DownloaderException):
|
|||||||
"""
|
"""
|
||||||
Thrown when pip returns a non-zero return code.
|
Thrown when pip returns a non-zero return code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import distutils.dir_util
|
|||||||
import shutil
|
import shutil
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import MutableMapping, Any
|
from typing import MutableMapping, Any, TYPE_CHECKING
|
||||||
|
|
||||||
from redbot.core.utils import TYPE_CHECKING
|
|
||||||
from .log import log
|
from .log import log
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
|
|
||||||
@@ -56,6 +55,7 @@ class Installable(RepoJSONMixin):
|
|||||||
:class:`InstallationType`.
|
:class:`InstallationType`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, location: Path):
|
def __init__(self, location: Path):
|
||||||
"""Base installable initializer.
|
"""Base installable initializer.
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ class Installable(RepoJSONMixin):
|
|||||||
self.bot_version = (3, 0, 0)
|
self.bot_version = (3, 0, 0)
|
||||||
self.min_python_version = (3, 5, 1)
|
self.min_python_version = (3, 5, 1)
|
||||||
self.hidden = False
|
self.hidden = False
|
||||||
|
self.disabled = False
|
||||||
self.required_cogs = {} # Cog name -> repo URL
|
self.required_cogs = {} # Cog name -> repo URL
|
||||||
self.requirements = ()
|
self.requirements = ()
|
||||||
self.tags = ()
|
self.tags = ()
|
||||||
@@ -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,12 @@ 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("Invalid JSON information file at path: {}".format(info_file_path))
|
||||||
" {}".format(info_file_path))
|
|
||||||
else:
|
else:
|
||||||
self._info = info
|
self._info = info
|
||||||
|
|
||||||
@@ -167,7 +163,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
|
||||||
@@ -178,6 +174,12 @@ class Installable(RepoJSONMixin):
|
|||||||
hidden = False
|
hidden = False
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
|
||||||
|
try:
|
||||||
|
disabled = bool(info.get("disabled", False))
|
||||||
|
except ValueError:
|
||||||
|
disabled = False
|
||||||
|
self.disabled = disabled
|
||||||
|
|
||||||
self.required_cogs = info.get("required_cogs", {})
|
self.required_cogs = info.get("required_cogs", {})
|
||||||
|
|
||||||
self.requirements = info.get("requirements", ())
|
self.requirements = info.get("requirements", ())
|
||||||
@@ -200,15 +202,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,93 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../downloader.py:215
|
|
||||||
msgid "That git repo has already been added under another name."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:217 ../downloader.py:218
|
|
||||||
msgid "Something went wrong during the cloning process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:220
|
|
||||||
msgid "Repo `{}` successfully added."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:229
|
|
||||||
msgid "The repo `{}` has been deleted successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:237
|
|
||||||
msgid ""
|
|
||||||
"Installed Repos:\n"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:258
|
|
||||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:263
|
|
||||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:273
|
|
||||||
msgid "`{}` cog successfully installed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:289
|
|
||||||
msgid "`{}` was successfully removed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:291
|
|
||||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:315
|
|
||||||
msgid "Cog update completed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:323
|
|
||||||
msgid ""
|
|
||||||
"Available Cogs:\n"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:335
|
|
||||||
msgid "There is no cog `{}` in the repo `{}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:340
|
|
||||||
msgid ""
|
|
||||||
"Information on {}:\n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:381
|
|
||||||
msgid "Missing from info.json"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:390
|
|
||||||
msgid ""
|
|
||||||
"Command: {}\n"
|
|
||||||
"Made by: {}\n"
|
|
||||||
"Repo: {}\n"
|
|
||||||
"Cog name: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../downloader.py:422
|
|
||||||
msgid "That command doesn't seem to exist."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../downloader.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -3,40 +3,47 @@ import functools
|
|||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import shutil
|
import shutil
|
||||||
|
import re
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import run as sp_run, PIPE
|
from subprocess import run as sp_run, PIPE
|
||||||
from sys import executable
|
from sys import executable
|
||||||
from typing import Tuple, MutableMapping, Union
|
from typing import Tuple, MutableMapping, Union, Optional
|
||||||
|
|
||||||
from discord.ext import commands
|
from redbot.core import data_manager, commands
|
||||||
|
|
||||||
from redbot.core import Config
|
|
||||||
from redbot.core import data_manager
|
|
||||||
from redbot.core.utils import safe_delete
|
from redbot.core.utils import safe_delete
|
||||||
from .errors import *
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
from . import errors
|
||||||
from .installable import Installable, InstallableType
|
from .installable import Installable, InstallableType
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
from .log import log
|
from .log import log
|
||||||
|
|
||||||
|
_ = Translator("RepoManager", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Repo(RepoJSONMixin):
|
class Repo(RepoJSONMixin):
|
||||||
GIT_CLONE = "git clone -b {branch} {url} {folder}"
|
GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}"
|
||||||
GIT_CLONE_NO_BRANCH = "git clone {url} {folder}"
|
GIT_CLONE_NO_BRANCH = "git clone --recurse-submodules {url} {folder}"
|
||||||
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
||||||
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 --recurse-submodules -q --ff-only"
|
||||||
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status"
|
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
||||||
" {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
|
||||||
|
|
||||||
@@ -61,21 +68,24 @@ class Repo(RepoJSONMixin):
|
|||||||
async def convert(cls, ctx: commands.Context, argument: str):
|
async def convert(cls, ctx: commands.Context, argument: str):
|
||||||
downloader_cog = ctx.bot.get_cog("Downloader")
|
downloader_cog = ctx.bot.get_cog("Downloader")
|
||||||
if downloader_cog is None:
|
if downloader_cog is None:
|
||||||
raise commands.CommandError("No Downloader cog found.")
|
raise commands.CommandError(_("No Downloader cog found."))
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
repo_manager = downloader_cog._repo_manager
|
repo_manager = downloader_cog._repo_manager
|
||||||
poss_repo = repo_manager.get_repo(argument)
|
poss_repo = repo_manager.get_repo(argument)
|
||||||
if poss_repo is None:
|
if poss_repo is None:
|
||||||
raise commands.BadArgument("Repo by the name {} does not exist.".format(argument))
|
raise commands.BadArgument(
|
||||||
|
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
|
||||||
|
)
|
||||||
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 +95,27 @@ 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 errors.GitDiffError(
|
||||||
" {}".format(self.folder_path))
|
"Git diff failed for repo at 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 +127,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 errors.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 +156,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 +168,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,28 +186,23 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, path = self._existing_git_repo()
|
exists, path = self._existing_git_repo()
|
||||||
if exists:
|
if exists:
|
||||||
raise ExistingGitRepo(
|
raise errors.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:
|
||||||
raise CloningError("Error when running git clone.")
|
# Try cleaning up folder
|
||||||
|
shutil.rmtree(str(self.folder_path), ignore_errors=True)
|
||||||
|
raise errors.CloningError("Error when running git clone.")
|
||||||
|
|
||||||
if self.branch is None:
|
if self.branch is None:
|
||||||
self.branch = await self.current_branch()
|
self.branch = await self.current_branch()
|
||||||
@@ -217,23 +222,20 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise errors.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 errors.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,23 +254,20 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise errors.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:
|
||||||
raise CurrentHashError("Unable to determine old commit hash.")
|
raise errors.CurrentHashError("Unable to determine old commit hash.")
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -284,24 +283,21 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
RuntimeError
|
.NoRemoteURL
|
||||||
When the folder does not contain a git repo with a FETCH URL.
|
When the folder does not contain a git repo with a FETCH URL.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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 errors.NoRemoteURL("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 +311,20 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo(
|
raise errors.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 errors.HardResetError(
|
||||||
" execute a hard reset on the repo at"
|
"Some error occurred when trying to"
|
||||||
" the following path: {}".format(self.folder_path))
|
" execute a hard reset on the repo at"
|
||||||
|
" 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 +340,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 errors.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)
|
||||||
|
|
||||||
@@ -379,7 +372,7 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if cog not in self.available_cogs:
|
if cog not in self.available_cogs:
|
||||||
raise DownloaderException("That cog does not exist in this repo")
|
raise errors.DownloaderException("That cog does not exist in this repo")
|
||||||
|
|
||||||
if not target_dir.is_dir():
|
if not target_dir.is_dir():
|
||||||
raise ValueError("That target directory is not actually a directory.")
|
raise ValueError("That target directory is not actually a directory.")
|
||||||
@@ -389,7 +382,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 +464,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(
|
||||||
" the following requirements:"
|
"Something went wrong when installing"
|
||||||
" {}".format(", ".join(requirements)))
|
" the following requirements:"
|
||||||
|
" {}".format(", ".join(requirements))
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -490,8 +485,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.disabled]
|
||||||
if m.type == InstallableType.COG and not m.hidden]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -501,8 +495,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,18 +508,23 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
|
|
||||||
class RepoManager:
|
class RepoManager:
|
||||||
def __init__(self, downloader_config: Config):
|
|
||||||
self.downloader_config = downloader_config
|
|
||||||
|
|
||||||
|
GITHUB_OR_GITLAB_RE = re.compile("https?://git(?:hub)|(?:lab)\.com/")
|
||||||
|
TREE_URL_RE = re.compile(r"(?P<tree>/tree)/(?P<branch>\S+)$")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
self._repos = {}
|
self._repos = {}
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self._load_repos(set=True)
|
||||||
|
|
||||||
@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
|
||||||
@@ -534,10 +532,10 @@ class RepoManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_and_normalize_repo_name(name: str) -> str:
|
def validate_and_normalize_repo_name(name: str) -> str:
|
||||||
if not name.isidentifier():
|
if not name.isidentifier():
|
||||||
raise InvalidRepoName("Not a valid Python variable name.")
|
raise errors.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: Optional[str] = None) -> Repo:
|
||||||
"""Add and clone a git repository.
|
"""Add and clone a git repository.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -556,14 +554,14 @@ class RepoManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if self.does_repo_exist(name):
|
if self.does_repo_exist(name):
|
||||||
raise InvalidRepoName(
|
raise errors.ExistingGitRepo(
|
||||||
"That repo name you provided already exists."
|
"That repo name you provided already exists. Please choose another."
|
||||||
" Please choose another."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
url, branch = self._parse_url(url, branch)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -607,13 +605,13 @@ class RepoManager:
|
|||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
MissingGitRepo
|
.MissingGitRepo
|
||||||
If the repo does not exist.
|
If the repo does not exist.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
repo = self.get_repo(name)
|
repo = self.get_repo(name)
|
||||||
if repo is None:
|
if repo is None:
|
||||||
raise MissingGitRepo("There is no repo with the name {}".format(name))
|
raise errors.MissingGitRepo("There is no repo with the name {}".format(name))
|
||||||
|
|
||||||
safe_delete(repo.folder_path)
|
safe_delete(repo.folder_path)
|
||||||
|
|
||||||
@@ -652,10 +650,26 @@ class RepoManager:
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
ret[folder.stem] = await Repo.from_folder(folder)
|
ret[folder.stem] = await Repo.from_folder(folder)
|
||||||
except RuntimeError:
|
except errors.NoRemoteURL:
|
||||||
# Thrown when there's no findable git remote URL
|
log.warning("A remote URL does not exist for repo %s", folder.stem)
|
||||||
pass
|
except errors.DownloaderException as err:
|
||||||
|
log.error("Discarding repo %s due to error.", folder.stem, exc_info=err)
|
||||||
|
shutil.rmtree(
|
||||||
|
str(folder),
|
||||||
|
onerror=lambda func, path, exc: log.error(
|
||||||
|
"Failed to remove folder %s", path, exc_info=exc
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if set:
|
if set:
|
||||||
self._repos = ret
|
self._repos = ret
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def _parse_url(self, url: str, branch: Optional[str]) -> Tuple[str, Optional[str]]:
|
||||||
|
if self.GITHUB_OR_GITLAB_RE.match(url):
|
||||||
|
tree_url_match = self.TREE_URL_RE.search(url)
|
||||||
|
if tree_url_match:
|
||||||
|
url = url[: tree_url_match.start("tree")]
|
||||||
|
if branch is None:
|
||||||
|
branch = tree_url_match["branch"]
|
||||||
|
return url, branch
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import cast, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||||
from redbot.core import Config, bank
|
from redbot.core import Config, bank, commands
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import pagify, box
|
from redbot.core.utils.chat_formatting import box
|
||||||
from discord.ext import commands
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
_ = CogI18n("Economy", __file__)
|
T_ = Translator("Economy", __file__)
|
||||||
|
|
||||||
logger = logging.getLogger("red.economy")
|
logger = logging.getLogger("red.economy")
|
||||||
|
|
||||||
@@ -34,45 +35,46 @@ class SMReel(Enum):
|
|||||||
snowflake = "\N{SNOWFLAKE}"
|
snowflake = "\N{SNOWFLAKE}"
|
||||||
|
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
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 = _(
|
||||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
"Slot machine payouts:\n"
|
||||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||||
"{two.value} {six.value} Bet * 4\n"
|
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||||
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
"{two.value} {six.value} Bet * 4\n"
|
||||||
"Three symbols: +500\n"
|
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
||||||
"Two symbols: Bet * 2").format(**SMReel.__dict__)
|
"Three symbols: +500\n"
|
||||||
|
"Two symbols: Bet * 2"
|
||||||
|
).format(**SMReel.__dict__)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
def guild_only_check():
|
def guild_only_check():
|
||||||
@@ -83,6 +85,7 @@ def guild_only_check():
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
@@ -104,10 +107,9 @@ class SetParser:
|
|||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
|
|
||||||
class Economy:
|
@cog_i18n(_)
|
||||||
"""Economy
|
class Economy(commands.Cog):
|
||||||
|
"""Get rich and have fun with imaginary currency!"""
|
||||||
Get rich and have fun with imaginary currency!"""
|
|
||||||
|
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
"PAYDAY_TIME": 300,
|
"PAYDAY_TIME": 300,
|
||||||
@@ -115,23 +117,19 @@ 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
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.file_path = "data/economy/settings.json"
|
self.file_path = "data/economy/settings.json"
|
||||||
self.config = Config.get_conf(self, 1256844281)
|
self.config = Config.get_conf(self, 1256844281)
|
||||||
@@ -142,15 +140,15 @@ class Economy:
|
|||||||
self.config.register_role(**self.default_role_settings)
|
self.config.register_role(**self.default_role_settings)
|
||||||
self.slot_register = defaultdict(dict)
|
self.slot_register = defaultdict(dict)
|
||||||
|
|
||||||
|
@guild_only_check()
|
||||||
@commands.group(name="bank")
|
@commands.group(name="bank")
|
||||||
async def _bank(self, ctx: commands.Context):
|
async def _bank(self, ctx: commands.Context):
|
||||||
"""Bank operations"""
|
"""Manage the bank."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||||
"""Shows balance of user.
|
"""Show the user's account balance.
|
||||||
|
|
||||||
Defaults to yours."""
|
Defaults to yours."""
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -159,79 +157,101 @@ 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(
|
||||||
user.display_name, bal, currency))
|
_("{user}'s balance is {num} {currency}").format(
|
||||||
|
user=user.display_name, num=bal, currency=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):
|
||||||
"""Transfer currency to other users"""
|
"""Transfer currency to other users."""
|
||||||
from_ = ctx.author
|
from_ = ctx.author
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
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(
|
||||||
from_.display_name, amount, currency, to.display_name
|
_("{user} transferred {num} {currency} to {other_user}").format(
|
||||||
))
|
user=from_.display_name, num=amount, currency=currency, other_user=to.display_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@_bank.command(name="set")
|
@_bank.command(name="set")
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
|
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
|
||||||
"""Sets balance of user's bank account. See help for more operations
|
"""Set the balance of user's bank account.
|
||||||
|
|
||||||
Passing positive and negative values will add/remove currency instead
|
Passing positive and negative values will add/remove currency instead.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bank set @Twentysix 26 - Sets balance to 26
|
- `[p]bank set @Twentysix 26` - Sets balance to 26
|
||||||
bank set @Twentysix +2 - Increases balance by 2
|
- `[p]bank set @Twentysix +2` - Increases balance by 2
|
||||||
bank set @Twentysix -6 - Decreases balance by 6"""
|
- `[p]bank set @Twentysix -6` - Decreases balance by 6
|
||||||
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
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(
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
_("{author} added {num} {currency} to {user}'s account.").format(
|
||||||
))
|
author=author.display_name,
|
||||||
|
num=creds.sum,
|
||||||
|
currency=currency,
|
||||||
|
user=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(
|
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
await bank.set_balance(to, creds.sum)
|
|
||||||
await ctx.send(_("{} set {}'s account to {} {}.").format(
|
|
||||||
author.display_name, to.display_name, creds.sum, currency
|
|
||||||
))
|
|
||||||
|
|
||||||
@_bank.command()
|
|
||||||
@guild_only_check()
|
|
||||||
@check_global_setting_guildowner()
|
|
||||||
async def reset(self, ctx, confirmation: bool = False):
|
|
||||||
"""Deletes bank accounts"""
|
|
||||||
if confirmation is False:
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This will delete all bank accounts for {}.\nIf you're sure, type "
|
_("{author} removed {num} {currency} from {user}'s account.").format(
|
||||||
"`{}bank reset yes`").format(
|
author=author.display_name,
|
||||||
self.bot.user.name if await bank.is_global() else "this server",
|
num=creds.sum,
|
||||||
ctx.prefix
|
currency=currency,
|
||||||
|
user=to.display_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.wipe_bank()
|
await bank.set_balance(to, creds.sum)
|
||||||
await ctx.send(_("All bank accounts for {} have been "
|
await ctx.send(
|
||||||
"deleted.").format(
|
_("{author} set {users}'s account balance to {num} {currency}.").format(
|
||||||
self.bot.user.name if await bank.is_global() else "this server"
|
author=author.display_name,
|
||||||
)
|
num=creds.sum,
|
||||||
)
|
currency=currency,
|
||||||
|
user=to.display_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@_bank.command()
|
||||||
|
@check_global_setting_guildowner()
|
||||||
|
async def reset(self, ctx, confirmation: bool = False):
|
||||||
|
"""Delete all bank accounts."""
|
||||||
|
if confirmation is False:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"This will delete all bank accounts for {scope}.\nIf you're sure, type "
|
||||||
|
"`{prefix}bank reset yes`"
|
||||||
|
).format(
|
||||||
|
scope=self.bot.user.name if await bank.is_global() else _("this server"),
|
||||||
|
prefix=ctx.prefix,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await bank.wipe_bank(guild=ctx.guild)
|
||||||
|
await ctx.send(
|
||||||
|
_("All bank accounts for {scope} have been deleted.").format(
|
||||||
|
scope=self.bot.user.name if await bank.is_global() else _("this server")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
|
@commands.command()
|
||||||
async def payday(self, ctx: commands.Context):
|
async def payday(self, ctx: commands.Context):
|
||||||
"""Get some free currency"""
|
"""Get some free currency."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
@@ -245,90 +265,114 @@ 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"
|
_(
|
||||||
"You currently have {3} {1}.\n\n"
|
"{author.mention} Here, take some {currency}. "
|
||||||
"You are currently #{4} on the leaderboard!"
|
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||||
).format(
|
"You currently have {new_balance} {currency}.\n\n"
|
||||||
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
|
"You are currently #{pos} on the global leaderboard!"
|
||||||
str(await bank.get_balance(author)), pos
|
).format(
|
||||||
))
|
author=author,
|
||||||
|
currency=credits_name,
|
||||||
|
amount=await self.config.PAYDAY_CREDITS(),
|
||||||
|
new_balance=await bank.get_balance(author),
|
||||||
|
pos=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"
|
_(
|
||||||
" wait {}.").format(author.mention, dtime)
|
"{author.mention} Too soon. For your next payday you have to wait {time}."
|
||||||
|
).format(author=author, time=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"
|
_(
|
||||||
"You currently have {3} {1}.\n\n"
|
"{author.mention} Here, take some {currency}. "
|
||||||
"You are currently #{4} on the leaderboard!"
|
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||||
).format(
|
"You currently have {new_balance} {currency}.\n\n"
|
||||||
author, credits_name, credit_amount,
|
"You are currently #{pos} on the global leaderboard!"
|
||||||
str(await bank.get_balance(author)), pos
|
).format(
|
||||||
))
|
author=author,
|
||||||
|
currency=credits_name,
|
||||||
|
amount=credit_amount,
|
||||||
|
new_balance=await bank.get_balance(author),
|
||||||
|
pos=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"
|
_(
|
||||||
" wait {}.").format(author.mention, dtime))
|
"{author.mention} Too soon. For your next payday you have to wait {time}."
|
||||||
|
).format(author=author, time=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
|
"""Print the leaderboard.
|
||||||
|
|
||||||
Defaults to top 10"""
|
Defaults to top 10.
|
||||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
author = ctx.author
|
||||||
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:
|
header = "{pound:4}{name:36}{score:2}\n".format(
|
||||||
top = len(bank_sorted)
|
pound="#", name=_("Name"), score=_("Score")
|
||||||
highscore = ""
|
)
|
||||||
for pos, acc in enumerate(bank_sorted, 1):
|
highscores = [
|
||||||
pos = pos
|
(
|
||||||
poswidth = 2
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||||
name = acc[1]["name"]
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
namewidth = 35
|
|
||||||
balance = acc[1]["balance"]
|
|
||||||
balwidth = 2
|
|
||||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
|
||||||
pos=pos, poswidth=poswidth, name=name, namewidth=namewidth,
|
|
||||||
balance=balance, balwidth=balwidth
|
|
||||||
)
|
)
|
||||||
if highscore != "":
|
if acc[0] != author.id
|
||||||
for page in pagify(highscore, shorten_by=12):
|
else (
|
||||||
await ctx.send(box(page, lang="py"))
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
||||||
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
|
)
|
||||||
|
for pos, acc in enumerate(bank_sorted, 1)
|
||||||
|
]
|
||||||
|
if highscores:
|
||||||
|
pages = [
|
||||||
|
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
||||||
|
for x in range(0, len(highscores), 10)
|
||||||
|
]
|
||||||
|
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no accounts in the bank."))
|
await ctx.send(_("There are no accounts in the bank."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def payouts(self, ctx: commands.Context):
|
async def payouts(self, ctx: commands.Context):
|
||||||
"""Shows slot machine payouts"""
|
"""Show the payouts for the slot machine."""
|
||||||
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
await ctx.author.send(SLOT_PAYOUTS_MSG())
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def slot(self, ctx: commands.Context, bid: int):
|
async def slot(self, ctx: commands.Context, bid: int):
|
||||||
"""Play the slot machine"""
|
"""Use the slot machine."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
@@ -337,7 +381,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())
|
||||||
@@ -357,16 +405,19 @@ class Economy:
|
|||||||
await self.config.member(author).last_slot.set(now)
|
await self.config.member(author).last_slot.set(now)
|
||||||
await self.slot_machine(author, channel, bid)
|
await self.slot_machine(author, channel, bid)
|
||||||
|
|
||||||
async def slot_machine(self, author, channel, bid):
|
@staticmethod
|
||||||
default_reel = deque(SMReel)
|
async def slot_machine(author, channel, bid):
|
||||||
|
default_reel = deque(cast(Iterable, SMReel))
|
||||||
reels = []
|
reels = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
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][1], reels[1][1], reels[2][1]),
|
(reels[0][0], reels[1][0], reels[2][0]),
|
||||||
(reels[0][2], reels[1][2], reels[2][2]))
|
(reels[0][1], reels[1][1], reels[2][1]),
|
||||||
|
(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 +429,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,57 +445,64 @@ 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{} → {}!"
|
phrase = T_(payout["phrase"])
|
||||||
"").format(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{} → {}!"
|
phrase = _("Nothing!")
|
||||||
"").format(slot, author.mention, bid, then, now))
|
await channel.send(
|
||||||
|
(
|
||||||
|
"{slot}\n{author.mention} {phrase}\n\n"
|
||||||
|
+ _("Your bid: {amount}")
|
||||||
|
+ "\n{old_balance} → {new_balance}!"
|
||||||
|
).format(
|
||||||
|
slot=slot,
|
||||||
|
author=author,
|
||||||
|
phrase=phrase,
|
||||||
|
amount=bid,
|
||||||
|
old_balance=then,
|
||||||
|
new_balance=now,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def economyset(self, ctx: commands.Context):
|
async def economyset(self, ctx: commands.Context):
|
||||||
"""Changes economy module settings"""
|
"""Manage Economy settings."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
slot_min = await self.config.SLOT_MIN()
|
conf = self.config
|
||||||
slot_max = await self.config.SLOT_MAX()
|
|
||||||
slot_time = await self.config.SLOT_TIME()
|
|
||||||
payday_time = await self.config.PAYDAY_TIME()
|
|
||||||
payday_amount = await self.config.PAYDAY_CREDITS()
|
|
||||||
else:
|
else:
|
||||||
slot_min = await self.config.guild(guild).SLOT_MIN()
|
conf = self.config.guild(ctx.guild)
|
||||||
slot_max = await self.config.guild(guild).SLOT_MAX()
|
await ctx.send(
|
||||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
box(
|
||||||
payday_time = await self.config.guild(guild).PAYDAY_TIME()
|
_(
|
||||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
"----Economy Settings---\n"
|
||||||
register_amount = await bank.get_default_balance(guild)
|
"Minimum slot bid: {slot_min}\n"
|
||||||
msg = box(
|
"Maximum slot bid: {slot_max}\n"
|
||||||
_("Minimum slot bid: {}\n"
|
"Slot cooldown: {slot_time}\n"
|
||||||
"Maximum slot bid: {}\n"
|
"Payday amount: {payday_amount}\n"
|
||||||
"Slot cooldown: {}\n"
|
"Payday cooldown: {payday_time}\n"
|
||||||
"Payday amount: {}\n"
|
"Amount given at account registration: {register_amount}"
|
||||||
"Payday cooldown: {}\n"
|
).format(
|
||||||
"Amount given at account registration: {}"
|
slot_min=await conf.SLOT_MIN(),
|
||||||
"").format(
|
slot_max=await conf.SLOT_MAX(),
|
||||||
slot_min, slot_max, slot_time,
|
slot_time=await conf.SLOT_TIME(),
|
||||||
payday_amount, payday_time, register_amount
|
payday_time=await conf.PAYDAY_TIME(),
|
||||||
),
|
payday_amount=await conf.PAYDAY_CREDITS(),
|
||||||
_("Current Economy settings:")
|
register_amount=await bank.get_default_balance(guild),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||||
"""Minimum slot machine bid"""
|
"""Set the 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():
|
||||||
@@ -453,15 +510,18 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_MIN.set(bid)
|
await self.config.guild(guild).SLOT_MIN.set(bid)
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
await ctx.send(_("Minimum bid is now {} {}.").format(bid, credits_name))
|
await ctx.send(
|
||||||
|
_("Minimum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slotmax(self, ctx: commands.Context, bid: int):
|
async def slotmax(self, ctx: commands.Context, bid: int):
|
||||||
"""Maximum slot machine bid"""
|
"""Set the 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(
|
||||||
' than slotmin.'))
|
_("Invalid maximum bid amount. Must be greater than the minimum amount.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
@@ -469,32 +529,37 @@ class Economy:
|
|||||||
await self.config.SLOT_MAX.set(bid)
|
await self.config.SLOT_MAX.set(bid)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_MAX.set(bid)
|
await self.config.guild(guild).SLOT_MAX.set(bid)
|
||||||
await ctx.send(_("Maximum bid is now {} {}.").format(bid, credits_name))
|
await ctx.send(
|
||||||
|
_("Maximum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slottime(self, ctx: commands.Context, seconds: int):
|
async def slottime(self, ctx: commands.Context, seconds: int):
|
||||||
"""Seconds between each slots use"""
|
"""Set the cooldown for the slot machine."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
await self.config.SLOT_TIME.set(seconds)
|
await self.config.SLOT_TIME.set(seconds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_TIME.set(seconds)
|
await self.config.guild(guild).SLOT_TIME.set(seconds)
|
||||||
await ctx.send(_("Cooldown is now {} seconds.").format(seconds))
|
await ctx.send(_("Cooldown is now {num} seconds.").format(num=seconds))
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
||||||
"""Seconds between each payday"""
|
"""Set the cooldown for payday."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
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 {num} seconds must pass between each payday.").format(
|
||||||
|
num=seconds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||||
"""Amount earned each payday"""
|
"""Set the amount earned each payday."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
if creds <= 0:
|
if creds <= 0:
|
||||||
@@ -504,40 +569,51 @@ 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(
|
||||||
"").format(creds, credits_name))
|
_("Every payday will now give {num} {currency}.").format(
|
||||||
|
num=creds, currency=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):
|
||||||
"""Amount earned each payday for a role"""
|
"""Set the amount earned each payday for a role."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
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 {num} {currency} "
|
||||||
|
"to people with the role {role_name}."
|
||||||
|
).format(num=creds, currency=credits_name, role_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):
|
||||||
"""Amount given on registering an account"""
|
"""Set the initial balance for new bank accounts."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if creds < 0:
|
if creds < 0:
|
||||||
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 {num} {currency}.").format(
|
||||||
|
num=creds, currency=credits_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# What would I ever do without stackoverflow?
|
# What would I ever do without stackoverflow?
|
||||||
def display_time(self, seconds, granularity=2):
|
@staticmethod
|
||||||
|
def display_time(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 +623,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,199 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../economy.py:40
|
|
||||||
msgid "JACKPOT! 226! Your bid has been multiplied * 2500!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:44
|
|
||||||
msgid "4LC! +1000!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:48
|
|
||||||
msgid "Three cherries! +800!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:52
|
|
||||||
msgid "2 6! Your bid has been multiplied * 4!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:56
|
|
||||||
msgid "Two cherries! Your bid has been multiplied * 3!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:60
|
|
||||||
msgid "Three symbols! +500!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:64
|
|
||||||
msgid "Two consecutive symbols! Your bid has been multiplied * 2!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:68
|
|
||||||
msgid ""
|
|
||||||
"Slot machine payouts:\n"
|
|
||||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
|
||||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
|
||||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
|
||||||
"{two.value} {six.value} Bet * 4\n"
|
|
||||||
"{cherries.value} {cherries.value} Bet * 3\n"
|
|
||||||
"\n"
|
|
||||||
"Three symbols: +500\n"
|
|
||||||
"Two symbols: Bet * 2"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:157
|
|
||||||
msgid "{}'s balance is {} {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:171
|
|
||||||
msgid "{} transferred {} {} to {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:191
|
|
||||||
msgid "{} added {} {} to {}'s account."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:196
|
|
||||||
msgid "{} removed {} {} from {}'s account."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:201
|
|
||||||
msgid "{} set {}'s account to {} {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:212
|
|
||||||
msgid ""
|
|
||||||
"This will delete all bank accounts for {}.\n"
|
|
||||||
"If you're sure, type `{}bank reset yes`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:229
|
|
||||||
msgid "All bank accounts of this guild have been deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:248 ../economy.py:268
|
|
||||||
msgid "{} Here, take some {}. Enjoy! (+{} {}!)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:258 ../economy.py:276
|
|
||||||
msgid "{} Too soon. For your next payday you have to wait {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:313
|
|
||||||
msgid "There are no accounts in the bank."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:339
|
|
||||||
msgid "You're on cooldown, try again in a bit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:342
|
|
||||||
msgid "That's an invalid bid amount, sorry :/"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:345
|
|
||||||
msgid "You ain't got enough money, friend."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:391
|
|
||||||
msgid ""
|
|
||||||
"{}\n"
|
|
||||||
"{} {}\n"
|
|
||||||
"\n"
|
|
||||||
"Your bid: {}\n"
|
|
||||||
"{} → {}!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:398
|
|
||||||
msgid ""
|
|
||||||
"{}\n"
|
|
||||||
"{} Nothing!\n"
|
|
||||||
"Your bid: {}\n"
|
|
||||||
"{} → {}!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:423
|
|
||||||
msgid ""
|
|
||||||
"Minimum slot bid: {}\n"
|
|
||||||
"Maximum slot bid: {}\n"
|
|
||||||
"Slot cooldown: {}\n"
|
|
||||||
"Payday amount: {}\n"
|
|
||||||
"Payday cooldown: {}\n"
|
|
||||||
"Amount given at account registration: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:433
|
|
||||||
msgid "Current Economy settings:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:441
|
|
||||||
msgid "Invalid min bid amount."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:449
|
|
||||||
msgid "Minimum bid is now {} {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:456
|
|
||||||
msgid "Invalid slotmax bid amount. Must be greater than slotmin."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:465
|
|
||||||
msgid "Maximum bid is now {} {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:475
|
|
||||||
msgid "Cooldown is now {} seconds."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:485
|
|
||||||
msgid "Value modified. At least {} seconds must pass between each payday."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:494
|
|
||||||
msgid "Har har so funny."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:500
|
|
||||||
msgid "Every payday will now give {} {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:511
|
|
||||||
msgid "Registering an account will now give {} {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:517
|
|
||||||
msgid "weeks"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:518
|
|
||||||
msgid "days"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:519
|
|
||||||
msgid "hours"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:520
|
|
||||||
msgid "minutes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../economy.py:521
|
|
||||||
msgid "seconds"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../economy.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from typing import Union
|
||||||
|
|
||||||
from redbot.core import checks, Config, modlog, RedContext
|
from redbot.core import checks, Config, modlog, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import pagify
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
from redbot.core.utils.mod import is_mod_or_superior
|
|
||||||
|
|
||||||
_ = CogI18n("Filter", __file__)
|
_ = Translator("Filter", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
@cog_i18n(_)
|
||||||
"""Filter-related commands"""
|
class Filter(commands.Cog):
|
||||||
|
"""Filter unwanted words and phrases from text channels."""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, 4766951341)
|
self.settings = Config.get_conf(self, 4766951341)
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
@@ -21,39 +22,85 @@ 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}
|
||||||
|
default_channel_settings = {"filter": []}
|
||||||
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.settings.register_channel(**default_channel_settings)
|
||||||
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
||||||
|
|
||||||
def __unload(self):
|
def __unload(self):
|
||||||
self.register_task.cancel()
|
self.register_task.cancel()
|
||||||
|
|
||||||
async def register_filterban(self):
|
@staticmethod
|
||||||
|
async def register_filterban():
|
||||||
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
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
|
async def filterset(self, ctx: commands.Context):
|
||||||
|
"""Manage filter settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@filterset.command(name="defaultname")
|
||||||
|
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||||
|
"""Set the nickname for users with a filtered name.
|
||||||
|
|
||||||
|
Note that this has no effect if filtering names is disabled
|
||||||
|
(to toggle, run `[p]filter names`).
|
||||||
|
|
||||||
|
The default name used is *John Doe*.
|
||||||
|
"""
|
||||||
|
guild = ctx.guild
|
||||||
|
await self.settings.guild(guild).filter_default_name.set(name)
|
||||||
|
await ctx.send(_("The name to use on filtered names has been set."))
|
||||||
|
|
||||||
|
@filterset.command(name="ban")
|
||||||
|
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||||
|
"""Set the filter's autoban conditions.
|
||||||
|
|
||||||
|
Users will be banned if they send `<count>` filtered words in
|
||||||
|
`<timeframe>` seconds.
|
||||||
|
|
||||||
|
Set both to zero to disable autoban.
|
||||||
|
"""
|
||||||
|
if (count <= 0) != (timeframe <= 0):
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Count and timeframe either both need to be 0 "
|
||||||
|
"or both need to be greater than 0!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif count == 0 and timeframe == 0:
|
||||||
|
await self.settings.guild(ctx.guild).filterban_count.set(0)
|
||||||
|
await self.settings.guild(ctx.guild).filterban_time.set(0)
|
||||||
|
await ctx.send(_("Autoban disabled."))
|
||||||
|
else:
|
||||||
|
await self.settings.guild(ctx.guild).filterban_count.set(count)
|
||||||
|
await self.settings.guild(ctx.guild).filterban_time.set(timeframe)
|
||||||
|
await ctx.send(_("Count and time have been set."))
|
||||||
|
|
||||||
@commands.group(name="filter")
|
@commands.group(name="filter")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def _filter(self, ctx: RedContext):
|
async def _filter(self, ctx: commands.Context):
|
||||||
"""Adds/removes words from filter
|
"""Add or remove words from server filter.
|
||||||
|
|
||||||
Use double quotes to add/remove sentences
|
Use double quotes to add or remove sentences.
|
||||||
Using this command with no subcommands will send
|
|
||||||
the list of the server's filtered words."""
|
Using this command with no subcommands will send the list of
|
||||||
|
the server's filtered words.
|
||||||
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
word_list = await self.settings.guild(server).filter()
|
word_list = await self.settings.guild(server).filter()
|
||||||
@@ -66,146 +113,217 @@ class Filter:
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I can't send direct messages to you."))
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
@_filter.command(name="add")
|
@_filter.group(name="channel")
|
||||||
async def filter_add(self, ctx: commands.Context, *, words: str):
|
async def _filter_channel(self, ctx: commands.Context):
|
||||||
"""Adds words to the filter
|
"""Add or remove words from channel filter.
|
||||||
|
|
||||||
|
Use double quotes to add or remove sentences.
|
||||||
|
|
||||||
|
Using this command with no subcommands will send the list of
|
||||||
|
the channel's filtered words.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
word_list = await self.settings.channel(channel).filter()
|
||||||
|
if word_list:
|
||||||
|
words = ", ".join(word_list)
|
||||||
|
words = _("Filtered in this channel:") + "\n\n" + words
|
||||||
|
try:
|
||||||
|
for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
|
||||||
|
await author.send(page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
|
@_filter_channel.command("add")
|
||||||
|
async def filter_channel_add(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Add words to the filter.
|
||||||
|
|
||||||
|
Use double quotes to add sentences.
|
||||||
|
|
||||||
Use double quotes to add sentences
|
|
||||||
Examples:
|
Examples:
|
||||||
filter add word1 word2 word3
|
- `[p]filter channel add word1 word2 word3`
|
||||||
filter add \"This is a sentence\""""
|
- `[p]filter channel add "This is a sentence"`
|
||||||
server = ctx.guild
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
split_words = words.split()
|
split_words = words.split()
|
||||||
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 = ""
|
||||||
else:
|
else:
|
||||||
tmp += word
|
tmp += word + " "
|
||||||
added = await self.add_to_filter(server, word_list)
|
added = await self.add_to_filter(channel, word_list)
|
||||||
if added:
|
if added:
|
||||||
await ctx.send(_("Words added to filter."))
|
await ctx.send(_("Words added to filter."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Words already in the filter."))
|
await ctx.send(_("Words already in the filter."))
|
||||||
|
|
||||||
@_filter.command(name="remove")
|
@_filter_channel.command("remove")
|
||||||
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
async def filter_channel_remove(self, ctx: commands.Context, *, words: str):
|
||||||
"""Remove words from the filter
|
"""Remove words from the filter.
|
||||||
|
|
||||||
|
Use double quotes to remove sentences.
|
||||||
|
|
||||||
Use double quotes to remove sentences
|
|
||||||
Examples:
|
Examples:
|
||||||
filter remove word1 word2 word3
|
- `[p]filter channel remove word1 word2 word3`
|
||||||
filter remove \"This is a sentence\""""
|
- `[p]filter channel remove "This is a sentence"`
|
||||||
server = ctx.guild
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
split_words = words.split()
|
split_words = words.split()
|
||||||
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 = ""
|
||||||
else:
|
else:
|
||||||
tmp += word
|
tmp += word + " "
|
||||||
removed = await self.remove_from_filter(server, word_list)
|
removed = await self.remove_from_filter(channel, word_list)
|
||||||
if removed:
|
if removed:
|
||||||
await ctx.send(_("Words removed from filter."))
|
await ctx.send(_("Words removed from filter."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Those words weren't in the filter."))
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
@_filter.command(name="names")
|
@_filter.command(name="add")
|
||||||
async def filter_names(self, ctx: RedContext):
|
async def filter_add(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Add words to the filter.
|
||||||
|
|
||||||
|
Use double quotes to add sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter add word1 word2 word3`
|
||||||
|
- `[p]filter add "This is a sentence"`
|
||||||
"""
|
"""
|
||||||
Toggles whether or not to check names and nicknames against the filter
|
server = ctx.guild
|
||||||
This is disabled by default
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
added = await self.add_to_filter(server, word_list)
|
||||||
|
if added:
|
||||||
|
await ctx.send(_("Words successfully added to filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words were already in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="remove")
|
||||||
|
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Remove words from the filter.
|
||||||
|
|
||||||
|
Use double quotes to remove sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter remove word1 word2 word3`
|
||||||
|
- `[p]filter remove "This is a sentence"`
|
||||||
|
"""
|
||||||
|
server = ctx.guild
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
removed = await self.remove_from_filter(server, word_list)
|
||||||
|
if removed:
|
||||||
|
await ctx.send(_("Words successfully removed from filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="names")
|
||||||
|
async def filter_names(self, ctx: commands.Context):
|
||||||
|
"""Toggle name and nickname filtering.
|
||||||
|
|
||||||
|
This is disabled by default.
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
current_setting = await self.settings.guild(guild).filter_names()
|
current_setting = await self.settings.guild(guild).filter_names()
|
||||||
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 filtered."))
|
||||||
_("Names and nicknames will no longer be "
|
|
||||||
"checked against the filter")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(_("Names and nicknames will now be filtered."))
|
||||||
_("Names and nicknames will now be checked against "
|
|
||||||
"the filter")
|
|
||||||
)
|
|
||||||
|
|
||||||
@_filter.command(name="defaultname")
|
async def add_to_filter(
|
||||||
async def filter_default_name(self, ctx: RedContext, name: str):
|
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list
|
||||||
"""
|
) -> bool:
|
||||||
Sets the default name to use if filtering names is enabled
|
|
||||||
Note that this has no effect if filtering names is disabled
|
|
||||||
The default name used is John Doe
|
|
||||||
"""
|
|
||||||
guild = ctx.guild
|
|
||||||
await self.settings.guild(guild).filter_default_name.set(name)
|
|
||||||
await ctx.send(_("The name to use on filtered names has been set"))
|
|
||||||
|
|
||||||
@_filter.command(name="ban")
|
|
||||||
async def filter_ban(
|
|
||||||
self, ctx: commands.Context, count: int, timeframe: int):
|
|
||||||
"""
|
|
||||||
Sets up an autoban if the specified number of messages are
|
|
||||||
filtered in the specified amount of time (in seconds)
|
|
||||||
"""
|
|
||||||
if (count <= 0) != (timeframe <= 0):
|
|
||||||
await ctx.send(
|
|
||||||
_("Count and timeframe either both need to be 0 "
|
|
||||||
"or both need to be greater than 0!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
elif count == 0 and timeframe == 0:
|
|
||||||
await self.settings.guild(ctx.guild).filterban_count.set(0)
|
|
||||||
await self.settings.guild(ctx.guild).filterban_time.set(0)
|
|
||||||
await ctx.send(_("Autoban disabled."))
|
|
||||||
else:
|
|
||||||
await self.settings.guild(ctx.guild).filterban_count.set(count)
|
|
||||||
await self.settings.guild(ctx.guild).filterban_time.set(timeframe)
|
|
||||||
await ctx.send(_("Count and time have been set."))
|
|
||||||
|
|
||||||
async def add_to_filter(self, server: discord.Guild, words: list) -> bool:
|
|
||||||
added = False
|
added = False
|
||||||
async with self.settings.guild(server).filter() as cur_list:
|
if isinstance(server_or_channel, discord.Guild):
|
||||||
for w in words:
|
async with self.settings.guild(server_or_channel).filter() as cur_list:
|
||||||
if w.lower() not in cur_list and w:
|
for w in words:
|
||||||
cur_list.append(w.lower())
|
if w.lower() not in cur_list and w:
|
||||||
added = True
|
cur_list.append(w.lower())
|
||||||
|
added = True
|
||||||
|
|
||||||
|
elif isinstance(server_or_channel, discord.TextChannel):
|
||||||
|
async with self.settings.channel(server_or_channel).filter() as cur_list:
|
||||||
|
for w in words:
|
||||||
|
if w.lower not in cur_list and w:
|
||||||
|
cur_list.append(w.lower())
|
||||||
|
added = True
|
||||||
|
|
||||||
return added
|
return added
|
||||||
|
|
||||||
async def remove_from_filter(self, server: discord.Guild, words: list) -> bool:
|
async def remove_from_filter(
|
||||||
|
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list
|
||||||
|
) -> bool:
|
||||||
removed = False
|
removed = False
|
||||||
async with self.settings.guild(server).filter() as cur_list:
|
if isinstance(server_or_channel, discord.Guild):
|
||||||
for w in words:
|
async with self.settings.guild(server_or_channel).filter() as cur_list:
|
||||||
if w.lower() in cur_list:
|
for w in words:
|
||||||
cur_list.remove(w.lower())
|
if w.lower() in cur_list:
|
||||||
removed = True
|
cur_list.remove(w.lower())
|
||||||
|
removed = True
|
||||||
|
|
||||||
|
elif isinstance(server_or_channel, discord.TextChannel):
|
||||||
|
async with self.settings.channel(server_or_channel).filter() as cur_list:
|
||||||
|
for w in words:
|
||||||
|
if w.lower() in cur_list:
|
||||||
|
cur_list.remove(w.lower())
|
||||||
|
removed = True
|
||||||
|
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
async def check_filter(self, message: discord.Message):
|
async def check_filter(self, message: discord.Message):
|
||||||
server = message.guild
|
server = message.guild
|
||||||
author = message.author
|
author = message.author
|
||||||
word_list = await self.settings.guild(server).filter()
|
word_list = set(
|
||||||
|
await self.settings.guild(server).filter()
|
||||||
|
+ await self.settings.channel(message.channel).filter()
|
||||||
|
)
|
||||||
filter_count = await self.settings.guild(server).filterban_count()
|
filter_count = await self.settings.guild(server).filterban_count()
|
||||||
filter_time = await self.settings.guild(server).filterban_time()
|
filter_time = await self.settings.guild(server).filterban_time()
|
||||||
user_count = await self.settings.member(author).filter_count()
|
user_count = await self.settings.member(author).filter_count()
|
||||||
@@ -213,9 +331,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)
|
||||||
@@ -225,23 +341,30 @@ class Filter:
|
|||||||
if w in message.content.lower():
|
if w in message.content.lower():
|
||||||
try:
|
try:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
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
|
||||||
reason = "Autoban (too many filtered messages)"
|
and message.created_at.timestamp() < next_reset_time
|
||||||
|
):
|
||||||
|
reason = _("Autoban (too many filtered messages.)")
|
||||||
try:
|
try:
|
||||||
await server.ban(author, reason=reason)
|
await server.ban(author, reason=reason)
|
||||||
except:
|
except discord.HTTPException:
|
||||||
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):
|
||||||
@@ -252,75 +375,40 @@ class Filter:
|
|||||||
if not valid_user:
|
if not valid_user:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Bots and mods or superior are ignored from the filter
|
if await self.bot.is_automod_immune(message):
|
||||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
|
||||||
if mod_or_superior:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.check_filter(message)
|
await self.check_filter(message)
|
||||||
|
|
||||||
async def on_message_edit(self, _, message):
|
async def on_message_edit(self, _prior, message):
|
||||||
author = message.author
|
# message content has to change for non-bot's currently.
|
||||||
if message.guild is None or self.bot.user == author:
|
# if this changes, we should compare before passing it.
|
||||||
return
|
await self.on_message(message)
|
||||||
valid_user = isinstance(author, discord.Member) and not author.bot
|
|
||||||
if not valid_user:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Bots and mods or superior are ignored from the filter
|
|
||||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
|
||||||
if mod_or_superior:
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.check_filter(message)
|
|
||||||
|
|
||||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
if not after.guild.me.guild_permissions.manage_nicknames:
|
if before.display_name != after.display_name:
|
||||||
return # No permissions to manage nicknames, so can't do anything
|
await self.maybe_filter_name(after)
|
||||||
word_list = await self.settings.guild(after.guild).filter()
|
|
||||||
filter_names = await self.settings.guild(after.guild).filter_names()
|
|
||||||
name_to_use = await self.settings.guild(after.guild).filter_default_name()
|
|
||||||
if not filter_names:
|
|
||||||
return
|
|
||||||
|
|
||||||
name_filtered = False
|
|
||||||
nick_filtered = False
|
|
||||||
|
|
||||||
for w in word_list:
|
|
||||||
if w in after.name:
|
|
||||||
name_filtered = True
|
|
||||||
if after.nick and w in after.nick: # since Member.nick can be None
|
|
||||||
nick_filtered = True
|
|
||||||
if name_filtered and nick_filtered: # Both true, so break from loop
|
|
||||||
break
|
|
||||||
|
|
||||||
if name_filtered and after.nick is None:
|
|
||||||
try:
|
|
||||||
await after.edit(nick=name_to_use, reason="Filtered name")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
elif nick_filtered:
|
|
||||||
try:
|
|
||||||
await after.edit(nick=None, reason="Filtered nickname")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_member_join(self, member: discord.Member):
|
async def on_member_join(self, member: discord.Member):
|
||||||
guild = member.guild
|
await self.maybe_filter_name(member)
|
||||||
if not guild.me.guild_permissions.manage_nicknames:
|
|
||||||
return
|
|
||||||
word_list = await self.settings.guild(guild).filter()
|
|
||||||
filter_names = await self.settings.guild(guild).filter_names()
|
|
||||||
name_to_use = await self.settings.guild(guild).filter_default_name()
|
|
||||||
|
|
||||||
if not filter_names:
|
async def maybe_filter_name(self, member: discord.Member):
|
||||||
|
if not member.guild.me.guild_permissions.manage_nicknames:
|
||||||
|
return # No permissions to manage nicknames, so can't do anything
|
||||||
|
if member.top_role >= member.guild.me.top_role:
|
||||||
|
return # Discord Hierarchy applies to nicks
|
||||||
|
if await self.bot.is_automod_immune(member):
|
||||||
|
return
|
||||||
|
if not await self.settings.guild(member.guild).filter_names():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
word_list = await self.settings.guild(member.guild).filter()
|
||||||
for w in word_list:
|
for w in word_list:
|
||||||
if w in member.name:
|
if w in member.display_name.lower():
|
||||||
|
name_to_use = await self.settings.guild(member.guild).filter_default_name()
|
||||||
|
reason = _("Filtered nickname") if member.nick else _("Filtered name")
|
||||||
try:
|
try:
|
||||||
await member.edit(nick=name_to_use, reason="Filtered name")
|
await member.edit(nick=name_to_use, reason=reason)
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
break
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
|
||||||
|
|
||||||
|
|
||||||
#: ../filter.py:62
|
|
||||||
msgid "Filtered in this server:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:67
|
|
||||||
msgid "I can't send direct messages to you."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:96
|
|
||||||
msgid "Words added to filter."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:98
|
|
||||||
msgid "Words already in the filter."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:127
|
|
||||||
msgid "Words removed from filter."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:129
|
|
||||||
msgid "Those words weren't in the filter."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:142
|
|
||||||
msgid "Names and nicknames will no longer be checked against the filter"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:147
|
|
||||||
msgid "Names and nicknames will now be checked against the filter"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:160
|
|
||||||
msgid "The name to use on filtered names has been set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:171
|
|
||||||
msgid "Count and timeframe either both need to be 0 or both need to be greater than 0!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:179
|
|
||||||
msgid "Autoban disabled."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../filter.py:183
|
|
||||||
msgid "Count and time have been set."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
TO_TRANSLATE = [
|
|
||||||
'../filter.py'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def regen_messages():
|
|
||||||
subprocess.run(
|
|
||||||
['pygettext', '-n'] + TO_TRANSLATE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
regen_messages()
|
|
||||||
@@ -2,21 +2,19 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from random import randint, choice
|
from random import randint, choice
|
||||||
from urllib.parse import quote_plus
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
from redbot.core.i18n import CogI18n
|
from redbot.core import commands
|
||||||
from discord.ext import commands
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
from redbot.core.utils.chat_formatting import escape, italics
|
||||||
|
|
||||||
from redbot.core.utils.chat_formatting import escape, italics, pagify
|
_ = T_ = Translator("General", __file__)
|
||||||
|
|
||||||
_ = CogI18n("General", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class RPS(Enum):
|
class RPS(Enum):
|
||||||
rock = "\N{MOYAI}"
|
rock = "\N{MOYAI}"
|
||||||
paper = "\N{PAGE FACING UP}"
|
paper = "\N{PAGE FACING UP}"
|
||||||
scissors = "\N{BLACK SCISSORS}"
|
scissors = "\N{BLACK SCISSORS}"
|
||||||
|
|
||||||
|
|
||||||
@@ -30,68 +28,82 @@ class RPSParser:
|
|||||||
elif argument == "scissors":
|
elif argument == "scissors":
|
||||||
self.choice = RPS.scissors
|
self.choice = RPS.scissors
|
||||||
else:
|
else:
|
||||||
raise
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
class General:
|
@cog_i18n(_)
|
||||||
|
class General(commands.Cog):
|
||||||
"""General commands."""
|
"""General commands."""
|
||||||
|
|
||||||
def __init__(self):
|
global _
|
||||||
self.stopwatches = {}
|
_ = lambda s: s
|
||||||
self.ball = [
|
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"),
|
||||||
|
]
|
||||||
|
_ = T_
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
def __init__(self):
|
||||||
async def ping(self, ctx):
|
super().__init__()
|
||||||
"""Pong."""
|
self.stopwatches = {}
|
||||||
await ctx.send("Pong.")
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def choose(self, ctx, *choices):
|
async def choose(self, ctx, *choices):
|
||||||
"""Chooses between multiple choices.
|
"""Choose between multiple options.
|
||||||
|
|
||||||
To denote multiple choices, you should use double quotes.
|
To denote options which include whitespace, you should use
|
||||||
|
double quotes.
|
||||||
"""
|
"""
|
||||||
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 options 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)
|
"""Roll a random number.
|
||||||
|
|
||||||
Defaults to 100.
|
The result will be between 1 and `<number>`.
|
||||||
|
|
||||||
|
`<number>` defaults to 100.
|
||||||
"""
|
"""
|
||||||
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("{author.mention} :game_die: {n} :game_die:".format(author=author, n=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(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
|
||||||
|
|
||||||
@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.
|
"""Flip a coin... or a user.
|
||||||
|
|
||||||
Defaults to coin.
|
Defaults to a coin.
|
||||||
"""
|
"""
|
||||||
if user != None:
|
if user is not None:
|
||||||
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)
|
||||||
@@ -102,79 +114,88 @@ 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:
|
||||||
outcome = None # Tie
|
outcome = None # Tie
|
||||||
else:
|
else:
|
||||||
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(
|
||||||
red_choice.value, author.mention
|
_("{choice} You win {author.mention}!").format(
|
||||||
))
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
elif outcome is False:
|
elif outcome is False:
|
||||||
await ctx.send(_("{} You lose {}!").format(
|
await ctx.send(
|
||||||
red_choice.value, author.mention
|
_("{choice} You lose {author.mention}!").format(
|
||||||
))
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("{} We're square {}!").format(
|
await ctx.send(
|
||||||
red_choice.value, author.mention
|
_("{choice} We're square {author.mention}!").format(
|
||||||
))
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@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.
|
||||||
"""
|
"""
|
||||||
if question.endswith("?") and question != "?":
|
if question.endswith("?") and question != "?":
|
||||||
await ctx.send("`" + choice(self.ball) + "`")
|
await ctx.send("`" + T_(choice(self.ball)) + "`")
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That doesn't look like a question."))
|
await ctx.send(_("That doesn't look like a question."))
|
||||||
|
|
||||||
@commands.command(aliases=["sw"])
|
@commands.command(aliases=["sw"])
|
||||||
async def stopwatch(self, ctx):
|
async def stopwatch(self, ctx):
|
||||||
"""Starts/stops stopwatch"""
|
"""Start or stop the stopwatch."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if not author.id in self.stopwatches:
|
if author.id not in self.stopwatches:
|
||||||
self.stopwatches[author.id] = int(time.perf_counter())
|
self.stopwatches[author.id] = int(time.perf_counter())
|
||||||
await ctx.send(author.mention + _(" Stopwatch started!"))
|
await ctx.send(author.mention + _(" Stopwatch started!"))
|
||||||
else:
|
else:
|
||||||
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
||||||
tmp = str(datetime.timedelta(seconds=tmp))
|
tmp = str(datetime.timedelta(seconds=tmp))
|
||||||
await ctx.send(author.mention + _(" Stopwatch stopped! Time: **") + tmp + "**")
|
await ctx.send(
|
||||||
|
author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp)
|
||||||
|
)
|
||||||
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"""
|
"""Create a lmgtfy link."""
|
||||||
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
search_terms = escape(
|
||||||
|
search_terms.replace("+", "%2B").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.
|
||||||
|
"""
|
||||||
name = italics(user.display_name)
|
name = italics(user.display_name)
|
||||||
if intensity <= 0:
|
if intensity <= 0:
|
||||||
msg = "(っ˘̩╭╮˘̩)っ" + name
|
msg = "(っ˘̩╭╮˘̩)っ" + name
|
||||||
@@ -186,104 +207,30 @@ class General:
|
|||||||
msg = "(つ≧▽≦)つ" + name
|
msg = "(つ≧▽≦)つ" + name
|
||||||
elif intensity >= 10:
|
elif intensity >= 10:
|
||||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||||
|
else:
|
||||||
|
# For the purposes of "msg might not be defined" linter errors
|
||||||
|
raise RuntimeError
|
||||||
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"""
|
"""Show server information."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
online = len([m.status for m in guild.members
|
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||||
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 {date}. That's over {num} days ago!").format(
|
||||||
"").format(guild.created_at.strftime("%d %b %Y %H:%M"),
|
date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed
|
||||||
passed))
|
)
|
||||||
|
data = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
|
||||||
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
|
|
||||||
colour = randint(0, 0xFFFFFF)
|
|
||||||
|
|
||||||
data = discord.Embed(
|
|
||||||
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=f"{online}/{total_users}")
|
||||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
data.add_field(name=_("Text Channels"), value=str(text_channels))
|
||||||
data.add_field(name=_("Voice Channels"), value=voice_channels)
|
data.add_field(name=_("Voice Channels"), value=str(voice_channels))
|
||||||
data.add_field(name=_("Roles"), value=len(guild.roles))
|
data.add_field(name=_("Roles"), value=str(len(guild.roles)))
|
||||||
data.add_field(name=_("Owner"), value=str(guild.owner))
|
data.add_field(name=_("Owner"), value=str(guild.owner))
|
||||||
data.set_footer(text=_("Server ID: ") + str(guild.id))
|
data.set_footer(text=_("Server ID: ") + str(guild.id))
|
||||||
|
|
||||||
@@ -295,53 +242,89 @@ class General:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
except discord.HTTPException:
|
except discord.Forbidden:
|
||||||
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, *, word):
|
||||||
"""Urban Dictionary search
|
"""Search the Urban Dictionary.
|
||||||
|
|
||||||
Definition number must be between 1 and 10"""
|
This uses the unofficial Urban Dictionary API.
|
||||||
def encode(s):
|
"""
|
||||||
return quote_plus(s, encoding='utf-8', errors='replace')
|
|
||||||
|
|
||||||
# definition_number is just there to show up in the help
|
|
||||||
# all this mess is to avoid forcing double quotes on the user
|
|
||||||
|
|
||||||
search_terms = search_terms.split(" ")
|
|
||||||
try:
|
try:
|
||||||
if len(search_terms) > 1:
|
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
|
||||||
pos = int(search_terms[-1]) - 1
|
|
||||||
search_terms = search_terms[:-1]
|
headers = {"content-type": "application/json"}
|
||||||
else:
|
|
||||||
pos = 0
|
|
||||||
if pos not in range(0, 11): # API only provides the
|
|
||||||
pos = 0 # top 10 definitions
|
|
||||||
except ValueError:
|
|
||||||
pos = 0
|
|
||||||
|
|
||||||
search_terms = {"term": "+".join([s for s in search_terms])}
|
|
||||||
url = "http://api.urbandictionary.com/v0/define"
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, params=search_terms) as r:
|
async with session.get(url, headers=headers) as response:
|
||||||
result = await r.json()
|
data = await response.json()
|
||||||
item_list = result["list"]
|
|
||||||
if item_list:
|
except aiohttp.ClientError:
|
||||||
definition = item_list[pos]['definition']
|
await ctx.send(
|
||||||
example = item_list[pos]['example']
|
_("No Urban dictionary entries were found, or there was an error in the process")
|
||||||
defs = len(item_list)
|
)
|
||||||
msg = ("**Definition #{} out of {}:\n**{}\n\n"
|
return
|
||||||
"**Example:\n**{}".format(pos+1, defs, definition,
|
|
||||||
example))
|
if data.get("error") != 404:
|
||||||
msg = pagify(msg, ["\n"])
|
|
||||||
for page in msg:
|
if await ctx.embed_requested():
|
||||||
await ctx.send(page)
|
# a list of embeds
|
||||||
|
embeds = []
|
||||||
|
for ud in data["list"]:
|
||||||
|
embed = discord.Embed()
|
||||||
|
embed.title = _("{word} by {author}").format(
|
||||||
|
word=ud["word"].capitalize(), author=ud["author"]
|
||||||
|
)
|
||||||
|
embed.url = ud["permalink"]
|
||||||
|
|
||||||
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
|
if len(description) > 2048:
|
||||||
|
description = "{}...".format(description[:2045])
|
||||||
|
embed.description = description
|
||||||
|
|
||||||
|
embed.set_footer(
|
||||||
|
text=_(
|
||||||
|
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||||||
|
).format(**ud)
|
||||||
|
)
|
||||||
|
embeds.append(embed)
|
||||||
|
|
||||||
|
if embeds is not None and len(embeds) > 0:
|
||||||
|
await menu(
|
||||||
|
ctx,
|
||||||
|
pages=embeds,
|
||||||
|
controls=DEFAULT_CONTROLS,
|
||||||
|
message=None,
|
||||||
|
page=0,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Your search terms gave no results."))
|
messages = []
|
||||||
except IndexError:
|
for ud in data["list"]:
|
||||||
await ctx.send(_("There is no definition #{}").format(pos+1))
|
ud.set_default("example", "N/A")
|
||||||
except:
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
await ctx.send(_("Error."))
|
if len(description) > 2048:
|
||||||
|
description = "{}...".format(description[:2045])
|
||||||
|
|
||||||
|
message = _(
|
||||||
|
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||||||
|
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
|
||||||
|
).format(word=ud.pop("word").capitalize(), description=description, **ud)
|
||||||
|
messages.append(message)
|
||||||
|
|
||||||
|
if messages is not None and len(messages) > 0:
|
||||||
|
await menu(
|
||||||
|
ctx,
|
||||||
|
pages=messages,
|
||||||
|
controls=DEFAULT_CONTROLS,
|
||||||
|
message=None,
|
||||||
|
page=0,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("No Urban dictionary entries were found, or there was an error in the process.")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user