mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
Compare commits
170 Commits
3.0.0b16
...
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 |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -24,6 +24,8 @@ redbot/core/utils/mod.py @palmtree5
|
||||
redbot/core/utils/data_converter.py @mikeshardmind
|
||||
redbot/core/utils/antispam.py @mikeshardmind
|
||||
redbot/core/utils/tunnel.py @mikeshardmind
|
||||
redbot/core/utils/caching.py @mikeshardmind
|
||||
redbot/core/utils/common_filters.py @mikeshardmind
|
||||
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
@@ -44,6 +46,7 @@ redbot/cogs/trivia/* @Tobotimus
|
||||
redbot/cogs/dataconverter/* @mikeshardmind
|
||||
redbot/cogs/reports/* @mikeshardmind
|
||||
redbot/cogs/permissions/* @mikeshardmind
|
||||
redbot/cogs/warnings/* @palmtree5
|
||||
|
||||
# Docs
|
||||
docs/* @tekulvw @palmtree5
|
||||
|
||||
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
@@ -31,7 +31,7 @@ We love receiving contributions from our community. Any assistance you can provi
|
||||
# 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.
|
||||
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:
|
||||
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.
|
||||
@@ -53,7 +53,7 @@ Red's repository is configured to follow a particular development workflow, usin
|
||||
|
||||
### 4.1 Setting up your development environment
|
||||
The following requirements must be installed prior to setting up:
|
||||
- Python 3.6
|
||||
- Python 3.6.2 or greater (3.6.6 or greater on Windows)
|
||||
- git
|
||||
- pip
|
||||
- pipenv
|
||||
@@ -79,7 +79,7 @@ Note: If you haven't used `pipenv` before but are comfortable with virtualenvs,
|
||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||
|
||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on both python 3.5 and 3.6 (test environments `py35` and `py36` respectively)
|
||||
- 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`)
|
||||
|
||||
@@ -92,9 +92,7 @@ Your PR will not be merged until all of these tests pass.
|
||||
### 4.3 Style
|
||||
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
|
||||
|
||||
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
||||
|
||||
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
|
||||
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
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
*.json
|
||||
*.exe
|
||||
*.dll
|
||||
*.pot
|
||||
.data
|
||||
!/tests/cogs/dataconverter/data/**/*.json
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ formats:
|
||||
build:
|
||||
image: latest
|
||||
|
||||
requirements_file: docs/requirements.txt
|
||||
requirements_file: dependency_links.txt
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
pip_install: true
|
||||
extra_requirements:
|
||||
- docs
|
||||
- mongo
|
||||
|
||||
35
.travis.yml
35
.travis.yml
@@ -1,33 +1,37 @@
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
language: python
|
||||
cache: pip
|
||||
notifications:
|
||||
email: false
|
||||
sudo: true
|
||||
|
||||
python:
|
||||
- 3.6.5
|
||||
- 3.6.6
|
||||
- 3.7
|
||||
env:
|
||||
global:
|
||||
PIPENV_IGNORE_VIRTUALENVS=1
|
||||
matrix:
|
||||
- TOXENV=py
|
||||
- TOXENV=docs
|
||||
- TOXENV=style
|
||||
TOXENV=py
|
||||
|
||||
install:
|
||||
- pip install --upgrade pip pipenv
|
||||
- pipenv install --dev
|
||||
- pip install --upgrade pip tox
|
||||
|
||||
script:
|
||||
- pipenv run tox
|
||||
- tox
|
||||
|
||||
jobs:
|
||||
include:
|
||||
|
||||
# These jobs only occur on tag creation for V3/develop if the prior ones succeed
|
||||
- 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
|
||||
if: tag IS present
|
||||
python: 3.6.5
|
||||
python: 3.6.6
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
deploy:
|
||||
@@ -38,12 +42,11 @@ jobs:
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
branch: V3/develop
|
||||
python: 3.6.5
|
||||
python: 3.6.6
|
||||
tags: true
|
||||
- stage: Crowdin Deployment
|
||||
if: tag IS present
|
||||
python: 3.6.5
|
||||
python: 3.6.6
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
before_deploy:
|
||||
@@ -51,12 +54,12 @@ jobs:
|
||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y crowdin
|
||||
- pip install redgettext==2.1
|
||||
deploy:
|
||||
- provider: script
|
||||
script: python3 ./generate_strings.py
|
||||
script: make gettext
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
branch: V3/develop
|
||||
python: 3.6.5
|
||||
python: 3.6.6
|
||||
tags: true
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
include README.rst
|
||||
include README.md
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include discord/bin/*.dll
|
||||
include redbot/cogs/audio/application.yml
|
||||
include dependency_links.txt
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,4 +1,7 @@
|
||||
reformat:
|
||||
black -l 99 `git ls-files "*.py"`
|
||||
black -l 99 -N `git ls-files "*.py"`
|
||||
stylecheck:
|
||||
black --check -l 99 `git ls-files "*.py"`
|
||||
black --check -l 99 -N `git ls-files "*.py"`
|
||||
gettext:
|
||||
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||
crowdin upload
|
||||
|
||||
14
Pipfile
14
Pipfile
@@ -4,17 +4,9 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = '7eb918b19e3e60b56eb9039eb267f8f3477c5e17', editable = true}
|
||||
"e1839a8" = {path = ".", editable = true}
|
||||
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true }
|
||||
"e1839a8" = { path = ".", editable = true, extras = ['mongo', 'voice'] }
|
||||
|
||||
[dev-packages]
|
||||
tox = "*"
|
||||
pytest = "*"
|
||||
pytest-asyncio = "*"
|
||||
sphinx = ">1.7"
|
||||
sphinxcontrib-asyncio = "*"
|
||||
sphinx-rtd-theme = "*"
|
||||
black = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
"e1839a9" = { path = ".", editable = true, extras = ['docs', 'test', 'style'] }
|
||||
|
||||
648
Pipfile.lock
generated
648
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
|
||||
"sha256": "edd35f353e1fadc20094e40de6627db77fd61303da01794214c44d748e99838b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -16,28 +16,37 @@
|
||||
"default": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:129d83dd067760cec3cfd4456b5c6d7ac29f2c639d856884568fd539bed5a51f",
|
||||
"sha256:33c62afd115c456b0cf1e890fe6753055effe0f31a28321efd4f787378d6f4ab",
|
||||
"sha256:666756e1d4cf161ed1486b82f65fdd386ac07dd20fb10f025abf4be54be12746",
|
||||
"sha256:9705ded5a0faa25c8f14c6afb7044002d66c9120ed7eadb4aa9ca4aad32bd00c",
|
||||
"sha256:af5bfdd164256118a0a306b3f7046e63207d1f8cba73a67dcc0bd858dcfcd3bc",
|
||||
"sha256:b80f44b99fa3c9b4530fcfa324a99b84843043c35b084e0b653566049974435d",
|
||||
"sha256:c67e105ec74b85c8cb666b6877569dee6f55b9548f982983b9bee80b3d47e6f3",
|
||||
"sha256:d15c6658de5b7783c2538407278fa062b079a46d5f814a133ae0f09bbb2cfbc4",
|
||||
"sha256:d611ebd1ef48498210b65486306e065fde031040a1f3c455ca1b6baa7bf32ad3",
|
||||
"sha256:dcc7e4dcec6b0012537b9f8a0726f8b111188894ab0f924b680d40b13d3298a0",
|
||||
"sha256:de8ef106e130b94ca143fdfc6f27cda1d8ba439462542377738af4d99d9f5dd2",
|
||||
"sha256:eb6f1405b607fff7e44168e3ceb5d3c8a8c5a2d3effe0a27f843b16ec047a6d7",
|
||||
"sha256:f0e2ac69cb709367400008cebccd5d48161dd146096a009a632a132babe5714c"
|
||||
"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": "==2.2.5"
|
||||
"version": "==3.4.4"
|
||||
},
|
||||
"aiohttp-json-rpc": {
|
||||
"hashes": [
|
||||
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
|
||||
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
|
||||
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||
],
|
||||
"version": "==0.8.7"
|
||||
"version": "==0.11.2"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
@@ -48,10 +57,18 @@
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:00cff4d2dce744607335cba84e9929c3165632da2d27970dbc55802a0c7873d0",
|
||||
"sha256:9093db5b8ddbe4b8f6885d1a6e0ad84ae3155464cbf6877c387605244c285f3c"
|
||||
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
"markers": "python_version >= '3.5.3'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -70,7 +87,7 @@
|
||||
"discord.py": {
|
||||
"editable": true,
|
||||
"git": "git://github.com/Rapptz/discord.py",
|
||||
"ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
|
||||
"ref": "836ae730401ea370aa10127bb9c86854c8b516ac"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
@@ -79,41 +96,116 @@
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c",
|
||||
"sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31"
|
||||
],
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
"extras": [
|
||||
"mongo",
|
||||
"voice"
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
||||
"sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f"
|
||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.6"
|
||||
"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:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||
"sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
|
||||
"sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
|
||||
"sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
|
||||
"sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
|
||||
"sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
|
||||
"sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
|
||||
"sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
|
||||
"sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
|
||||
"sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
|
||||
"sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
|
||||
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
|
||||
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
|
||||
"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.3.1"
|
||||
"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": [
|
||||
@@ -123,87 +215,131 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
||||
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
||||
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
||||
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
||||
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
||||
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
||||
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
||||
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
||||
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
||||
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
||||
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
||||
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
||||
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
||||
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||
],
|
||||
"version": "==3.12"
|
||||
"version": "==3.13"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:0adae40e004dfe2181d1f2883aa3d4ca1cf16dbe449ae4b445b011c6eb220a90",
|
||||
"sha256:84da75114739191bdf2388f296ffd6177e83567a7fbaf2701e034ad6026e4f3b"
|
||||
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
|
||||
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
|
||||
],
|
||||
"version": "==6.5.0"
|
||||
"version": "==6.9.0"
|
||||
},
|
||||
"red-trivia": {
|
||||
"raven-aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:39413b9fb3f9b9362d6de1dcf69a4bf635b0f3518243f7178299b96d26cbb6a7"
|
||||
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
||||
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
"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:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||
"sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a",
|
||||
"sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8",
|
||||
"sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345",
|
||||
"sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5",
|
||||
"sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937",
|
||||
"sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5",
|
||||
"sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584",
|
||||
"sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af",
|
||||
"sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27",
|
||||
"sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab",
|
||||
"sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac",
|
||||
"sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa",
|
||||
"sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc",
|
||||
"sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7",
|
||||
"sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4",
|
||||
"sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a",
|
||||
"sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941",
|
||||
"sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2",
|
||||
"sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da"
|
||||
"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"
|
||||
],
|
||||
"version": "==3.4"
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:605480ee43eead69ec8e8c52cdfefc79cef6379cc0e87d908cf290408c1e49af",
|
||||
"sha256:7fad2530cb4ddf2b74c1e4f6f9f0e28eac482094c6542f98fd71ecf67fb4fded",
|
||||
"sha256:837d866a70f1ea03005914a740bddea89a253afabd6589db981b91738768bd25",
|
||||
"sha256:885e40812ff9fc80e6f28ef04ad6396e3ae583ab504b1a76301fdcec7fc9f30f",
|
||||
"sha256:a5457e075eab1170141774a8c69906c223ea0088eaebd6ef91b04b33527fa905",
|
||||
"sha256:baa0d3f7982fa0c03a55433109c405e79a597141f2e2d6ee7e16c03eabd74886",
|
||||
"sha256:beeefbe0edd47fc8b657bf7bf44791f7a6e5b14f3de1846daf999687cb68c156",
|
||||
"sha256:cf6a3d6fd3e79d3457d520c12d5d18b030d5ca5d0b205ca6481857804d8d944d",
|
||||
"sha256:d07d3dc6849345b7437dc58ea49ad2a1960017386d86288550728ca38e482ddc",
|
||||
"sha256:d81e45bedefccb97e4e8f7d32cfae0af1d9eadd1ae795fc420c8319c3dab2a28",
|
||||
"sha256:e1da2853a92fbc7e2d0248bbfa931cd621121e70ce6dda7c1eeef3516d51b46c",
|
||||
"sha256:f1201de3e93fb1efc3111c8928d9366875edefd65d77c0f6b847fe299e8e1122",
|
||||
"sha256:fe0390a29b5c7e90975feefe863e3d3a851be546bd797b23f338d24a15efa920"
|
||||
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
|
||||
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
|
||||
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
|
||||
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
|
||||
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
|
||||
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
|
||||
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
|
||||
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
|
||||
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
|
||||
],
|
||||
"version": "==0.18.0"
|
||||
"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:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
|
||||
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
|
||||
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
|
||||
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
|
||||
],
|
||||
"version": "==0.7.10"
|
||||
"version": "==0.7.11"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
@@ -212,19 +348,28 @@
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||
],
|
||||
"markers": "python_version >= '3.5.3'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
|
||||
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"version": "==1.1.5"
|
||||
"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:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
|
||||
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.1.0"
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
@@ -235,18 +380,17 @@
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
|
||||
"sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
|
||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.6b2"
|
||||
"version": "==18.9b0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
||||
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
],
|
||||
"version": "==2018.4.16"
|
||||
"version": "==2018.8.24"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -257,10 +401,24 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==6.7"
|
||||
"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": [
|
||||
@@ -270,19 +428,42 @@
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"e1839a9": {
|
||||
"editable": true,
|
||||
"extras": [
|
||||
"docs",
|
||||
"test",
|
||||
"style"
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||
],
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.6"
|
||||
"version": "==2.7"
|
||||
},
|
||||
"idna-ssl": {
|
||||
"hashes": [
|
||||
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
|
||||
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
"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": [
|
||||
@@ -299,33 +480,68 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
|
||||
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
|
||||
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
],
|
||||
"version": "==4.2.0"
|
||||
"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:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
|
||||
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||
],
|
||||
"version": "==17.1"
|
||||
"version": "==18.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
||||
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
||||
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"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:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
||||
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
||||
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
|
||||
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
|
||||
],
|
||||
"version": "==1.5.3"
|
||||
"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": [
|
||||
@@ -336,45 +552,82 @@
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
|
||||
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
|
||||
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
|
||||
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
|
||||
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
|
||||
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
|
||||
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
|
||||
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
|
||||
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
|
||||
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.6.1"
|
||||
"version": "==3.8.2"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd",
|
||||
"sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e"
|
||||
"sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
|
||||
"sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.8.0"
|
||||
"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:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
||||
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
],
|
||||
"version": "==2018.4"
|
||||
"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:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
|
||||
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.18.4"
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"schema": {
|
||||
"hashes": [
|
||||
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||
],
|
||||
"version": "==0.6.8"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -392,25 +645,22 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
|
||||
"sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
|
||||
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
|
||||
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.5"
|
||||
"version": "==1.7.9"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
|
||||
"sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
|
||||
"sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
|
||||
"sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"sphinxcontrib-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
@@ -418,35 +668,81 @@
|
||||
"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:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
|
||||
"sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42",
|
||||
"sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957"
|
||||
],
|
||||
"version": "==0.9.4"
|
||||
"version": "==0.9.6"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
"sha256:96efa09710a3daeeb845561ebbe1497641d9cef2ee0aea30db6969058b2bda2f",
|
||||
"sha256:9ee7de958a43806402a38c0d2aa07fa8553f4d2c20a15b140e9f771c2afeade0"
|
||||
"sha256:7f802b37fffd3b5ef2aab104943fa5dad24bf9564bb7e732e54b8d0cfec2fca0",
|
||||
"sha256:cc97859bd7f38aa5b3b8ba55ffe7ee9952e7050faad1aedc0829cd3db2fb61d6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
"version": "==3.4.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
||||
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.22"
|
||||
"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.
|
||||
97
README.rst
97
README.rst
@@ -1,97 +0,0 @@
|
||||
.. class:: center
|
||||
|
||||
.. image:: https://imgur.com/pY1WUFX.png
|
||||
:target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
|
||||
:alt: Red Discord Bot
|
||||
|
||||
|
||||
.. class:: center
|
||||
|
||||
Music, Moderation, Trivia, Stream Alerts and fully customizable.
|
||||
|
||||
.. class:: center
|
||||
|
||||
.. 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/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
:alt: Code style: black
|
||||
|
||||
.. 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
|
||||
|
||||
==========
|
||||
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)
|
||||
- Slot machine
|
||||
- Custom commands
|
||||
- Imgur/gif search
|
||||
|
||||
|
||||
**Additionally, other plugins (cogs) can be easily found and added from our growing community of cog repositories.**
|
||||
|
||||
- Cleverbot integration (talk to Red and she talks back)
|
||||
- Ban sync
|
||||
- Welcome messages
|
||||
- Casino
|
||||
- Reaction roles
|
||||
- Slow Mode
|
||||
- Anilist
|
||||
- And much, much more!
|
||||
|
||||
Feel free to take a `peek <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_!
|
||||
|
||||
==============
|
||||
Installation
|
||||
==============
|
||||
|
||||
**The following platforms are officially supported:**
|
||||
|
||||
- `Windows <https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html>`_
|
||||
- `MacOS <https://red-discordbot.readthedocs.io/en/v3-develop/install_mac.html>`_
|
||||
- `Ubuntu <https://red-discordbot.readthedocs.io/en/v3-develop/install_ubuntu.html>`_
|
||||
- `Debian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_debian.html>`_
|
||||
- `CentOS 7 <https://red-discordbot.readthedocs.io/en/v3-develop/install_centos.html>`_
|
||||
- `Arch Linux <https://red-discordbot.readthedocs.io/en/v3-develop/install_arch.html>`_
|
||||
- `Raspbian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_raspbian.html>`_
|
||||
|
||||
Already using **Red** V2? Take a look at the `Data Converter <https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html>`_ to import your data to V3.
|
||||
|
||||
If `after reading the guides <https://red-discordbot.readthedocs.io/en/v3-develop/>`_ you are still experiencing issues, feel free to join the `Official Server <https://discord.gg/red>`_ and ask in the **#support** channel for help.
|
||||
|
||||
=====================
|
||||
Join the community!
|
||||
=====================
|
||||
|
||||
**Red** is in continuous development, and it’s supported by an active community which produces new content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t `find <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_ what you’re looking for, consult our `guide <https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html>`_ on building your own cogs!
|
||||
|
||||
Join us on our `Official Discord Server <https://discord.gg/red>`_!
|
||||
|
||||
=========
|
||||
License
|
||||
=========
|
||||
|
||||
Released under the `GNU GPL v3 <#License>`_.
|
||||
|
||||
Red is named after the main character of "Transistor", a videogame by `Super Giant Games <https://www.supergiantgames.com/games/transistor/>`_
|
||||
|
||||
Artwork created by `Sinlaire <https://sinlaire.deviantart.com/>`_ on Deviant Art for the Red Bot Project.
|
||||
@@ -1,5 +1,5 @@
|
||||
api_key_env: CROWDIN_API_KEY
|
||||
project_identifier_env: CROWDIN_PROJECT_ID
|
||||
files:
|
||||
- source: /**/*.pot
|
||||
- source: /redbot/**/*.pot
|
||||
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
|
||||
@@ -10,7 +10,7 @@ Creating the 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):
|
||||
|
||||
|
||||
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
|
||||
@@ -8,98 +8,90 @@ 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
|
||||
When loaded, the permissions cog will allow you to define extra custom rules for who can use a
|
||||
command.
|
||||
|
||||
If no applicable rules are found, the command will behave as if
|
||||
the cog was not loaded.
|
||||
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 will be checked in the following order
|
||||
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.
|
||||
|
||||
1. Owner level command specific settings
|
||||
2. Owner level cog specific settings
|
||||
3. Server level command specific settings
|
||||
4. Server level cog specific settings
|
||||
For each of those, the first rule pertaining to one of the following models will be used:
|
||||
|
||||
For each of those, settings have varying priorities (listed below, highest to lowest priority)
|
||||
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
|
||||
|
||||
1. User whitelist
|
||||
2. User blacklist
|
||||
3. Voice Channel whitelist
|
||||
4. Voice Channel blacklist
|
||||
5. Text Channel whitelist
|
||||
6. Text Channel blacklist
|
||||
7. Role settings (see below)
|
||||
8. Server whitelist
|
||||
9. Server blacklist
|
||||
10. Default settings
|
||||
|
||||
For the role whitelist and blacklist settings,
|
||||
roles will be checked individually in order from highest to lowest role the user has
|
||||
Each role will be checked for whitelist, then blacklist. The first role with a setting
|
||||
found will be the one used.
|
||||
In private messages, only global rules about a user will be checked.
|
||||
|
||||
-------------------------
|
||||
Setting Rules from a file
|
||||
Setting Rules From a File
|
||||
-------------------------
|
||||
|
||||
The permissions cog can set rules from a yaml file:
|
||||
All entries are based on ID.
|
||||
An example of the expected format is shown below.
|
||||
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
|
||||
|
||||
cogs:
|
||||
COG:
|
||||
Admin:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
deny:
|
||||
- 96733288462286848
|
||||
78631113035100160: true
|
||||
96733288462286848: false
|
||||
Audio:
|
||||
allow:
|
||||
- 133049272517001216
|
||||
default: deny
|
||||
commands:
|
||||
133049272517001216: true
|
||||
default: false
|
||||
COMMAND:
|
||||
cleanup bot:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
default: deny
|
||||
78631113035100160: true
|
||||
default: false
|
||||
ping:
|
||||
deny:
|
||||
- 96733288462286848
|
||||
default: allow
|
||||
96733288462286848: false
|
||||
default: true
|
||||
|
||||
----------------------
|
||||
Example configurations
|
||||
----------------------
|
||||
|
||||
Locking Audio cog to approved server(s) as a bot owner
|
||||
Locking the ``[p]play`` command to approved server(s) as a bot owner:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions setglobaldefault Audio deny
|
||||
[p]permissions addglobalrule allow Audio [server ID or name]
|
||||
[p]permissions setglobaldefault play deny
|
||||
[p]permissions addglobalrule allow play [server ID or name]
|
||||
|
||||
Locking Audio to specific voice channel(s) as a serverowner or admin:
|
||||
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions setguilddefault Audio deny
|
||||
[p]permissions addguildrule allow Audio [voice channel ID or name]
|
||||
[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 cleanup
|
||||
Allowing extra roles to use ``[p]cleanup``:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions addguildrule allow Cleanup [role ID]
|
||||
[p]permissions addserverrule allow cleanup [role ID]
|
||||
|
||||
Preventing cleanup from being used in channels where message history is important:
|
||||
Preventing ``[p]cleanup`` from being used in channels where message history is important:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions addguildrule deny Cleanup [channel ID or mention]
|
||||
[p]permissions addserverrule deny cleanup [channel ID or mention]
|
||||
|
||||
17
docs/conf.py
17
docs/conf.py
@@ -39,6 +39,7 @@ extensions = [
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinxcontrib.asyncio",
|
||||
]
|
||||
|
||||
@@ -190,9 +191,23 @@ texinfo_documents = [
|
||||
]
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
# -- 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*"]
|
||||
|
||||
|
||||
# -- Options for extensions -----------------------------------------------
|
||||
|
||||
# Intersphinx
|
||||
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
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:
|
||||
@@ -5,8 +5,9 @@ Commands Package
|
||||
================
|
||||
|
||||
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||
they both have the same attributes. Some of these attributes, however, have been slightly modified,
|
||||
as outlined below.
|
||||
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
|
||||
|
||||
@@ -20,3 +21,6 @@ as outlined below.
|
||||
|
||||
.. autoclass:: redbot.core.commands.Context
|
||||
:members:
|
||||
|
||||
.. automodule:: redbot.core.commands.requires
|
||||
:members: PrivilegeLevel, PermState, Requires
|
||||
|
||||
@@ -187,6 +187,7 @@ This usage guide will cover the following features:
|
||||
|
||||
- :py:meth:`Group.get_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
|
||||
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)
|
||||
|
||||
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
|
||||
*************
|
||||
|
||||
@@ -20,6 +20,9 @@ Keys common to both repo and cog info.json (case sensitive)
|
||||
|
||||
- ``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``
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
Utility Functions
|
||||
=================
|
||||
|
||||
General Utility
|
||||
===============
|
||||
|
||||
.. automodule:: redbot.core.utils
|
||||
:members: deduplicate_iterables, bounded_gather, bounded_gather_iter
|
||||
|
||||
Chat Formatting
|
||||
===============
|
||||
|
||||
@@ -16,12 +22,18 @@ Embed Helpers
|
||||
.. automodule:: redbot.core.utils.embed
|
||||
:members:
|
||||
|
||||
Menu Helpers
|
||||
============
|
||||
Reaction Menus
|
||||
==============
|
||||
|
||||
.. automodule:: redbot.core.utils.menus
|
||||
:members:
|
||||
|
||||
Event Predicates
|
||||
================
|
||||
|
||||
.. automodule:: redbot.core.utils.predicates
|
||||
:members:
|
||||
|
||||
Mod Helpers
|
||||
===========
|
||||
|
||||
@@ -38,4 +50,10 @@ Tunnel
|
||||
======
|
||||
|
||||
.. automodule:: redbot.core.utils.tunnel
|
||||
:members: Tunnel
|
||||
:members: Tunnel
|
||||
|
||||
Common Filters
|
||||
==============
|
||||
|
||||
.. automodule:: redbot.core.utils.common_filters
|
||||
:members:
|
||||
|
||||
@@ -17,11 +17,10 @@ you in the process.
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
||||
are on Windows, stick with 3.5). 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]`
|
||||
To start off, be sure that you have installed Python 3.6.2 or higher (3.6.6 or higher on Windows).
|
||||
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]`
|
||||
(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.
|
||||
|
||||
--------------------
|
||||
Setting up a package
|
||||
@@ -45,7 +44,7 @@ In that file, place the following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
class Mycog:
|
||||
"""My custom cog"""
|
||||
|
||||
@@ -11,13 +11,8 @@ Welcome to Red - Discord Bot's documentation!
|
||||
:caption: Installation Guides:
|
||||
|
||||
install_windows
|
||||
install_mac
|
||||
install_ubuntu_xenial
|
||||
install_ubuntu_bionic
|
||||
install_debian
|
||||
install_centos
|
||||
install_arch
|
||||
install_raspbian
|
||||
install_linux_mac
|
||||
venv_guide
|
||||
cog_dataconverter
|
||||
autostart_systemd
|
||||
|
||||
@@ -25,6 +20,7 @@ Welcome to Red - Discord Bot's documentation!
|
||||
:maxdepth: 2
|
||||
:caption: Cog Reference:
|
||||
|
||||
cog_customcom
|
||||
cog_downloader
|
||||
cog_permissions
|
||||
|
||||
@@ -37,14 +33,15 @@ Welcome to Red - Discord Bot's documentation!
|
||||
guide_data_conversion
|
||||
framework_bank
|
||||
framework_bot
|
||||
framework_checks
|
||||
framework_cogmanager
|
||||
framework_commands
|
||||
framework_config
|
||||
framework_datamanager
|
||||
framework_downloader
|
||||
framework_events
|
||||
framework_i18n
|
||||
framework_modlog
|
||||
framework_commands
|
||||
framework_rpc
|
||||
framework_utils
|
||||
|
||||
|
||||
@@ -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 -Syu 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,55 +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 python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
||||
|
||||
--------------
|
||||
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,70 +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
|
||||
|
||||
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||
|
||||
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||
|
||||
Then run the following command:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||
|
||||
This may take a long time to complete.
|
||||
|
||||
After that is finished, run:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pyenv global 3.6.5
|
||||
|
||||
------------------
|
||||
Installing 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,72 +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 install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||
|
||||
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||
|
||||
Then run the following command:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||
|
||||
This may take a long time to complete.
|
||||
|
||||
After that is finished, run:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pyenv global 3.6.5
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
--------------
|
||||
|
||||
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 bionic install guide
|
||||
|
||||
==============================
|
||||
Installing Red on Ubuntu 18.04
|
||||
==============================
|
||||
|
||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||
|
||||
-------------------------------
|
||||
Installing the pre-requirements
|
||||
-------------------------------
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt install python3.6-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,59 +0,0 @@
|
||||
.. ubuntu xenial install guide
|
||||
|
||||
==============================
|
||||
Installing Red on Ubuntu 16.04
|
||||
==============================
|
||||
|
||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||
|
||||
-------------------------------
|
||||
Installing the pre-requirements
|
||||
-------------------------------
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt install software-properties-common
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt update
|
||||
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
|
||||
wget https://bootstrap.pypa.io/get-pip.py
|
||||
sudo python3.6 get-pip.py
|
||||
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
------------------
|
||||
|
||||
To install without audio:
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links red-discordbot --user`
|
||||
|
||||
To install with audio:
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links red-discordbot[voice] --user`
|
||||
|
||||
To install the development version (without audio):
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
|
||||
|
||||
To install the development version (with audio):
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
|
||||
|
||||
------------------------
|
||||
Setting up your instance
|
||||
------------------------
|
||||
|
||||
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
|
||||
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
|
||||
for confirmation of that selection. Next, it will ask you to choose your storage backend
|
||||
(the default here is JSON). It will then ask for a name for your instance. This can be
|
||||
anything as long as it does not contain spaces; however, keep in mind that this is the
|
||||
name you will use to run your bot, and so it should be something you can remember.
|
||||
|
||||
-----------
|
||||
Running Red
|
||||
-----------
|
||||
|
||||
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
|
||||
your token and a prefix.
|
||||
@@ -1,4 +1,4 @@
|
||||
.. windows installation docs
|
||||
.. _windows-install-guide:
|
||||
|
||||
=========================
|
||||
Installing Red on Windows
|
||||
@@ -8,7 +8,7 @@ Installing Red on Windows
|
||||
Needed Software
|
||||
---------------
|
||||
|
||||
* `Python <https://python.org/downloads/>`_ - Red needs Python 3.6
|
||||
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6.6 or greater on Windows
|
||||
|
||||
.. 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
|
||||
@@ -21,23 +21,74 @@ Needed Software
|
||||
|
||||
.. attention:: Please choose the "Windows Online" installer
|
||||
|
||||
.. _installing-red-windows:
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
--------------
|
||||
|
||||
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`
|
||||
* 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]`
|
||||
.. note::
|
||||
|
||||
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
|
||||
storage backend and the name of the instance (which will be used for
|
||||
running the bot)
|
||||
* No audio:
|
||||
|
||||
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red.
|
||||
It will walk through the initial setup, asking for your token and a prefix
|
||||
.. code-block:: none
|
||||
|
||||
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,37 +0,0 @@
|
||||
-i https://pypi.org/simple
|
||||
alabaster==0.7.10
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.1.5
|
||||
attrs==18.1.0
|
||||
babel==2.6.0
|
||||
black==18.6b2
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
click==6.7
|
||||
docutils==0.14
|
||||
idna==2.6
|
||||
imagesize==1.0.0
|
||||
jinja2==2.10
|
||||
markupsafe==1.0
|
||||
more-itertools==4.2.0
|
||||
packaging==17.1
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pygments==2.2.0
|
||||
pyparsing==2.2.0
|
||||
pytest-asyncio==0.8.0
|
||||
pytest==3.6.1
|
||||
pytz==2018.4
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
sphinx-rtd-theme==0.4.0
|
||||
sphinx==1.7.5
|
||||
sphinxcontrib-asyncio==0.2.0
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
toml==0.9.4
|
||||
tox==3.0.0
|
||||
urllib3==1.22
|
||||
virtualenv==16.0.0
|
||||
yarl==0.18.0
|
||||
git+https://github.com/Rapptz/discord.py@7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0
|
||||
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,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
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(
|
||||
f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
|
||||
)
|
||||
return 1
|
||||
else:
|
||||
print("Running 'regen_messages.py' for {}".format(d))
|
||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||
if retval.returncode != 0:
|
||||
return 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:
|
||||
return 1
|
||||
os.chdir(root_dir)
|
||||
subprocess.run(["crowdin", "upload"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
4
make.bat
4
make.bat
@@ -14,11 +14,11 @@ for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
||||
goto %1
|
||||
|
||||
:reformat
|
||||
black -l 99 !PYFILES!
|
||||
black -l 99 -N !PYFILES!
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:stylecheck
|
||||
black -l 99 --check !PYFILES!
|
||||
black -l 99 -N --check !PYFILES!
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:help
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import sys
|
||||
import warnings
|
||||
import discord
|
||||
from colorama import init, Back
|
||||
import colorama
|
||||
|
||||
init()
|
||||
# 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:
|
||||
print(
|
||||
"You are not running the rewritten version of discord.py.\n\n"
|
||||
"In order to use Red v3 you MUST be running d.py version"
|
||||
" >= 1.0.0."
|
||||
"In order to use Red V3 you MUST be running d.py version "
|
||||
"1.0.0 or greater."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if sys.version_info < (3, 6, 0):
|
||||
print(Back.RED + "[DEPRECATION WARNING]")
|
||||
print(
|
||||
Back.RED + "You are currently running Python 3.5."
|
||||
" Support for Python 3.5 will end with the release of beta 16."
|
||||
" Please update your environment to Python 3.6 as soon as possible to avoid"
|
||||
" any interruptions after the beta 16 release."
|
||||
)
|
||||
|
||||
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,7 +6,7 @@ import sys
|
||||
import discord
|
||||
from redbot.core.bot import Red, ExitCodes
|
||||
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.global_checks import init_global_checks
|
||||
from redbot.core.events import init_events
|
||||
@@ -19,6 +19,18 @@ import logging.handlers
|
||||
import logging
|
||||
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
|
||||
@@ -51,7 +63,7 @@ def init_loggers(cli_flags):
|
||||
os.environ["PYTHONASYNCIODEBUG"] = "1"
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
from redbot.core.data_manager import core_data_path
|
||||
|
||||
@@ -106,9 +118,17 @@ def main():
|
||||
elif cli_flags.version:
|
||||
print(description)
|
||||
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!")
|
||||
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)
|
||||
log, sentry_log = init_loggers(cli_flags)
|
||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||
@@ -122,6 +142,8 @@ def main():
|
||||
tmp_data = {}
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||
if cli_flags.token:
|
||||
token = cli_flags.token
|
||||
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||
if not (token and prefix):
|
||||
if cli_flags.no_prompt is False:
|
||||
@@ -139,14 +161,9 @@ def main():
|
||||
if tmp_data["enable_sentry"]:
|
||||
red.enable_sentry()
|
||||
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:
|
||||
log.critical(
|
||||
"This token doesn't seem to be valid. If it belongs to "
|
||||
"a user account, remember that the --not-bot flag "
|
||||
"must be used. For self-bot functionalities instead, "
|
||||
"--self-bot"
|
||||
)
|
||||
log.critical("This token doesn't seem to be valid.")
|
||||
db_token = loop.run_until_complete(red.db.token())
|
||||
if db_token and not cli_flags.no_prompt:
|
||||
print("\nDo you want to reset the token? (y/n)")
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
import logging
|
||||
from typing import Tuple
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import Config, checks, commands
|
||||
|
||||
import logging
|
||||
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from .announcer import Announcer
|
||||
from .converters import MemberDefaultAuthor, SelfRole
|
||||
|
||||
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."
|
||||
" Your command failed to successfully complete."
|
||||
)
|
||||
|
||||
HIERARCHY_ISSUE = (
|
||||
HIERARCHY_ISSUE = _(
|
||||
"I tried to add {role.name} to {member.display_name} but that role"
|
||||
" 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 "
|
||||
"try again."
|
||||
)
|
||||
|
||||
USER_HIERARCHY_ISSUE = (
|
||||
USER_HIERARCHY_ISSUE = _(
|
||||
"I tried to add {role.name} to {member.display_name} but that role"
|
||||
" is higher than your highest role in the Discord hierarchy so I was"
|
||||
" unable to successfully add it. Please get a higher role and "
|
||||
"try again."
|
||||
)
|
||||
|
||||
RUNNING_ANNOUNCEMENT = (
|
||||
RUNNING_ANNOUNCEMENT = _(
|
||||
"I am already announcing something. If you would like to make a"
|
||||
" different announcement please use `{prefix}announce cancel`"
|
||||
" first."
|
||||
)
|
||||
_ = T_
|
||||
|
||||
|
||||
class Admin:
|
||||
@cog_i18n(_)
|
||||
class Admin(commands.Cog):
|
||||
"""A collection of server administration utilities."""
|
||||
|
||||
def __init__(self, config=Config):
|
||||
super().__init__()
|
||||
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||
|
||||
self.conf.register_global(serverlocked=False)
|
||||
@@ -97,13 +104,14 @@ class Admin:
|
||||
await member.add_roles(role)
|
||||
except discord.Forbidden:
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||
else:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||
else:
|
||||
await ctx.send(
|
||||
"I successfully added {role.name} to"
|
||||
" {member.display_name}".format(role=role, member=member)
|
||||
_("I successfully added {role.name} to {member.display_name}").format(
|
||||
role=role, member=member
|
||||
)
|
||||
)
|
||||
|
||||
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||
@@ -111,13 +119,14 @@ class Admin:
|
||||
await member.remove_roles(role)
|
||||
except discord.Forbidden:
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||
else:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||
else:
|
||||
await ctx.send(
|
||||
"I successfully removed {role.name} from"
|
||||
" {member.display_name}".format(role=role, member=member)
|
||||
_("I successfully removed {role.name} from {member.display_name}").format(
|
||||
role=role, member=member
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@@ -126,9 +135,9 @@ class Admin:
|
||||
async def addrole(
|
||||
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
|
||||
author of the command.
|
||||
"""Add a role to a user.
|
||||
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -136,7 +145,7 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
await self._addrole(ctx, user, rolename)
|
||||
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.guild_only()
|
||||
@@ -144,9 +153,9 @@ class Admin:
|
||||
async def removerole(
|
||||
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
|
||||
author of the command.
|
||||
"""Remove a role from a user.
|
||||
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -154,50 +163,54 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
await self._removerole(ctx, user, rolename)
|
||||
else:
|
||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def editrole(self, ctx: commands.Context):
|
||||
"""Edits roles settings"""
|
||||
"""Edit role settings."""
|
||||
pass
|
||||
|
||||
@editrole.command(name="colour", aliases=["color"])
|
||||
async def editrole_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.
|
||||
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"""
|
||||
`[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, USER_HIERARCHY_ISSUE)
|
||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||
return
|
||||
|
||||
try:
|
||||
await role.edit(reason=reason, color=value)
|
||||
except discord.Forbidden:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||
else:
|
||||
log.info(reason)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@editrole.command(name="name")
|
||||
@checks.admin_or_permissions(administrator=True)
|
||||
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.
|
||||
|
||||
Examples:
|
||||
!editrole name \"The Transistor\" Test"""
|
||||
`[p]editrole name \"The Transistor\" Test`
|
||||
"""
|
||||
author = ctx.message.author
|
||||
old_name = role.name
|
||||
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
||||
@@ -205,73 +218,74 @@ class Admin:
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
await role.edit(reason=reason, name=name)
|
||||
except discord.Forbidden:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||
else:
|
||||
log.info(reason)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@checks.is_owner()
|
||||
async def announce(self, ctx: commands.Context, *, message: str):
|
||||
"""
|
||||
Announces a message to all servers the bot is in.
|
||||
"""
|
||||
"""Announce a message to all servers the bot is in."""
|
||||
if not self.is_announcing():
|
||||
announcer = Announcer(ctx, message, config=self.conf)
|
||||
announcer.start()
|
||||
|
||||
self.__current_announcer = announcer
|
||||
|
||||
await ctx.send("The announcement has begun.")
|
||||
await ctx.send(_("The announcement has begun."))
|
||||
else:
|
||||
prefix = ctx.prefix
|
||||
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
|
||||
await self.complain(ctx, T_(RUNNING_ANNOUNCEMENT), prefix=prefix)
|
||||
|
||||
@announce.command(name="cancel")
|
||||
@checks.is_owner()
|
||||
async def announce_cancel(self, ctx):
|
||||
"""
|
||||
Cancels a running announce.
|
||||
"""
|
||||
"""Cancel a running announce."""
|
||||
try:
|
||||
self.__current_announcer.cancel()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
await ctx.send("The current announcement has been cancelled.")
|
||||
await ctx.send(_("The current announcement has been cancelled."))
|
||||
|
||||
@announce.command(name="channel")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
|
||||
"""
|
||||
Changes the channel on which the bot makes announcements.
|
||||
"""
|
||||
"""Change the channel to which the bot makes announcements."""
|
||||
if channel is None:
|
||||
channel = ctx.channel
|
||||
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
||||
|
||||
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
|
||||
await ctx.send(
|
||||
_("The announcement channel has been set to {channel.mention}").format(channel=channel)
|
||||
)
|
||||
|
||||
@announce.command(name="ignore")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_ignore(self, ctx):
|
||||
"""
|
||||
Toggles whether the announcements will ignore the current server.
|
||||
"""
|
||||
"""Toggle announcements being enabled this server."""
|
||||
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||
|
||||
verb = "will" if ignored else "will not"
|
||||
|
||||
await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
|
||||
if ignored: # Keeping original logic....
|
||||
await ctx.send(
|
||||
_("The server {guild.name} will receive announcements.").format(guild=ctx.guild)
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_("The server {guild.name} will not receive announcements.").format(
|
||||
guild=ctx.guild
|
||||
)
|
||||
)
|
||||
|
||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||
"""
|
||||
@@ -291,11 +305,12 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
return valid_roles
|
||||
|
||||
@commands.guild_only()
|
||||
@commands.group(invoke_without_command=True)
|
||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Add a role to yourself that server admins have configured as
|
||||
user settable.
|
||||
"""Add a role to yourself.
|
||||
|
||||
Server admins must have configured the role as user settable.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
@@ -304,8 +319,7 @@ class Admin:
|
||||
|
||||
@selfrole.command(name="remove")
|
||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Removes a selfrole from yourself.
|
||||
"""Remove a selfrole from yourself.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
@@ -313,10 +327,9 @@ class Admin:
|
||||
await self._removerole(ctx, ctx.author, selfrole)
|
||||
|
||||
@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):
|
||||
"""
|
||||
Add a role to the list of available selfroles.
|
||||
"""Add a role to the list of available selfroles.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
@@ -324,20 +337,19 @@ class Admin:
|
||||
if role.id not in curr_selfroles:
|
||||
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")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||
"""
|
||||
Removes a role from the list of available selfroles.
|
||||
"""Remove 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:
|
||||
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")
|
||||
async def selfrole_list(self, ctx: commands.Context):
|
||||
@@ -347,7 +359,7 @@ class Admin:
|
||||
selfroles = await self._valid_selfroles(ctx.guild)
|
||||
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"))
|
||||
|
||||
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
||||
@@ -364,15 +376,14 @@ class Admin:
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def serverlock(self, ctx: commands.Context):
|
||||
"""
|
||||
Locks a bot to its current servers only.
|
||||
"""
|
||||
"""Lock a bot to its current servers only."""
|
||||
serverlocked = await self.conf.serverlocked()
|
||||
await self.conf.serverlocked.set(not serverlocked)
|
||||
|
||||
verb = "is now" if not serverlocked else "is no longer"
|
||||
|
||||
await ctx.send("The bot {} serverlocked.".format(verb))
|
||||
if serverlocked:
|
||||
await ctx.send(_("The bot is no longer serverlocked."))
|
||||
else:
|
||||
await ctx.send(_("The bot is now serverlocked."))
|
||||
|
||||
# region Event Handlers
|
||||
async def on_guild_join(self, guild: discord.Guild):
|
||||
|
||||
@@ -2,6 +2,9 @@ import asyncio
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("Announcer", __file__)
|
||||
|
||||
|
||||
class Announcer:
|
||||
@@ -63,7 +66,9 @@ class Announcer:
|
||||
try:
|
||||
await channel.send(self.message)
|
||||
except discord.Forbidden:
|
||||
await bot_owner.send("I could not announce to server: {}".format(g.id))
|
||||
await bot_owner.send(
|
||||
_("I could not announce to server: {server.id}").format(server=g)
|
||||
)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
self.active = False
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("AdminConverters", __file__)
|
||||
|
||||
|
||||
class MemberDefaultAuthor(commands.Converter):
|
||||
@@ -19,7 +22,7 @@ class SelfRole(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
||||
admin = ctx.command.instance
|
||||
if admin is None:
|
||||
raise commands.BadArgument("Admin is not loaded.")
|
||||
raise commands.BadArgument(_("The Admin cog is not loaded."))
|
||||
|
||||
conf = admin.conf
|
||||
selfroles = await conf.guild(ctx.guild).selfroles()
|
||||
@@ -28,5 +31,5 @@ class SelfRole(commands.Converter):
|
||||
role = await role_converter.convert(ctx, arg)
|
||||
|
||||
if role.id not in selfroles:
|
||||
raise commands.BadArgument("The provided role is not a valid selfrole.")
|
||||
raise commands.BadArgument(_("The provided role is not a valid selfrole."))
|
||||
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,11 +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 copy import copy
|
||||
from re import search
|
||||
from typing import Generator, Tuple, Iterable
|
||||
from typing import Generator, Tuple, Iterable, Optional
|
||||
|
||||
import discord
|
||||
from redbot.core import Config, commands, checks
|
||||
@@ -14,16 +14,15 @@ _ = Translator("Alias", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Alias:
|
||||
"""
|
||||
Alias
|
||||
|
||||
Aliases are per server shortcuts for commands. They
|
||||
can act as both a lambda (storing arguments for repeated use)
|
||||
or as simply a shortcut to saying "x y z".
|
||||
|
||||
class Alias(commands.Cog):
|
||||
"""Create aliases for commands.
|
||||
|
||||
Aliases are alternative names shortcuts for commands. They
|
||||
can act as both a lambda (storing arguments for repeated use)
|
||||
or as simply a shortcut to saying "x y z".
|
||||
|
||||
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 = {"entries": []}
|
||||
@@ -31,6 +30,7 @@ class Alias:
|
||||
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self._aliases = Config.get_conf(self, 8927348724)
|
||||
|
||||
@@ -53,10 +53,13 @@ class Alias:
|
||||
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, 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)
|
||||
|
||||
global_aliases = await self.unloaded_global_aliases()
|
||||
@@ -170,35 +173,31 @@ class Alias:
|
||||
new_message.content = "{}{} {}".format(prefix, alias.command, args)
|
||||
await self.bot.process_commands(new_message)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
async def alias(self, ctx: commands.Context):
|
||||
"""Manage per-server aliases for commands"""
|
||||
"""Manage command aliases."""
|
||||
pass
|
||||
|
||||
@alias.group(name="global", autohelp=True)
|
||||
@alias.group(name="global")
|
||||
async def global_(self, ctx: commands.Context):
|
||||
"""
|
||||
Manage global aliases.
|
||||
"""
|
||||
"""Manage global aliases."""
|
||||
pass
|
||||
|
||||
@checks.mod_or_permissions(manage_guild=True)
|
||||
@alias.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||
"""
|
||||
Add an alias for a command.
|
||||
"""
|
||||
"""Add an alias for a command."""
|
||||
# region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" name is already a command on this bot."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -207,9 +206,9 @@ class Alias:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" alias already exists on this server."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -218,10 +217,10 @@ class Alias:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
# endregion
|
||||
@@ -231,23 +230,23 @@ class Alias:
|
||||
|
||||
await self.add_alias(ctx, alias_name, command)
|
||||
|
||||
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
|
||||
await ctx.send(
|
||||
_("A new alias with the trigger `{name}` has been created.").format(name=alias_name)
|
||||
)
|
||||
|
||||
@checks.is_owner()
|
||||
@global_.command(name="add")
|
||||
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||
"""
|
||||
Add a global alias for a command.
|
||||
"""
|
||||
"""Add a global alias for a command."""
|
||||
# region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" name is already a command on this bot."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -256,9 +255,9 @@ class Alias:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" alias already exists on this server."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -267,10 +266,10 @@ class Alias:
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" with the name {name} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces."
|
||||
).format(alias_name)
|
||||
).format(name=alias_name)
|
||||
)
|
||||
return
|
||||
# endregion
|
||||
@@ -278,63 +277,65 @@ class Alias:
|
||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||
|
||||
await ctx.send(
|
||||
_("A new global alias with the trigger `{}` 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")
|
||||
@commands.guild_only()
|
||||
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 = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||
if is_alias:
|
||||
base_cmd = alias.command[0]
|
||||
|
||||
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)
|
||||
else:
|
||||
ctx.send(_("No such alias exists."))
|
||||
await ctx.send(_("No such alias exists."))
|
||||
|
||||
@alias.command(name="show")
|
||||
@commands.guild_only()
|
||||
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)
|
||||
|
||||
if is_alias:
|
||||
await ctx.send(
|
||||
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
|
||||
_("The `{alias_name}` alias will execute the command `{command}`").format(
|
||||
alias_name=alias_name, command=alias.command
|
||||
)
|
||||
)
|
||||
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")
|
||||
@commands.guild_only()
|
||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""
|
||||
Deletes an existing alias on this server.
|
||||
"""
|
||||
"""Delete an existing alias on this server."""
|
||||
aliases = await self.unloaded_aliases(ctx.guild)
|
||||
try:
|
||||
next(aliases)
|
||||
except StopIteration:
|
||||
await ctx.send(_("There are no aliases on this guild."))
|
||||
await ctx.send(_("There are no aliases on this server."))
|
||||
return
|
||||
|
||||
if await self.delete_alias(ctx, alias_name):
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
|
||||
)
|
||||
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")
|
||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""
|
||||
Deletes an existing global alias.
|
||||
"""
|
||||
"""Delete an existing global alias."""
|
||||
aliases = await self.unloaded_global_aliases()
|
||||
try:
|
||||
next(aliases)
|
||||
@@ -344,17 +345,15 @@ class Alias:
|
||||
|
||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
|
||||
)
|
||||
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")
|
||||
@commands.guild_only()
|
||||
async def _list_alias(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists the available aliases on this server.
|
||||
"""
|
||||
"""List the available aliases on this server."""
|
||||
names = [_("Aliases:")] + sorted(
|
||||
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
|
||||
)
|
||||
@@ -365,9 +364,7 @@ class Alias:
|
||||
|
||||
@global_.command(name="list")
|
||||
async def _list_global_alias(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists the available global aliases on this bot.
|
||||
"""
|
||||
"""List the available global aliases on this bot."""
|
||||
names = [_("Aliases:")] + sorted(
|
||||
["+ " + a.name for a in await self.unloaded_global_aliases()]
|
||||
)
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../alias.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -19,7 +19,7 @@ LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
||||
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
||||
|
||||
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):
|
||||
@@ -34,7 +34,7 @@ async def download_lavalink(session):
|
||||
|
||||
async def maybe_download_lavalink(loop, cog):
|
||||
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())
|
||||
|
||||
if not jar_exists or current_build < redbot.core.version_info:
|
||||
log.info("Downloading Lavalink.jar")
|
||||
@@ -52,6 +52,6 @@ async def setup(bot: commands.Bot):
|
||||
await maybe_download_lavalink(bot.loop, cog)
|
||||
await start_lavalink_server(bot.loop)
|
||||
|
||||
await cog.initialize()
|
||||
|
||||
bot.add_cog(cog)
|
||||
bot.loop.create_task(cog.disconnect_timer())
|
||||
bot.loop.create_task(cog.init_config())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ lavalink:
|
||||
vimeo: true
|
||||
mixer: true
|
||||
http: true
|
||||
local: false
|
||||
local: true
|
||||
sentryDsn: ""
|
||||
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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../audio.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -1,9 +1,11 @@
|
||||
import shlex
|
||||
import shutil
|
||||
import asyncio
|
||||
from subprocess import Popen, DEVNULL, PIPE
|
||||
import asyncio.subprocess
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
from subprocess import Popen, DEVNULL
|
||||
from typing import Optional, Tuple
|
||||
|
||||
_JavaVersion = Tuple[int, int]
|
||||
@@ -45,23 +47,42 @@ async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
|
||||
return False, None
|
||||
|
||||
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) -> _JavaVersion:
|
||||
"""
|
||||
This assumes we've already checked that java exists.
|
||||
"""
|
||||
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
|
||||
_, err = proc.communicate()
|
||||
_proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
|
||||
"java",
|
||||
"-version",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
loop=loop,
|
||||
)
|
||||
# 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+)?"')
|
||||
|
||||
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)
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
async def start_lavalink_server(loop):
|
||||
|
||||
@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -36,32 +38,36 @@ def check_global_setting_admin():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
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
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Bank:
|
||||
class Bank(commands.Cog):
|
||||
"""Bank"""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
|
||||
# SECTION commands
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@check_global_setting_guildowner()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.group(autohelp=True)
|
||||
async def bankset(self, ctx: commands.Context):
|
||||
"""Base command for bank settings"""
|
||||
"""Base command for bank settings."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
if await bank.is_global():
|
||||
bank_name = await bank._conf.bank_name()
|
||||
@@ -75,42 +81,47 @@ class Bank:
|
||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||
|
||||
settings = _(
|
||||
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
|
||||
).format(bank_name, currency_name, default_balance)
|
||||
"Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n"
|
||||
"Default balance: {default_balance}"
|
||||
).format(
|
||||
bank_name=bank_name, currency_name=currency_name, default_balance=default_balance
|
||||
)
|
||||
await ctx.send(box(settings))
|
||||
|
||||
@bankset.command(name="toggleglobal")
|
||||
@checks.is_owner()
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
|
||||
"""Toggles 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"""
|
||||
"""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.
|
||||
"""
|
||||
cur_setting = await bank.is_global()
|
||||
|
||||
word = _("per-server") if cur_setting else _("global")
|
||||
if confirm is False:
|
||||
await ctx.send(
|
||||
_(
|
||||
"This will toggle the bank to be {}, deleting all accounts "
|
||||
"in the process! If you're sure, type `{}`"
|
||||
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
|
||||
"This will toggle the bank to be {banktype}, deleting all accounts "
|
||||
"in the process! If you're sure, type `{command}`"
|
||||
).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix))
|
||||
)
|
||||
else:
|
||||
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")
|
||||
@check_global_setting_guildowner()
|
||||
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 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")
|
||||
@check_global_setting_guildowner()
|
||||
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 ctx.send(_("Currency name has been set to {}").format(name))
|
||||
await ctx.send(_("Currency name has been set to: {name}").format(name=name))
|
||||
|
||||
# ENDSECTION
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../bank.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -1,4 +1,6 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Union, List, Callable
|
||||
|
||||
import discord
|
||||
|
||||
@@ -7,15 +9,17 @@ from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||
from redbot.cogs.mod.log import log
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
_ = Translator("Cleanup", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Cleanup:
|
||||
"""Commands for cleaning messages"""
|
||||
class Cleanup(commands.Cog):
|
||||
"""Commands for cleaning up messages."""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
|
||||
@staticmethod
|
||||
@@ -28,19 +32,16 @@ class Cleanup:
|
||||
Tries its best to cleanup after itself if the response is positive.
|
||||
"""
|
||||
|
||||
def author_check(message):
|
||||
return message.author == ctx.author
|
||||
|
||||
prompt = await ctx.send(
|
||||
_("Are you sure you want to delete {} messages? (y/n)").format(number)
|
||||
_("Are you sure you want to delete {number} messages? (y/n)").format(number=number)
|
||||
)
|
||||
response = await ctx.bot.wait_for("message", check=author_check)
|
||||
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
|
||||
|
||||
if response.content.lower().startswith("y"):
|
||||
await prompt.delete()
|
||||
try:
|
||||
await response.delete()
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
return True
|
||||
else:
|
||||
@@ -49,53 +50,61 @@ class Cleanup:
|
||||
|
||||
@staticmethod
|
||||
async def get_messages_for_deletion(
|
||||
ctx: commands.Context,
|
||||
*,
|
||||
channel: discord.TextChannel,
|
||||
number,
|
||||
check=lambda x: True,
|
||||
limit=100,
|
||||
before=None,
|
||||
after=None,
|
||||
delete_pinned=False,
|
||||
) -> 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.
|
||||
|
||||
Generally, the requirements are:
|
||||
- We don't have the number of messages to be deleted already
|
||||
- The message passes a provided check (if no check is provided,
|
||||
this is automatically true)
|
||||
- The message is less than 14 days old
|
||||
- The message is not pinned
|
||||
|
||||
Warning: Due to the way the API hands messages back in chunks,
|
||||
passing after and a number together is not advisable.
|
||||
If you need to accomplish this, you should filter messages on
|
||||
the entire applicable range, rather than use this utility.
|
||||
"""
|
||||
to_delete = []
|
||||
too_old = False
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
message = None
|
||||
async for message in channel.history(limit=limit, before=before, after=after):
|
||||
if (
|
||||
(not number or len(to_delete) - 1 < number)
|
||||
and check(message)
|
||||
and (ctx.message.created_at - message.created_at).days < 14
|
||||
and (delete_pinned or not message.pinned)
|
||||
):
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
break
|
||||
elif number and len(to_delete) >= number:
|
||||
break
|
||||
if message is None:
|
||||
# This isn't actually two weeks ago to allow some wiggle room on API limits
|
||||
two_weeks_ago = datetime.utcnow() - timedelta(days=14, minutes=-5)
|
||||
|
||||
def message_filter(message):
|
||||
return (
|
||||
check(message)
|
||||
and message.created_at > two_weeks_ago
|
||||
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
|
||||
else:
|
||||
before = message
|
||||
return to_delete
|
||||
if check(message):
|
||||
collected.append(message)
|
||||
if number and number <= len(collected):
|
||||
break
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
return collected
|
||||
|
||||
@commands.group()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: commands.Context):
|
||||
"""Deletes messages."""
|
||||
"""Delete messages."""
|
||||
pass
|
||||
|
||||
@cleanup.command()
|
||||
@@ -104,16 +113,17 @@ class Cleanup:
|
||||
async def text(
|
||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
"""Deletes last X messages matching the specified text.
|
||||
"""Delete the last X messages matching the specified text.
|
||||
|
||||
Example:
|
||||
cleanup text \"test\" 5
|
||||
`[p]cleanup text "test" 5`
|
||||
|
||||
Remember to use double quotes."""
|
||||
Remember to use double quotes.
|
||||
"""
|
||||
|
||||
channel = ctx.channel
|
||||
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
@@ -129,11 +139,9 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
channel=channel,
|
||||
number=number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
@@ -143,10 +151,7 @@ class Cleanup:
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
await mass_purge(to_delete, channel)
|
||||
else:
|
||||
await slow_deletion(to_delete)
|
||||
await mass_purge(to_delete, channel)
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@@ -154,11 +159,13 @@ class Cleanup:
|
||||
async def user(
|
||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
"""Deletes last X messages from specified user.
|
||||
"""Delete the last X messages from a specified user.
|
||||
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
cleanup user Red 6"""
|
||||
`[p]cleanup user @\u200bTwentysix 2`
|
||||
`[p]cleanup user Red 6`
|
||||
"""
|
||||
channel = ctx.channel
|
||||
|
||||
member = None
|
||||
try:
|
||||
@@ -171,9 +178,7 @@ class Cleanup:
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
@@ -189,11 +194,9 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
channel=channel,
|
||||
number=number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
@@ -204,41 +207,61 @@ class Cleanup:
|
||||
)
|
||||
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)
|
||||
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):
|
||||
"""Deletes all messages after specified message.
|
||||
"""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.
|
||||
|
||||
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
|
||||
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(
|
||||
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
||||
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(
|
||||
@@ -252,23 +275,22 @@ class Cleanup:
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Deletes last X messages.
|
||||
"""Delete the last X messages.
|
||||
|
||||
Example:
|
||||
cleanup messages 26"""
|
||||
`[p]cleanup messages 26`
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned
|
||||
channel=channel, number=number, before=ctx.message, delete_pinned=delete_pinned
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
@@ -277,20 +299,16 @@ class Cleanup:
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
await mass_purge(to_delete, channel)
|
||||
else:
|
||||
await slow_deletion(to_delete)
|
||||
await mass_purge(to_delete, channel)
|
||||
|
||||
@cleanup.command(name="bot")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
@@ -317,11 +335,9 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
channel=channel,
|
||||
number=number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
@@ -334,10 +350,7 @@ class Cleanup:
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
await mass_purge(to_delete, channel)
|
||||
else:
|
||||
await slow_deletion(to_delete)
|
||||
await mass_purge(to_delete, channel)
|
||||
|
||||
@cleanup.command(name="self")
|
||||
async def cleanup_self(
|
||||
@@ -347,7 +360,7 @@ class Cleanup:
|
||||
match_pattern: str = None,
|
||||
delete_pinned: bool = False,
|
||||
):
|
||||
"""Cleans up messages owned by the bot.
|
||||
"""Clean up messages owned by the bot.
|
||||
|
||||
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 ),
|
||||
@@ -359,7 +372,6 @@ class Cleanup:
|
||||
"""
|
||||
channel = ctx.channel
|
||||
author = ctx.message.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
@@ -399,20 +411,14 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
channel=channel,
|
||||
number=number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
|
||||
# Selfbot convenience, delete trigger message
|
||||
if author == self.bot.user:
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
if channel.name:
|
||||
if ctx.guild:
|
||||
channel_name = "channel " + channel.name
|
||||
else:
|
||||
channel_name = str(channel)
|
||||
@@ -424,7 +430,7 @@ class Cleanup:
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot and can_mass_purge:
|
||||
if can_mass_purge:
|
||||
await mass_purge(to_delete, channel)
|
||||
else:
|
||||
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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../cleanup.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -1,13 +1,16 @@
|
||||
import os
|
||||
import re
|
||||
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
|
||||
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
_ = Translator("CustomCommands", __file__)
|
||||
|
||||
@@ -16,11 +19,19 @@ class CCError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyExists(CCError):
|
||||
pass
|
||||
|
||||
|
||||
class ArgParseError(CCError):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(CCError):
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyExists(CCError):
|
||||
class OnCooldown(CCError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -40,24 +51,31 @@ class CommandObj:
|
||||
|
||||
async def get_responses(self, ctx):
|
||||
intro = _(
|
||||
"Welcome to the interactive random {} maker!\n"
|
||||
"Welcome to the interactive random {cc} maker!\n"
|
||||
"Every message you send will be added as one of the random "
|
||||
"responses to choose from once this {} is "
|
||||
"triggered. To exit this interactive menu, type `{}`"
|
||||
).format("customcommand", "customcommand", "exit()")
|
||||
"responses to choose from once this {cc} is "
|
||||
"triggered. To exit this interactive menu, type `{quit}`"
|
||||
).format(cc="customcommand", quit="exit()")
|
||||
await ctx.send(intro)
|
||||
|
||||
def check(m):
|
||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||
|
||||
responses = []
|
||||
args = None
|
||||
while True:
|
||||
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()":
|
||||
break
|
||||
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)
|
||||
return responses
|
||||
|
||||
@@ -66,58 +84,89 @@ class CommandObj:
|
||||
# in the ccinfo dict
|
||||
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||
|
||||
async def get(self, message: discord.Message, command: str) -> str:
|
||||
async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
|
||||
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
||||
if not ccinfo:
|
||||
raise NotFound
|
||||
raise NotFound()
|
||||
else:
|
||||
return ccinfo["response"]
|
||||
return ccinfo["response"], ccinfo.get("cooldowns", {})
|
||||
|
||||
async def create(self, ctx: commands.Context, command: str, response):
|
||||
async def create(self, ctx: commands.Context, command: str, *, response):
|
||||
"""Create a custom command"""
|
||||
# Check if this command is already registered as a customcommand
|
||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
raise AlreadyExists()
|
||||
# test to raise
|
||||
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
||||
author = ctx.message.author
|
||||
ccinfo = {
|
||||
"author": {"id": author.id, "name": author.name},
|
||||
"command": command,
|
||||
"cooldowns": {},
|
||||
"created_at": self.get_now(),
|
||||
"editors": [],
|
||||
"response": response,
|
||||
}
|
||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||
|
||||
async def edit(self, ctx: commands.Context, command: str, response: None):
|
||||
async def edit(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
command: str,
|
||||
*,
|
||||
response=None,
|
||||
cooldowns: Mapping[str, int] = None,
|
||||
ask_for: bool = True
|
||||
):
|
||||
"""Edit an already existing custom command"""
|
||||
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
||||
|
||||
# Check if this command is registered
|
||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
if not ccinfo:
|
||||
raise NotFound()
|
||||
|
||||
author = ctx.message.author
|
||||
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
||||
|
||||
def check(m):
|
||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||
if ask_for and not response:
|
||||
await ctx.send(_("Do you want to create a 'randomized' custom command? (y/n)"))
|
||||
|
||||
if not response:
|
||||
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
|
||||
|
||||
msg = await self.bot.wait_for("message", check=check)
|
||||
if msg.content.lower() == "y":
|
||||
pred = MessagePredicate.yes_or_no(ctx)
|
||||
try:
|
||||
await self.bot.wait_for("message", check=pred, timeout=30)
|
||||
except TimeoutError:
|
||||
await ctx.send(_("Response timed out, please try again later."))
|
||||
return
|
||||
if pred.result is True:
|
||||
response = await self.get_responses(ctx=ctx)
|
||||
else:
|
||||
await ctx.send(_("What response do you want?"))
|
||||
response = (await self.bot.wait_for("message", check=check)).content
|
||||
try:
|
||||
resp = await self.bot.wait_for(
|
||||
"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
|
||||
ccinfo["edited_at"] = self.get_now()
|
||||
if response:
|
||||
# test to raise
|
||||
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
||||
ccinfo["response"] = response
|
||||
|
||||
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
|
||||
# editors, if the person is not yet in there
|
||||
ccinfo["editors"].append(author.id)
|
||||
|
||||
ccinfo["edited_at"] = self.get_now()
|
||||
|
||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||
|
||||
async def delete(self, ctx: commands.Context, command: str):
|
||||
@@ -129,100 +178,140 @@ class CommandObj:
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class CustomCommands:
|
||||
"""Custom commands
|
||||
|
||||
Creates commands used to display text"""
|
||||
class CustomCommands(commands.Cog):
|
||||
"""Creates commands used to display text."""
|
||||
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.key = 414589031223512
|
||||
self.config = Config.get_conf(self, self.key)
|
||||
self.config.register_guild(commands={})
|
||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||
self.cooldowns = {}
|
||||
|
||||
@commands.group(aliases=["cc"], autohelp=True)
|
||||
@commands.group(aliases=["cc"])
|
||||
@commands.guild_only()
|
||||
async def customcom(self, ctx: commands.Context):
|
||||
"""Custom commands management"""
|
||||
"""Custom commands management."""
|
||||
pass
|
||||
|
||||
@customcom.group(name="add", autohelp=True)
|
||||
@customcom.group(name="create", aliases=["add"])
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add(self, ctx: commands.Context):
|
||||
"""
|
||||
CCs can be enhanced with arguments:
|
||||
async def cc_create(self, ctx: commands.Context):
|
||||
"""Create custom commands.
|
||||
|
||||
Argument What it will be substituted with
|
||||
|
||||
{message} message
|
||||
|
||||
{author} message.author
|
||||
|
||||
{channel} message.channel
|
||||
|
||||
{guild} message.guild
|
||||
|
||||
{server} message.guild
|
||||
CCs can be enhanced with arguments, see the guide
|
||||
[here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html).
|
||||
"""
|
||||
pass
|
||||
|
||||
@cc_add.command(name="random")
|
||||
@cc_create.command(name="random")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_random(self, ctx: commands.Context, command: str):
|
||||
"""
|
||||
Create a CC where it will randomly choose a response!
|
||||
async def cc_create_random(self, ctx: commands.Context, command: str.lower):
|
||||
"""Create a CC where it will randomly choose a response!
|
||||
|
||||
Note: This is interactive
|
||||
Note: This command is interactive.
|
||||
"""
|
||||
channel = ctx.channel
|
||||
responses = []
|
||||
|
||||
responses = await self.commandobj.get_responses(ctx=ctx)
|
||||
try:
|
||||
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
await ctx.send(
|
||||
_("This command already exists. Use `{}` to edit it.").format(
|
||||
"{}customcom edit".format(ctx.prefix)
|
||||
_("This command already exists. Use `{command}` to edit it.").format(
|
||||
command="{}customcom edit".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
|
||||
# await ctx.send(str(responses))
|
||||
|
||||
@cc_add.command(name="simple")
|
||||
@cc_create.command(name="simple")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_simple(self, ctx, command: str, *, text):
|
||||
"""Adds a simple custom command
|
||||
async def cc_create_simple(self, ctx, command: str.lower, *, text: str):
|
||||
"""Add a simple custom command.
|
||||
|
||||
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:
|
||||
await ctx.send(_("That command is already a standard command."))
|
||||
await ctx.send(_("There already exists a bot command with the same name."))
|
||||
return
|
||||
try:
|
||||
await self.commandobj.create(ctx=ctx, command=command, response=text)
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
await ctx.send(
|
||||
_("This command already exists. Use `{}` to edit it.").format(
|
||||
"{}customcom edit".format(ctx.prefix)
|
||||
_("This command already exists. Use `{command}` to edit it.").format(
|
||||
command="{}customcom edit".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
except ArgParseError as e:
|
||||
await ctx.send(e.args[0])
|
||||
|
||||
@customcom.command(name="cooldown")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_cooldown(
|
||||
self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
|
||||
):
|
||||
"""Set, edit, or view the cooldown for a custom command.
|
||||
|
||||
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:
|
||||
await self.commandobj.edit(ctx=ctx, command=command, cooldowns=cooldown, ask_for=False)
|
||||
await ctx.send(_("Custom command cooldown successfully edited."))
|
||||
except NotFound:
|
||||
await ctx.send(
|
||||
_("That command doesn't exist. Use `{command}` to add it.").format(
|
||||
command="{}customcom create".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
|
||||
@customcom.command(name="delete")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_delete(self, ctx, command: str.lower):
|
||||
"""Delete a custom command
|
||||
.
|
||||
Example:
|
||||
- `[p]customcom delete yourcommand`
|
||||
"""
|
||||
try:
|
||||
await self.commandobj.delete(ctx=ctx, command=command)
|
||||
await ctx.send(_("Custom command successfully deleted."))
|
||||
except NotFound:
|
||||
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, *, text=None):
|
||||
"""Edits a custom command
|
||||
async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
|
||||
"""Edit a custom command.
|
||||
|
||||
Example:
|
||||
[p]customcom edit yourcommand Text you want
|
||||
- `[p]customcom edit yourcommand Text you want`
|
||||
"""
|
||||
guild = ctx.message.guild
|
||||
command = command.lower()
|
||||
|
||||
try:
|
||||
@@ -230,28 +319,16 @@ class CustomCommands:
|
||||
await ctx.send(_("Custom command successfully edited."))
|
||||
except NotFound:
|
||||
await ctx.send(
|
||||
_("That command doesn't exist. Use `{}` to add it.").format(
|
||||
"{}customcom add".format(ctx.prefix)
|
||||
_("That command doesn't exist. Use `{command}` to add it.").format(
|
||||
command="{}customcom create".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
|
||||
@customcom.command(name="delete")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_delete(self, ctx, command: str):
|
||||
"""Deletes a custom command
|
||||
Example:
|
||||
[p]customcom delete yourcommand"""
|
||||
guild = ctx.message.guild
|
||||
command = command.lower()
|
||||
try:
|
||||
await self.commandobj.delete(ctx=ctx, command=command)
|
||||
await ctx.send(_("Custom command successfully deleted."))
|
||||
except NotFound:
|
||||
await ctx.send(_("That command doesn't exist."))
|
||||
except ArgParseError as e:
|
||||
await ctx.send(e.args[0])
|
||||
|
||||
@customcom.command(name="list")
|
||||
async def cc_list(self, ctx):
|
||||
"""Shows custom commands list"""
|
||||
"""List all available custom commands."""
|
||||
|
||||
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||
|
||||
@@ -259,8 +336,8 @@ class CustomCommands:
|
||||
await ctx.send(
|
||||
_(
|
||||
"There are no custom commands in this server."
|
||||
" Use `{}` to start adding some."
|
||||
).format("{}customcom add".format(ctx.prefix))
|
||||
" Use `{command}` to start adding some."
|
||||
).format(command="{}customcom create".format(ctx.prefix))
|
||||
)
|
||||
return
|
||||
|
||||
@@ -286,49 +363,172 @@ class CustomCommands:
|
||||
|
||||
async def on_message(self, message):
|
||||
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
|
||||
# something similar once it's added
|
||||
|
||||
user_allowed = True
|
||||
|
||||
for prefix in prefixes:
|
||||
if message.content.startswith(prefix):
|
||||
break
|
||||
else:
|
||||
if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
|
||||
return
|
||||
|
||||
if user_allowed:
|
||||
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)
|
||||
ctx = await self.bot.get_context(message)
|
||||
|
||||
def format_cc(self, command, message) -> str:
|
||||
results = re.findall("\{([^}]+)\}", command)
|
||||
if ctx.prefix is None or ctx.valid:
|
||||
return
|
||||
|
||||
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:
|
||||
param = self.transform_parameter(result, message)
|
||||
command = command.replace("{" + result + "}", param)
|
||||
return command
|
||||
param = self.transform_parameter(result, ctx.message)
|
||||
raw_response = raw_response.replace("{" + result + "}", param)
|
||||
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 prepare_args(self, raw_response) -> Mapping[str, Parameter]:
|
||||
args = re.findall(r"\{(\d+)[^:}]*(:[^\.}]*)?[^}]*\}", raw_response)
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../customcom.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -6,29 +6,26 @@ from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
_ = Translator("DataConverter", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class DataConverter:
|
||||
"""
|
||||
Cog for importing Red v2 Data
|
||||
"""
|
||||
class DataConverter(commands.Cog):
|
||||
"""Import Red V2 data to your V3 instance."""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command(name="convertdata")
|
||||
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
|
||||
|
||||
Overwrites values which have entries in both v2 and v3,
|
||||
use with caution.
|
||||
Takes the path where the V2 install is, and overwrites
|
||||
values which have entries in both V2 and v3; use with caution.
|
||||
"""
|
||||
resolver = SpecResolver(Path(v2path.strip()))
|
||||
|
||||
@@ -47,13 +44,12 @@ class DataConverter:
|
||||
|
||||
menu_message = await ctx.send(box(menu))
|
||||
|
||||
def pred(m):
|
||||
return m.channel == ctx.channel and m.author == ctx.author
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=60)
|
||||
message = await self.bot.wait_for(
|
||||
"message", check=MessagePredicate.same_context(ctx), timeout=60
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send(_("Try this again when you are more ready"))
|
||||
return await ctx.send(_("Try this again when you are ready."))
|
||||
else:
|
||||
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
|
||||
return await ctx.tick()
|
||||
@@ -71,7 +67,7 @@ class DataConverter:
|
||||
else:
|
||||
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,11 +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
|
||||
|
||||
|
||||
def setup(bot: Red):
|
||||
bot.add_cog(Downloader(bot))
|
||||
async def setup(bot):
|
||||
cog = Downloader(bot)
|
||||
await cog.initialize()
|
||||
bot.add_cog(cog)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
__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"
|
||||
" and its community have no responsibility for any potential "
|
||||
"damage that the content of 3rd party repositories might cause."
|
||||
@@ -14,6 +18,7 @@ REPO_INSTALL_MSG = (
|
||||
"shown again until the next reboot.\n\nYou have **30** seconds"
|
||||
" to reply to this message."
|
||||
)
|
||||
_ = T_
|
||||
|
||||
|
||||
async def do_install_agreement(ctx: commands.Context):
|
||||
@@ -21,15 +26,14 @@ async def do_install_agreement(ctx: commands.Context):
|
||||
if downloader is None or downloader.already_agreed:
|
||||
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)
|
||||
await ctx.send(T_(REPO_INSTALL_MSG))
|
||||
|
||||
try:
|
||||
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
||||
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.")
|
||||
await ctx.send(_("Your response has timed out, please try again."))
|
||||
return False
|
||||
|
||||
downloader.already_agreed = True
|
||||
|
||||
@@ -3,14 +3,15 @@ from redbot.core import commands
|
||||
from .installable import Installable
|
||||
|
||||
|
||||
class InstalledCog(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
||||
class InstalledCog(Installable):
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, arg: str) -> Installable:
|
||||
downloader = ctx.bot.get_cog("Downloader")
|
||||
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)
|
||||
if cog is None:
|
||||
raise commands.BadArgument("That cog is not installed")
|
||||
raise commands.BadArgument(_("That cog is not installed"))
|
||||
|
||||
return cog
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from sys import path as syspath
|
||||
from typing import Tuple, Union
|
||||
from typing import Tuple, Union, Iterable
|
||||
|
||||
import discord
|
||||
import sys
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core import checks
|
||||
from redbot.core import checks, commands, Config
|
||||
from redbot.core.bot import Red
|
||||
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
|
||||
from redbot.core import commands
|
||||
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 redbot.core.bot import Red
|
||||
from . import errors
|
||||
from .checks import do_install_agreement
|
||||
from .converters import InstalledCog
|
||||
from .errors import CloningError, ExistingGitRepo
|
||||
from .installable import Installable
|
||||
from .log import log
|
||||
from .repo_manager import RepoManager, Repo
|
||||
@@ -26,8 +27,9 @@ _ = Translator("Downloader", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Downloader:
|
||||
class Downloader(commands.Cog):
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
|
||||
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
||||
@@ -51,6 +53,9 @@ class Downloader:
|
||||
|
||||
self._repo_manager = RepoManager()
|
||||
|
||||
async def initialize(self):
|
||||
await self._repo_manager.initialize()
|
||||
|
||||
async def cog_install_path(self):
|
||||
"""Get the current cog install path.
|
||||
|
||||
@@ -107,7 +112,7 @@ class Downloader:
|
||||
installed.remove(cog_json)
|
||||
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.
|
||||
:param cogs:
|
||||
@@ -121,7 +126,7 @@ class Downloader:
|
||||
# noinspection PyTypeChecker
|
||||
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
|
||||
were updated.
|
||||
@@ -141,7 +146,7 @@ class Downloader:
|
||||
# noinspection PyTypeChecker
|
||||
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.
|
||||
Returns a bool that indicates if all requirement installations
|
||||
@@ -188,9 +193,7 @@ class Downloader:
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def pipinstall(self, ctx, *deps: str):
|
||||
"""
|
||||
Installs a group of dependencies using pip.
|
||||
"""
|
||||
"""Install a group of dependencies using pip."""
|
||||
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||
|
||||
@@ -204,21 +207,18 @@ class Downloader:
|
||||
)
|
||||
)
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def repo(self, ctx):
|
||||
"""
|
||||
Command group for managing Downloader repos.
|
||||
"""
|
||||
"""Repo management commands."""
|
||||
pass
|
||||
|
||||
@repo.command(name="add")
|
||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||
"""
|
||||
Add a new repo to Downloader.
|
||||
"""Add a new repo.
|
||||
|
||||
Name can only contain characters A-z, numbers and underscore
|
||||
Branch will default to master if not specified
|
||||
The name can only contain characters A-z, numbers and underscores.
|
||||
The branch will be the default branch if not specified.
|
||||
"""
|
||||
agreed = await do_install_agreement(ctx)
|
||||
if not agreed:
|
||||
@@ -226,30 +226,33 @@ class Downloader:
|
||||
try:
|
||||
# noinspection PyTypeChecker
|
||||
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||
except ExistingGitRepo:
|
||||
except errors.ExistingGitRepo:
|
||||
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."))
|
||||
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:
|
||||
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:
|
||||
await ctx.send(repo.install_msg)
|
||||
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@repo.command(name="delete")
|
||||
async def _repo_del(self, ctx, repo_name: Repo):
|
||||
"""
|
||||
Removes a repo from Downloader and its' files.
|
||||
"""
|
||||
await self._repo_manager.delete_repo(repo_name.name)
|
||||
@repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
|
||||
async def _repo_del(self, ctx, repo: Repo):
|
||||
"""Remove a repo and its files."""
|
||||
await self._repo_manager.delete_repo(repo.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")
|
||||
async def _repo_list(self, ctx):
|
||||
"""
|
||||
Lists all installed repos.
|
||||
"""
|
||||
"""List all installed repos."""
|
||||
repos = self._repo_manager.get_all_repo_names()
|
||||
repos = sorted(repos, key=str.lower)
|
||||
joined = _("Installed Repos:\n\n")
|
||||
@@ -260,126 +263,156 @@ class Downloader:
|
||||
for page in pagify(joined, ["\n"], shorten_by=16):
|
||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||
|
||||
@repo.command(name="info")
|
||||
async def _repo_info(self, ctx, repo_name: Repo):
|
||||
"""
|
||||
Lists information about a single repo
|
||||
"""
|
||||
if repo_name is None:
|
||||
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
|
||||
@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 {}:\n{}").format(repo_name.name, repo_name.description or "")
|
||||
msg = _("Information on {repo.name}:\n{description}").format(
|
||||
repo=repo, description=repo.description or ""
|
||||
)
|
||||
await ctx.send(box(msg))
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def cog(self, ctx):
|
||||
"""
|
||||
Command group for managing installable Cogs.
|
||||
"""
|
||||
"""Cog installation management commands."""
|
||||
pass
|
||||
|
||||
@cog.command(name="install")
|
||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
||||
"""
|
||||
Installs a cog from the given repo.
|
||||
"""
|
||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
||||
@cog.command(name="install", usage="<repo_name> <cog_name>")
|
||||
async def _cog_install(self, ctx, repo: Repo, cog_name: str):
|
||||
"""Install a cog from the given repo."""
|
||||
cog: Installable = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||
if cog is None:
|
||||
await ctx.send(
|
||||
_("Error, there is no cog by the name of `{}` 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
|
||||
elif cog.min_python_version > sys.version_info:
|
||||
await ctx.send(
|
||||
_(
|
||||
"This cog requires at least python version {}, aborting install.".format(
|
||||
".".join([str(n) for n in cog.min_python_version])
|
||||
)
|
||||
_("This cog requires at least python version {version}, aborting install.").format(
|
||||
version=".".join([str(n) for n in cog.min_python_version])
|
||||
)
|
||||
)
|
||||
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 `{}`: `{}`").format(
|
||||
cog.name, cog.requirements
|
||||
)
|
||||
_(
|
||||
"Failed to install the required libraries for `{cog_name}`: `{libraries}`"
|
||||
).format(cog_name=cog.name, libraries=cog.requirements)
|
||||
)
|
||||
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 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:
|
||||
await ctx.send(cog.install_msg)
|
||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@cog.command(name="uninstall")
|
||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||
"""
|
||||
Allows you to uninstall cogs that were previously installed
|
||||
through Downloader.
|
||||
@cog.command(name="uninstall", usage="<cog_name>")
|
||||
async def _cog_uninstall(self, ctx, cog: InstalledCog):
|
||||
"""Uninstall a cog.
|
||||
|
||||
You may only uninstall cogs which were previously installed
|
||||
by Downloader.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
real_name = cog_name.name
|
||||
real_name = cog.name
|
||||
|
||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||
if poss_installed_path.exists():
|
||||
await self._delete_cog(poss_installed_path)
|
||||
# noinspection PyTypeChecker
|
||||
await self._remove_from_installed(cog_name)
|
||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
||||
await self._remove_from_installed(cog)
|
||||
await ctx.send(
|
||||
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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")
|
||||
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
|
||||
"""
|
||||
Updates all cogs or one of your choosing.
|
||||
"""
|
||||
"""Update all cogs, or one of your choosing."""
|
||||
installed_cogs = set(await self.installed_cogs())
|
||||
|
||||
if cog_name is None:
|
||||
updated = await self._repo_manager.update_all_repos()
|
||||
async with ctx.typing():
|
||||
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:
|
||||
try:
|
||||
updated = await self._repo_manager.update_repo(cog_name.repo_name)
|
||||
except KeyError:
|
||||
# Thrown if the repo no longer exists
|
||||
updated = {}
|
||||
pred = MessagePredicate.yes_or_no(ctx)
|
||||
event = "message"
|
||||
try:
|
||||
await ctx.bot.wait_for(event, check=pred, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await query.delete()
|
||||
return
|
||||
|
||||
updated_cogs = set(cog for repo in updated.keys() for cog in repo.available_cogs)
|
||||
installed_and_updated = updated_cogs & installed_cogs
|
||||
if pred.result is True:
|
||||
if can_react:
|
||||
with contextlib.suppress(discord.Forbidden):
|
||||
await query.clear_reactions()
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
await self._reinstall_requirements(installed_and_updated)
|
||||
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||
else:
|
||||
if can_react:
|
||||
await query.delete()
|
||||
else:
|
||||
await ctx.send(_("OK then."))
|
||||
|
||||
# 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.
|
||||
"""
|
||||
@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:
|
||||
@@ -387,10 +420,10 @@ class Downloader:
|
||||
[
|
||||
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
||||
for i in installed
|
||||
if i.repo_name == repo_name.name
|
||||
if i.repo_name == repo.name
|
||||
]
|
||||
)
|
||||
cogs = repo_name.available_cogs
|
||||
cogs = repo.available_cogs
|
||||
cogs = _("Available Cogs:\n") + "\n".join(
|
||||
[
|
||||
"+ {}: {}".format(c.name, c.short or "")
|
||||
@@ -402,20 +435,24 @@ class Downloader:
|
||||
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||
await ctx.send(box(page.lstrip(" "), 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)
|
||||
@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 `{}` in the repo `{}`").format(cog_name, repo_name.name)
|
||||
_("There is no cog `{cog_name}` in the repo `{repo.name}`").format(
|
||||
cog_name=cog_name, repo=repo
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
|
||||
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
|
||||
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))
|
||||
|
||||
@@ -469,9 +506,9 @@ class Downloader:
|
||||
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
||||
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:
|
||||
"""Determines the cog name that Downloader knows from the cog instance.
|
||||
@@ -494,9 +531,9 @@ class Downloader:
|
||||
|
||||
@commands.command()
|
||||
async def findcog(self, ctx: commands.Context, command_name: str):
|
||||
"""
|
||||
Figures out which cog a command comes from. Only works with loaded
|
||||
cogs.
|
||||
"""Find which cog a command comes from.
|
||||
|
||||
This will only work with loaded cogs.
|
||||
"""
|
||||
command = ctx.bot.all_commands.get(command_name)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ __all__ = [
|
||||
"HardResetError",
|
||||
"UpdateError",
|
||||
"GitDiffError",
|
||||
"NoRemoteURL",
|
||||
"PipError",
|
||||
]
|
||||
|
||||
@@ -96,6 +97,14 @@ class GitDiffError(GitException):
|
||||
pass
|
||||
|
||||
|
||||
class NoRemoteURL(GitException):
|
||||
"""
|
||||
Thrown when no remote URL exists for a repo.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PipError(DownloaderException):
|
||||
"""
|
||||
Thrown when pip returns a non-zero return code.
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../downloader.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -2,27 +2,33 @@ import asyncio
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import shutil
|
||||
import re
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from subprocess import run as sp_run, PIPE
|
||||
from sys import executable
|
||||
from typing import Tuple, MutableMapping, Union
|
||||
from typing import Tuple, MutableMapping, Union, Optional
|
||||
|
||||
from redbot.core import data_manager, commands
|
||||
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 .json_mixins import RepoJSONMixin
|
||||
from .log import log
|
||||
|
||||
_ = Translator("RepoManager", __file__)
|
||||
|
||||
|
||||
class Repo(RepoJSONMixin):
|
||||
GIT_CLONE = "git clone -b {branch} {url} {folder}"
|
||||
GIT_CLONE_NO_BRANCH = "git clone {url} {folder}"
|
||||
GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}"
|
||||
GIT_CLONE_NO_BRANCH = "git clone --recurse-submodules {url} {folder}"
|
||||
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||
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 {old_hash} {new_hash}"
|
||||
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"
|
||||
@@ -62,13 +68,15 @@ class Repo(RepoJSONMixin):
|
||||
async def convert(cls, ctx: commands.Context, argument: str):
|
||||
downloader_cog = ctx.bot.get_cog("Downloader")
|
||||
if downloader_cog is None:
|
||||
raise commands.CommandError("No Downloader cog found.")
|
||||
raise commands.CommandError(_("No Downloader cog found."))
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
repo_manager = downloader_cog._repo_manager
|
||||
poss_repo = repo_manager.get_repo(argument)
|
||||
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
|
||||
|
||||
def _existing_git_repo(self) -> (bool, Path):
|
||||
@@ -92,7 +100,9 @@ class Repo(RepoJSONMixin):
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitDiffError("Git diff failed for repo at path: {}".format(self.folder_path))
|
||||
raise errors.GitDiffError(
|
||||
"Git diff failed for repo at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
stdout = p.stdout.strip().decode().split("\n")
|
||||
|
||||
@@ -122,7 +132,7 @@ class Repo(RepoJSONMixin):
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException(
|
||||
raise errors.GitException(
|
||||
"An exception occurred while executing git log on"
|
||||
" this repo: {}".format(self.folder_path)
|
||||
)
|
||||
@@ -176,7 +186,7 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, path = self._existing_git_repo()
|
||||
if exists:
|
||||
raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||
raise errors.ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||
|
||||
if self.branch is not None:
|
||||
p = await self._run(
|
||||
@@ -189,8 +199,10 @@ class Repo(RepoJSONMixin):
|
||||
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise CloningError("Error when running git clone.")
|
||||
if p.returncode:
|
||||
# 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:
|
||||
self.branch = await self.current_branch()
|
||||
@@ -210,12 +222,14 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException(
|
||||
raise errors.GitException(
|
||||
"Could not determine current branch at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
@@ -240,14 +254,16 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
@@ -267,8 +283,9 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
.NoRemoteURL
|
||||
When the folder does not contain a git repo with a FETCH URL.
|
||||
|
||||
"""
|
||||
if folder is None:
|
||||
folder = self.folder_path
|
||||
@@ -276,7 +293,7 @@ class Repo(RepoJSONMixin):
|
||||
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
|
||||
|
||||
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()
|
||||
|
||||
@@ -294,14 +311,16 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise HardResetError(
|
||||
raise errors.HardResetError(
|
||||
"Some error occurred when trying to"
|
||||
" execute a hard reset on the repo at"
|
||||
" the following path: {}".format(self.folder_path)
|
||||
@@ -324,7 +343,7 @@ class Repo(RepoJSONMixin):
|
||||
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
|
||||
|
||||
if p.returncode != 0:
|
||||
raise UpdateError(
|
||||
raise errors.UpdateError(
|
||||
"Git pull returned a non zero exit code"
|
||||
" for the repo located at path: {}".format(self.folder_path)
|
||||
)
|
||||
@@ -353,7 +372,7 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
"""
|
||||
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():
|
||||
raise ValueError("That target directory is not actually a directory.")
|
||||
@@ -489,13 +508,19 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
|
||||
class RepoManager:
|
||||
def __init__(self):
|
||||
|
||||
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 = {}
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
||||
|
||||
async def initialize(self):
|
||||
await self._load_repos(set=True)
|
||||
|
||||
@property
|
||||
def repos_folder(self) -> Path:
|
||||
data_folder = data_manager.cog_data_path(self)
|
||||
@@ -507,10 +532,10 @@ class RepoManager:
|
||||
@staticmethod
|
||||
def validate_and_normalize_repo_name(name: str) -> str:
|
||||
if not name.isidentifier():
|
||||
raise InvalidRepoName("Not a valid Python variable name.")
|
||||
raise errors.InvalidRepoName("Not a valid Python variable name.")
|
||||
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.
|
||||
|
||||
Parameters
|
||||
@@ -529,10 +554,12 @@ class RepoManager:
|
||||
|
||||
"""
|
||||
if self.does_repo_exist(name):
|
||||
raise ExistingGitRepo(
|
||||
raise errors.ExistingGitRepo(
|
||||
"That repo name you provided already exists. Please choose another."
|
||||
)
|
||||
|
||||
url, branch = self._parse_url(url, branch)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
|
||||
await r.clone()
|
||||
@@ -578,13 +605,13 @@ class RepoManager:
|
||||
|
||||
Raises
|
||||
------
|
||||
MissingGitRepo
|
||||
.MissingGitRepo
|
||||
If the repo does not exist.
|
||||
|
||||
"""
|
||||
repo = self.get_repo(name)
|
||||
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)
|
||||
|
||||
@@ -623,10 +650,26 @@ class RepoManager:
|
||||
continue
|
||||
try:
|
||||
ret[folder.stem] = await Repo.from_folder(folder)
|
||||
except RuntimeError:
|
||||
# Thrown when there's no findable git remote URL
|
||||
pass
|
||||
except errors.NoRemoteURL:
|
||||
log.warning("A remote URL does not exist for repo %s", folder.stem)
|
||||
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:
|
||||
self._repos = 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,17 +3,19 @@ import logging
|
||||
import random
|
||||
from collections import defaultdict, deque
|
||||
from enum import Enum
|
||||
from typing import cast, Iterable
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank, commands
|
||||
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 redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
_ = Translator("Economy", __file__)
|
||||
T_ = Translator("Economy", __file__)
|
||||
|
||||
logger = logging.getLogger("red.economy")
|
||||
|
||||
@@ -33,6 +35,7 @@ class SMReel(Enum):
|
||||
snowflake = "\N{SNOWFLAKE}"
|
||||
|
||||
|
||||
_ = lambda s: s
|
||||
PAYOUTS = {
|
||||
(SMReel.two, SMReel.two, SMReel.six): {
|
||||
"payout": lambda x: x * 2500 + x,
|
||||
@@ -71,6 +74,7 @@ SLOT_PAYOUTS_MSG = _(
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2"
|
||||
).format(**SMReel.__dict__)
|
||||
_ = T_
|
||||
|
||||
|
||||
def guild_only_check():
|
||||
@@ -104,10 +108,8 @@ class SetParser:
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Economy:
|
||||
"""Economy
|
||||
|
||||
Get rich and have fun with imaginary currency!"""
|
||||
class Economy(commands.Cog):
|
||||
"""Get rich and have fun with imaginary currency!"""
|
||||
|
||||
default_guild_settings = {
|
||||
"PAYDAY_TIME": 300,
|
||||
@@ -127,6 +129,7 @@ class Economy:
|
||||
default_user_settings = default_member_settings
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.file_path = "data/economy/settings.json"
|
||||
self.config = Config.get_conf(self, 1256844281)
|
||||
@@ -137,14 +140,15 @@ class Economy:
|
||||
self.config.register_role(**self.default_role_settings)
|
||||
self.slot_register = defaultdict(dict)
|
||||
|
||||
@commands.group(name="bank", autohelp=True)
|
||||
@guild_only_check()
|
||||
@commands.group(name="bank")
|
||||
async def _bank(self, ctx: commands.Context):
|
||||
"""Bank operations"""
|
||||
"""Manage the bank."""
|
||||
pass
|
||||
|
||||
@_bank.command()
|
||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||
"""Shows balance of user.
|
||||
"""Show the user's account balance.
|
||||
|
||||
Defaults to yours."""
|
||||
if user is None:
|
||||
@@ -153,11 +157,15 @@ class Economy:
|
||||
bal = await bank.get_balance(user)
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
|
||||
await ctx.send(
|
||||
_("{user}'s balance is {num} {currency}").format(
|
||||
user=user.display_name, num=bal, currency=currency
|
||||
)
|
||||
)
|
||||
|
||||
@_bank.command()
|
||||
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
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
@@ -167,73 +175,83 @@ class Economy:
|
||||
return await ctx.send(str(e))
|
||||
|
||||
await ctx.send(
|
||||
_("{} transferred {} {} to {}").format(
|
||||
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")
|
||||
@check_global_setting_admin()
|
||||
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:
|
||||
bank set @Twentysix 26 - Sets balance to 26
|
||||
bank set @Twentysix +2 - Increases balance by 2
|
||||
bank set @Twentysix -6 - Decreases balance by 6"""
|
||||
- `[p]bank set @Twentysix 26` - Sets balance to 26
|
||||
- `[p]bank set @Twentysix +2` - Increases balance by 2
|
||||
- `[p]bank set @Twentysix -6` - Decreases balance by 6
|
||||
"""
|
||||
author = ctx.author
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
if creds.operation == "deposit":
|
||||
await bank.deposit_credits(to, creds.sum)
|
||||
await ctx.send(
|
||||
_("{} added {} {} to {}'s account.").format(
|
||||
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":
|
||||
await bank.withdraw_credits(to, creds.sum)
|
||||
await ctx.send(
|
||||
_("{} removed {} {} from {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
_("{author} removed {num} {currency} from {user}'s account.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=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
|
||||
_("{author} set {users}'s account balance to {num} {currency}.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=to.display_name,
|
||||
)
|
||||
)
|
||||
|
||||
@_bank.command()
|
||||
@guild_only_check()
|
||||
@check_global_setting_guildowner()
|
||||
async def reset(self, ctx, confirmation: bool = False):
|
||||
"""Deletes bank accounts"""
|
||||
"""Delete all bank accounts."""
|
||||
if confirmation is False:
|
||||
await ctx.send(
|
||||
_(
|
||||
"This will delete all bank accounts for {}.\nIf you're sure, type "
|
||||
"`{}bank reset yes`"
|
||||
"This will delete all bank accounts for {scope}.\nIf you're sure, type "
|
||||
"`{prefix}bank reset yes`"
|
||||
).format(
|
||||
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
|
||||
scope=self.bot.user.name if await bank.is_global() else _("this server"),
|
||||
prefix=ctx.prefix,
|
||||
)
|
||||
)
|
||||
else:
|
||||
await bank.wipe_bank()
|
||||
await bank.wipe_bank(guild=ctx.guild)
|
||||
await ctx.send(
|
||||
_("All bank accounts for {} have been deleted.").format(
|
||||
self.bot.user.name if await bank.is_global() else "this server"
|
||||
_("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()
|
||||
@commands.command()
|
||||
async def payday(self, ctx: commands.Context):
|
||||
"""Get some free currency"""
|
||||
"""Get some free currency."""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
|
||||
@@ -249,24 +267,25 @@ class Economy:
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
"{author.mention} Here, take some {currency}. "
|
||||
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||
"You currently have {new_balance} {currency}.\n\n"
|
||||
"You are currently #{pos} on the global leaderboard!"
|
||||
).format(
|
||||
author,
|
||||
credits_name,
|
||||
str(await self.config.PAYDAY_CREDITS()),
|
||||
str(await bank.get_balance(author)),
|
||||
pos,
|
||||
author=author,
|
||||
currency=credits_name,
|
||||
amount=await self.config.PAYDAY_CREDITS(),
|
||||
new_balance=await bank.get_balance(author),
|
||||
pos=pos,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
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:
|
||||
next_payday = await self.config.member(author).next_payday()
|
||||
@@ -284,33 +303,35 @@ class Economy:
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
"{author.mention} Here, take some {currency}. "
|
||||
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||
"You currently have {new_balance} {currency}.\n\n"
|
||||
"You are currently #{pos} on the global leaderboard!"
|
||||
).format(
|
||||
author,
|
||||
credits_name,
|
||||
credit_amount,
|
||||
str(await bank.get_balance(author)),
|
||||
pos,
|
||||
author=author,
|
||||
currency=credits_name,
|
||||
amount=credit_amount,
|
||||
new_balance=await bank.get_balance(author),
|
||||
pos=pos,
|
||||
)
|
||||
)
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
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()
|
||||
@guild_only_check()
|
||||
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"""
|
||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
||||
Defaults to top 10.
|
||||
"""
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
if top < 1:
|
||||
top = 10
|
||||
if (
|
||||
@@ -318,40 +339,40 @@ class Economy:
|
||||
): # show_global is only applicable if bank is global
|
||||
guild = None
|
||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
highscore = ""
|
||||
for pos, acc in enumerate(bank_sorted, 1):
|
||||
pos = pos
|
||||
poswidth = 2
|
||||
name = acc[1]["name"]
|
||||
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,
|
||||
header = "{pound:4}{name:36}{score:2}\n".format(
|
||||
pound="#", name=_("Name"), score=_("Score")
|
||||
)
|
||||
highscores = [
|
||||
(
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
if acc[0] != author.id
|
||||
else (
|
||||
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:
|
||||
await ctx.send(_("There are no accounts in the bank."))
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def payouts(self, ctx: commands.Context):
|
||||
"""Shows slot machine payouts"""
|
||||
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
||||
"""Show the payouts for the slot machine."""
|
||||
await ctx.author.send(SLOT_PAYOUTS_MSG())
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def slot(self, ctx: commands.Context, bid: int):
|
||||
"""Play the slot machine"""
|
||||
"""Use the slot machine."""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
channel = ctx.channel
|
||||
@@ -384,8 +405,9 @@ class Economy:
|
||||
await self.config.member(author).last_slot.set(now)
|
||||
await self.slot_machine(author, channel, bid)
|
||||
|
||||
async def slot_machine(self, author, channel, bid):
|
||||
default_reel = deque(SMReel)
|
||||
@staticmethod
|
||||
async def slot_machine(author, channel, bid):
|
||||
default_reel = deque(cast(Iterable, SMReel))
|
||||
reels = []
|
||||
for i in range(3):
|
||||
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
||||
@@ -423,60 +445,62 @@ class Economy:
|
||||
pay = payout["payout"](bid)
|
||||
now = then - bid + pay
|
||||
await bank.set_balance(author, now)
|
||||
await channel.send(
|
||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, payout["phrase"], bid, then, now
|
||||
)
|
||||
)
|
||||
phrase = T_(payout["phrase"])
|
||||
else:
|
||||
then = await bank.get_balance(author)
|
||||
await bank.withdraw_credits(author, bid)
|
||||
now = then - bid
|
||||
await channel.send(
|
||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, bid, then, now
|
||||
)
|
||||
phrase = _("Nothing!")
|
||||
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(autohelp=True)
|
||||
@commands.group()
|
||||
@guild_only_check()
|
||||
@check_global_setting_admin()
|
||||
async def economyset(self, ctx: commands.Context):
|
||||
"""Changes economy module settings"""
|
||||
"""Manage Economy settings."""
|
||||
guild = ctx.guild
|
||||
if ctx.invoked_subcommand is None:
|
||||
if await bank.is_global():
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
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()
|
||||
conf = self.config
|
||||
else:
|
||||
slot_min = await self.config.guild(guild).SLOT_MIN()
|
||||
slot_max = await self.config.guild(guild).SLOT_MAX()
|
||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
||||
payday_time = await self.config.guild(guild).PAYDAY_TIME()
|
||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||
register_amount = await bank.get_default_balance(guild)
|
||||
msg = box(
|
||||
_(
|
||||
"Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
""
|
||||
).format(
|
||||
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
|
||||
),
|
||||
_("Current Economy settings:"),
|
||||
conf = self.config.guild(ctx.guild)
|
||||
await ctx.send(
|
||||
box(
|
||||
_(
|
||||
"----Economy Settings---\n"
|
||||
"Minimum slot bid: {slot_min}\n"
|
||||
"Maximum slot bid: {slot_max}\n"
|
||||
"Slot cooldown: {slot_time}\n"
|
||||
"Payday amount: {payday_amount}\n"
|
||||
"Payday cooldown: {payday_time}\n"
|
||||
"Amount given at account registration: {register_amount}"
|
||||
).format(
|
||||
slot_min=await conf.SLOT_MIN(),
|
||||
slot_max=await conf.SLOT_MAX(),
|
||||
slot_time=await conf.SLOT_TIME(),
|
||||
payday_time=await conf.PAYDAY_TIME(),
|
||||
payday_amount=await conf.PAYDAY_CREDITS(),
|
||||
register_amount=await bank.get_default_balance(guild),
|
||||
)
|
||||
)
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@economyset.command()
|
||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||
"""Minimum slot machine bid"""
|
||||
"""Set the minimum slot machine bid."""
|
||||
if bid < 1:
|
||||
await ctx.send(_("Invalid min bid amount."))
|
||||
return
|
||||
@@ -486,14 +510,18 @@ class Economy:
|
||||
else:
|
||||
await self.config.guild(guild).SLOT_MIN.set(bid)
|
||||
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()
|
||||
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()
|
||||
if bid < 1 or bid < slot_min:
|
||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
|
||||
await ctx.send(
|
||||
_("Invalid maximum bid amount. Must be greater than the minimum amount.")
|
||||
)
|
||||
return
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
@@ -501,33 +529,37 @@ class Economy:
|
||||
await self.config.SLOT_MAX.set(bid)
|
||||
else:
|
||||
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()
|
||||
async def slottime(self, ctx: commands.Context, seconds: int):
|
||||
"""Seconds between each slots use"""
|
||||
"""Set the cooldown for the slot machine."""
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
await self.config.SLOT_TIME.set(seconds)
|
||||
else:
|
||||
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()
|
||||
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
||||
"""Seconds between each payday"""
|
||||
"""Set the cooldown for payday."""
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
await self.config.PAYDAY_TIME.set(seconds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||
await ctx.send(
|
||||
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
|
||||
_("Value modified. At least {num} seconds must pass between each payday.").format(
|
||||
num=seconds
|
||||
)
|
||||
)
|
||||
|
||||
@economyset.command()
|
||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||
"""Amount earned each payday"""
|
||||
"""Set the amount earned each payday."""
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if creds <= 0:
|
||||
@@ -537,37 +569,45 @@ class Economy:
|
||||
await self.config.PAYDAY_CREDITS.set(creds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
|
||||
await ctx.send(
|
||||
_("Every payday will now give {num} {currency}.").format(
|
||||
num=creds, currency=credits_name
|
||||
)
|
||||
)
|
||||
|
||||
@economyset.command()
|
||||
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
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
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:
|
||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(
|
||||
_("Every payday will now give {} {} to people with the role {}.").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()
|
||||
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
|
||||
if creds < 0:
|
||||
creds = 0
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
await bank.set_default_balance(creds, guild)
|
||||
await ctx.send(
|
||||
_("Registering an account will now give {} {}.").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?
|
||||
def display_time(self, seconds, granularity=2):
|
||||
@staticmethod
|
||||
def display_time(seconds, granularity=2):
|
||||
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
||||
(_("weeks"), 604800), # 60 * 60 * 24 * 7
|
||||
(_("days"), 86400), # 60 * 60 * 24
|
||||
|
||||
@@ -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,11 +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
|
||||
from typing import Union
|
||||
|
||||
from redbot.core import checks, Config, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
from redbot.core.utils.mod import is_mod_or_superior
|
||||
|
||||
_ = Translator("Filter", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Filter:
|
||||
"""Filter-related commands"""
|
||||
class Filter(commands.Cog):
|
||||
"""Filter unwanted words and phrases from text channels."""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.settings = Config.get_conf(self, 4766951341)
|
||||
default_guild_settings = {
|
||||
@@ -24,14 +25,17 @@ class Filter:
|
||||
"filter_default_name": "John Doe",
|
||||
}
|
||||
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
|
||||
default_channel_settings = {"filter": []}
|
||||
self.settings.register_guild(**default_guild_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())
|
||||
|
||||
def __unload(self):
|
||||
self.register_task.cancel()
|
||||
|
||||
async def register_filterban(self):
|
||||
@staticmethod
|
||||
async def register_filterban():
|
||||
try:
|
||||
await modlog.register_casetype(
|
||||
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
|
||||
@@ -39,119 +43,34 @@ class Filter:
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@commands.group(name="filter", autohelp=True)
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def _filter(self, ctx: commands.Context):
|
||||
"""Adds/removes words from filter
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
async def filterset(self, ctx: commands.Context):
|
||||
"""Manage filter settings."""
|
||||
pass
|
||||
|
||||
Use double quotes to add/remove sentences
|
||||
Using this command with no subcommands will send
|
||||
the list of the server's filtered words."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
server = ctx.guild
|
||||
author = ctx.author
|
||||
word_list = await self.settings.guild(server).filter()
|
||||
if word_list:
|
||||
words = ", ".join(word_list)
|
||||
words = _("Filtered in this server:") + "\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.command(name="add")
|
||||
async def filter_add(self, ctx: commands.Context, *, words: str):
|
||||
"""Adds words to the filter
|
||||
|
||||
Use double quotes to add sentences
|
||||
Examples:
|
||||
filter add word1 word2 word3
|
||||
filter add \"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
|
||||
added = await self.add_to_filter(server, word_list)
|
||||
if added:
|
||||
await ctx.send(_("Words added to filter."))
|
||||
else:
|
||||
await ctx.send(_("Words 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:
|
||||
filter remove word1 word2 word3
|
||||
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 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):
|
||||
"""Toggles whether or not to check names and nicknames against the filter
|
||||
|
||||
This is disabled by default
|
||||
"""
|
||||
guild = ctx.guild
|
||||
current_setting = await self.settings.guild(guild).filter_names()
|
||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||
if current_setting:
|
||||
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
|
||||
else:
|
||||
await ctx.send(_("Names and nicknames will now be checked against the filter."))
|
||||
|
||||
@_filter.command(name="defaultname")
|
||||
@filterset.command(name="defaultname")
|
||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||
"""Sets the default name to use if filtering names is enabled
|
||||
"""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
|
||||
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")
|
||||
@filterset.command(name="ban")
|
||||
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||
"""Autobans if the specified number of messages are filtered in the timeframe
|
||||
"""Set the filter's autoban conditions.
|
||||
|
||||
The timeframe is represented by seconds.
|
||||
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(
|
||||
@@ -170,30 +89,241 @@ class Filter:
|
||||
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:
|
||||
@commands.group(name="filter")
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def _filter(self, ctx: commands.Context):
|
||||
"""Add or remove words from server filter.
|
||||
|
||||
Use double quotes to add or remove sentences.
|
||||
|
||||
Using this command with no subcommands will send the list of
|
||||
the server's filtered words.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
server = ctx.guild
|
||||
author = ctx.author
|
||||
word_list = await self.settings.guild(server).filter()
|
||||
if word_list:
|
||||
words = ", ".join(word_list)
|
||||
words = _("Filtered in this server:") + "\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.group(name="channel")
|
||||
async def _filter_channel(self, ctx: commands.Context):
|
||||
"""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.
|
||||
|
||||
Examples:
|
||||
- `[p]filter channel add word1 word2 word3`
|
||||
- `[p]filter channel add "This is a sentence"`
|
||||
"""
|
||||
channel = ctx.channel
|
||||
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(channel, word_list)
|
||||
if added:
|
||||
await ctx.send(_("Words added to filter."))
|
||||
else:
|
||||
await ctx.send(_("Words already in the filter."))
|
||||
|
||||
@_filter_channel.command("remove")
|
||||
async def filter_channel_remove(self, ctx: commands.Context, *, words: str):
|
||||
"""Remove words from the filter.
|
||||
|
||||
Use double quotes to remove sentences.
|
||||
|
||||
Examples:
|
||||
- `[p]filter channel remove word1 word2 word3`
|
||||
- `[p]filter channel remove "This is a sentence"`
|
||||
"""
|
||||
channel = ctx.channel
|
||||
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(channel, word_list)
|
||||
if removed:
|
||||
await ctx.send(_("Words removed from filter."))
|
||||
else:
|
||||
await ctx.send(_("Those words weren't in the filter."))
|
||||
|
||||
@_filter.command(name="add")
|
||||
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"`
|
||||
"""
|
||||
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 + " "
|
||||
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
|
||||
current_setting = await self.settings.guild(guild).filter_names()
|
||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||
if current_setting:
|
||||
await ctx.send(_("Names and nicknames will no longer be filtered."))
|
||||
else:
|
||||
await ctx.send(_("Names and nicknames will now be filtered."))
|
||||
|
||||
async def add_to_filter(
|
||||
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list
|
||||
) -> bool:
|
||||
added = False
|
||||
async with self.settings.guild(server).filter() as cur_list:
|
||||
for w in words:
|
||||
if w.lower() not in cur_list and w:
|
||||
cur_list.append(w.lower())
|
||||
added = True
|
||||
if isinstance(server_or_channel, discord.Guild):
|
||||
async with self.settings.guild(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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
async with self.settings.guild(server).filter() as cur_list:
|
||||
for w in words:
|
||||
if w.lower() in cur_list:
|
||||
cur_list.remove(w.lower())
|
||||
removed = True
|
||||
if isinstance(server_or_channel, discord.Guild):
|
||||
async with self.settings.guild(server_or_channel).filter() as cur_list:
|
||||
for w in words:
|
||||
if w.lower() in cur_list:
|
||||
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
|
||||
|
||||
async def check_filter(self, message: discord.Message):
|
||||
server = message.guild
|
||||
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_time = await self.settings.guild(server).filterban_time()
|
||||
user_count = await self.settings.member(author).filter_count()
|
||||
@@ -211,7 +341,7 @@ class Filter:
|
||||
if w in message.content.lower():
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
if filter_count > 0 and filter_time > 0:
|
||||
@@ -221,10 +351,10 @@ class Filter:
|
||||
user_count >= filter_count
|
||||
and message.created_at.timestamp() < next_reset_time
|
||||
):
|
||||
reason = "Autoban (too many filtered messages.)"
|
||||
reason = _("Autoban (too many filtered messages.)")
|
||||
try:
|
||||
await server.ban(author, reason=reason)
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
await modlog.create_case(
|
||||
@@ -245,74 +375,40 @@ class Filter:
|
||||
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:
|
||||
if await self.bot.is_automod_immune(message):
|
||||
return
|
||||
|
||||
await self.check_filter(message)
|
||||
|
||||
async def on_message_edit(self, _, message):
|
||||
author = message.author
|
||||
if message.guild is None or self.bot.user == author:
|
||||
return
|
||||
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_message_edit(self, _prior, message):
|
||||
# message content has to change for non-bot's currently.
|
||||
# if this changes, we should compare before passing it.
|
||||
await self.on_message(message)
|
||||
|
||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||
if not after.guild.me.guild_permissions.manage_nicknames:
|
||||
return # No permissions to manage nicknames, so can't do anything
|
||||
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
|
||||
if before.display_name != after.display_name:
|
||||
await self.maybe_filter_name(after)
|
||||
|
||||
async def on_member_join(self, member: discord.Member):
|
||||
guild = member.guild
|
||||
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()
|
||||
await self.maybe_filter_name(member)
|
||||
|
||||
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
|
||||
|
||||
word_list = await self.settings.guild(member.guild).filter()
|
||||
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:
|
||||
await member.edit(nick=name_to_use, reason="Filtered name")
|
||||
except:
|
||||
await member.edit(nick=name_to_use, reason=reason)
|
||||
except discord.HTTPException:
|
||||
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,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../filter.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -2,16 +2,14 @@ import datetime
|
||||
import time
|
||||
from enum import Enum
|
||||
from random import randint, choice
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from redbot.core 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
|
||||
|
||||
_ = Translator("General", __file__)
|
||||
_ = T_ = Translator("General", __file__)
|
||||
|
||||
|
||||
class RPS(Enum):
|
||||
@@ -30,70 +28,78 @@ class RPSParser:
|
||||
elif argument == "scissors":
|
||||
self.choice = RPS.scissors
|
||||
else:
|
||||
raise
|
||||
raise ValueError
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class General:
|
||||
class General(commands.Cog):
|
||||
"""General commands."""
|
||||
|
||||
global _
|
||||
_ = lambda s: s
|
||||
ball = [
|
||||
_("As I see it, yes"),
|
||||
_("It is certain"),
|
||||
_("It is decidedly so"),
|
||||
_("Most likely"),
|
||||
_("Outlook good"),
|
||||
_("Signs point to yes"),
|
||||
_("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_
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.stopwatches = {}
|
||||
self.ball = [
|
||||
_("As I see it, yes"),
|
||||
_("It is certain"),
|
||||
_("It is decidedly so"),
|
||||
_("Most likely"),
|
||||
_("Outlook good"),
|
||||
_("Signs point to yes"),
|
||||
_("Without a doubt"),
|
||||
_("Yes"),
|
||||
_("Yes – definitely"),
|
||||
_("You may rely on it"),
|
||||
_("Reply hazy, try again"),
|
||||
_("Ask again later"),
|
||||
_("Better not tell you now"),
|
||||
_("Cannot predict now"),
|
||||
_("Concentrate and ask again"),
|
||||
_("Don't count on it"),
|
||||
_("My reply is no"),
|
||||
_("My sources say no"),
|
||||
_("Outlook not so good"),
|
||||
_("Very doubtful"),
|
||||
]
|
||||
|
||||
@commands.command()
|
||||
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]
|
||||
if len(choices) < 2:
|
||||
await ctx.send(_("Not enough choices to pick from."))
|
||||
await ctx.send(_("Not enough options to pick from."))
|
||||
else:
|
||||
await ctx.send(choice(choices))
|
||||
|
||||
@commands.command()
|
||||
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
|
||||
if number > 1:
|
||||
n = randint(1, number)
|
||||
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
|
||||
await ctx.send("{author.mention} :game_die: {n} :game_die:".format(author=author, n=n))
|
||||
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()
|
||||
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 = ""
|
||||
if user.id == ctx.bot.user.id:
|
||||
user = ctx.author
|
||||
@@ -112,7 +118,7 @@ class General:
|
||||
|
||||
@commands.command()
|
||||
async def rps(self, ctx, your_choice: RPSParser):
|
||||
"""Play rock paper scissors"""
|
||||
"""Play Rock Paper Scissors."""
|
||||
author = ctx.author
|
||||
player_choice = your_choice.choice
|
||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||
@@ -131,48 +137,65 @@ class General:
|
||||
outcome = cond[(player_choice, red_choice)]
|
||||
|
||||
if outcome is True:
|
||||
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
|
||||
await ctx.send(
|
||||
_("{choice} You win {author.mention}!").format(
|
||||
choice=red_choice.value, author=author
|
||||
)
|
||||
)
|
||||
elif outcome is False:
|
||||
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
|
||||
await ctx.send(
|
||||
_("{choice} You lose {author.mention}!").format(
|
||||
choice=red_choice.value, author=author
|
||||
)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
|
||||
await ctx.send(
|
||||
_("{choice} We're square {author.mention}!").format(
|
||||
choice=red_choice.value, author=author
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command(name="8", aliases=["8ball"])
|
||||
async def _8ball(self, ctx, *, question: str):
|
||||
"""Ask 8 ball a question
|
||||
"""Ask 8 ball a question.
|
||||
|
||||
Question must end with a question mark.
|
||||
"""
|
||||
if question.endswith("?") and question != "?":
|
||||
await ctx.send("`" + choice(self.ball) + "`")
|
||||
await ctx.send("`" + T_(choice(self.ball)) + "`")
|
||||
else:
|
||||
await ctx.send(_("That doesn't look like a question."))
|
||||
|
||||
@commands.command(aliases=["sw"])
|
||||
async def stopwatch(self, ctx):
|
||||
"""Starts/stops stopwatch"""
|
||||
"""Start or stop the stopwatch."""
|
||||
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())
|
||||
await ctx.send(author.mention + _(" Stopwatch started!"))
|
||||
else:
|
||||
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
||||
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)
|
||||
|
||||
@commands.command()
|
||||
async def lmgtfy(self, ctx, *, search_terms: str):
|
||||
"""Creates a lmgtfy link"""
|
||||
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
||||
"""Create a lmgtfy link."""
|
||||
search_terms = escape(
|
||||
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
|
||||
)
|
||||
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
|
||||
|
||||
@commands.command(hidden=True)
|
||||
@commands.guild_only()
|
||||
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)
|
||||
if intensity <= 0:
|
||||
msg = "(っ˘̩╭╮˘̩)っ" + name
|
||||
@@ -184,31 +207,30 @@ class General:
|
||||
msg = "(つ≧▽≦)つ" + name
|
||||
elif intensity >= 10:
|
||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||
else:
|
||||
# For the purposes of "msg might not be defined" linter errors
|
||||
raise RuntimeError
|
||||
await ctx.send(msg)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def serverinfo(self, ctx):
|
||||
"""Shows server's informations"""
|
||||
"""Show server information."""
|
||||
guild = ctx.guild
|
||||
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||
total_users = len(guild.members)
|
||||
text_channels = len(guild.text_channels)
|
||||
voice_channels = len(guild.voice_channels)
|
||||
passed = (ctx.message.created_at - guild.created_at).days
|
||||
created_at = _("Since {}. That's over {} days ago!").format(
|
||||
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
||||
created_at = _("Since {date}. That's over {num} days ago!").format(
|
||||
date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed
|
||||
)
|
||||
|
||||
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 = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
|
||||
data.add_field(name=_("Region"), value=str(guild.region))
|
||||
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
||||
data.add_field(name=_("Voice Channels"), value=voice_channels)
|
||||
data.add_field(name=_("Roles"), value=len(guild.roles))
|
||||
data.add_field(name=_("Users"), value=f"{online}/{total_users}")
|
||||
data.add_field(name=_("Text Channels"), value=str(text_channels))
|
||||
data.add_field(name=_("Voice Channels"), value=str(voice_channels))
|
||||
data.add_field(name=_("Roles"), value=str(len(guild.roles)))
|
||||
data.add_field(name=_("Owner"), value=str(guild.owner))
|
||||
data.set_footer(text=_("Server ID: ") + str(guild.id))
|
||||
|
||||
@@ -220,53 +242,89 @@ class General:
|
||||
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
except discord.HTTPException:
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("I need the `Embed links` permission to send this."))
|
||||
|
||||
@commands.command()
|
||||
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
||||
"""Urban Dictionary search
|
||||
async def urban(self, ctx, *, word):
|
||||
"""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:
|
||||
if len(search_terms) > 1:
|
||||
pos = int(search_terms[-1]) - 1
|
||||
search_terms = search_terms[:-1]
|
||||
else:
|
||||
pos = 0
|
||||
if pos not in range(0, 11): # API only provides the
|
||||
pos = 0 # top 10 definitions
|
||||
except ValueError:
|
||||
pos = 0
|
||||
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
|
||||
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
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 session.get(url, params=search_terms) as r:
|
||||
result = await r.json()
|
||||
item_list = result["list"]
|
||||
if item_list:
|
||||
definition = item_list[pos]["definition"]
|
||||
example = item_list[pos]["example"]
|
||||
defs = len(item_list)
|
||||
msg = "**Definition #{} out of {}:\n**{}\n\n**Example:\n**{}".format(
|
||||
pos + 1, defs, definition, example
|
||||
)
|
||||
msg = pagify(msg, ["\n"])
|
||||
for page in msg:
|
||||
await ctx.send(page)
|
||||
async with session.get(url, headers=headers) as response:
|
||||
data = await response.json()
|
||||
|
||||
except aiohttp.ClientError:
|
||||
await ctx.send(
|
||||
_("No Urban dictionary entries were found, or there was an error in the process")
|
||||
)
|
||||
return
|
||||
|
||||
if data.get("error") != 404:
|
||||
|
||||
if await ctx.embed_requested():
|
||||
# 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:
|
||||
await ctx.send(_("Your search terms gave no results."))
|
||||
except IndexError:
|
||||
await ctx.send(_("There is no definition #{}").format(pos + 1))
|
||||
except:
|
||||
await ctx.send(_("Error."))
|
||||
messages = []
|
||||
for ud in data["list"]:
|
||||
ud.set_default("example", "N/A")
|
||||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||
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
|
||||
|
||||
@@ -1,241 +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"
|
||||
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "As I see it, yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "It is certain"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "It is decidedly so"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Most likely"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Outlook good"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Signs point to yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Without a doubt"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Yes – definitely"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "You may rely on it"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Ask again later"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Reply hazy, try again"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Better not tell you now"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Cannot predict now"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:47
|
||||
msgid "Concentrate and ask again"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:47
|
||||
msgid "Don't count on it"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:47
|
||||
msgid "My reply is no"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:48
|
||||
msgid "My sources say no"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:48
|
||||
msgid "Outlook not so good"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:48
|
||||
msgid "Very doubtful"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:64
|
||||
msgid "Not enough choices to pick from."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:78
|
||||
msgid "{} :game_die: {} :game_die:"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:81
|
||||
msgid "{} Maybe higher than 1? ;P"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:93
|
||||
msgid ""
|
||||
"Nice try. You think this is funny?How about *this* instead:\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:106
|
||||
msgid "*flips a coin and... "
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:106
|
||||
msgid "HEADS!*"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:106
|
||||
msgid "TAILS!*"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:130
|
||||
msgid "{} You win {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:134
|
||||
msgid "{} You lose {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:138
|
||||
msgid "{} We're square {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:151
|
||||
msgid "That doesn't look like a question."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:159
|
||||
msgid " Stopwatch started!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:163
|
||||
msgid " Stopwatch stopped! Time: **"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:216 ../general.py:217
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"({} days ago)"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:219
|
||||
msgid "Chilling in {} status"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:223
|
||||
msgid "Playing {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:225
|
||||
msgid "Streaming [{}]({})"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:227
|
||||
msgid "Listening to {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:229
|
||||
msgid "Watching {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:234
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:237
|
||||
msgid "Joined Discord on"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:238
|
||||
msgid "Joined this guild on"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:239 ../general.py:286
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:240
|
||||
msgid "Member #{} | User ID: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:257 ../general.py:299
|
||||
msgid "I need the `Embed links` permission to send this."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:272
|
||||
msgid "Since {}. That's over {} days ago!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:282
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:283
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:284
|
||||
msgid "Text Channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:285
|
||||
msgid "Voice Channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:287
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:288
|
||||
msgid "Guild ID: "
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:343
|
||||
msgid "Your search terms gave no results."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:345
|
||||
msgid "There is no definition #{}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:347
|
||||
msgid "Error."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../general.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -11,12 +11,13 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Image:
|
||||
class Image(commands.Cog):
|
||||
"""Image related commands."""
|
||||
|
||||
default_global = {"imgur_client_id": None}
|
||||
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
||||
self.settings.register_global(**self.default_global)
|
||||
@@ -24,27 +25,30 @@ class Image:
|
||||
self.imgur_base_url = "https://api.imgur.com/3/"
|
||||
|
||||
def __unload(self):
|
||||
self.session.close()
|
||||
self.session.detach()
|
||||
|
||||
@commands.group(name="imgur", autohelp=True)
|
||||
@commands.group(name="imgur")
|
||||
async def _imgur(self, ctx):
|
||||
"""Retrieves pictures from imgur
|
||||
"""Retrieve pictures from Imgur.
|
||||
|
||||
Make sure to set the client ID using
|
||||
[p]imgurcreds"""
|
||||
Make sure to set the Client ID using `[p]imgurcreds`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@_imgur.command(name="search")
|
||||
async def imgur_search(self, ctx, *, term: str):
|
||||
"""Searches Imgur for the specified term and returns up to 3 results"""
|
||||
"""Search Imgur for the specified term.
|
||||
|
||||
Returns up to 3 results.
|
||||
"""
|
||||
url = self.imgur_base_url + "gallery/search/time/all/0"
|
||||
params = {"q": term}
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
_(
|
||||
"A Client ID has not been set! Please set one with `{prefix}imgurcreds`."
|
||||
).format(prefix=ctx.prefix)
|
||||
)
|
||||
return
|
||||
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
||||
@@ -63,37 +67,41 @@ class Image:
|
||||
msg += "\n"
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
await ctx.send(
|
||||
_("Something went wrong. Error code is {code}.").format(code=data["status"])
|
||||
)
|
||||
|
||||
@_imgur.command(name="subreddit")
|
||||
async def imgur_subreddit(
|
||||
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
|
||||
):
|
||||
"""Gets images from the specified subreddit section
|
||||
"""Get images from a subreddit.
|
||||
|
||||
Sort types: new, top
|
||||
Time windows: day, week, month, year, all"""
|
||||
You can customize the search with the following options:
|
||||
- `<sort_type>`: new, top
|
||||
- `<window>`: day, week, month, year, all
|
||||
"""
|
||||
sort_type = sort_type.lower()
|
||||
window = window.lower()
|
||||
|
||||
if sort_type not in ("new", "top"):
|
||||
await ctx.send(_("Only 'new' and 'top' are a valid sort type."))
|
||||
return
|
||||
elif window not in ("day", "week", "month", "year", "all"):
|
||||
await ctx.send_help()
|
||||
return
|
||||
|
||||
if sort_type == "new":
|
||||
sort = "time"
|
||||
elif sort_type == "top":
|
||||
sort = "top"
|
||||
else:
|
||||
await ctx.send(_("Only 'new' and 'top' are a valid sort type."))
|
||||
return
|
||||
|
||||
if window not in ("day", "week", "month", "year", "all"):
|
||||
await ctx.send_help()
|
||||
return
|
||||
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
_(
|
||||
"A Client ID has not been set! Please set one with `{prefix}imgurcreds`."
|
||||
).format(prefix=ctx.prefix)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -116,28 +124,33 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
await ctx.send(
|
||||
_("Something went wrong. Error code is {code}.").format(code=data["status"])
|
||||
)
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command()
|
||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||
"""Sets the imgur client id
|
||||
"""Set the Imgur Client ID.
|
||||
|
||||
You will need an account on Imgur to get this
|
||||
|
||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
||||
and filling out the form. Enter a name for the application, select
|
||||
'Anonymous usage without user authorization' for the auth type,
|
||||
set the authorization callback url to 'https://localhost'
|
||||
leave the app website blank, enter a valid email address, and
|
||||
enter a description. Check the box for the captcha, then click Next.
|
||||
Your client ID will be on the page that loads."""
|
||||
To get an Imgur Client ID:
|
||||
1. Login to an Imgur account.
|
||||
2. Visit [this](https://api.imgur.com/oauth2/addclient) page
|
||||
3. Enter a name for your application
|
||||
4. Select *Anonymous usage without user authorization* for the auth type
|
||||
5. Set the authorization callback URL to `https://localhost`
|
||||
6. Leave the app website blank
|
||||
7. Enter a valid email address and a description
|
||||
8. Check the captcha box and click next
|
||||
9. Your Client ID will be on the next page.
|
||||
"""
|
||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||
await ctx.send(_("Set the imgur client id!"))
|
||||
await ctx.send(_("The Imgur Client ID has been set!"))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@commands.guild_only()
|
||||
@commands.command()
|
||||
async def gif(self, ctx, *keywords):
|
||||
"""Retrieves first search result from giphy"""
|
||||
"""Retrieve the first search result from Giphy."""
|
||||
if keywords:
|
||||
keywords = "+".join(keywords)
|
||||
else:
|
||||
@@ -156,11 +169,12 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API."))
|
||||
await ctx.send(_("Error contacting the Giphy API."))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@commands.guild_only()
|
||||
@commands.command()
|
||||
async def gifr(self, ctx, *keywords):
|
||||
"""Retrieves a random gif from a giphy search"""
|
||||
"""Retrieve a random GIF from a Giphy search."""
|
||||
if keywords:
|
||||
keywords = "+".join(keywords)
|
||||
else:
|
||||
|
||||
@@ -1,46 +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"
|
||||
|
||||
|
||||
#: ../image.py:49
|
||||
msgid "Your search returned no results"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:52
|
||||
msgid ""
|
||||
"Search results...\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:58 ../image.py:100
|
||||
msgid "Something went wrong. Error code is {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:70
|
||||
msgid "Only 'new' and 'top' are a valid sort type."
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:98 ../image.py:135 ../image.py:157
|
||||
msgid "No results found."
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:115
|
||||
msgid "Set the imgur client id!"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:137 ../image.py:159
|
||||
msgid "Error contacting the API"
|
||||
msgstr ""
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../image.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -1,5 +1,4 @@
|
||||
from redbot.core import commands
|
||||
import discord
|
||||
|
||||
|
||||
def mod_or_voice_permissions(**perms):
|
||||
@@ -10,8 +9,8 @@ def mod_or_voice_permissions(**perms):
|
||||
# Author is bot owner or guild owner
|
||||
return True
|
||||
|
||||
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
||||
mod_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).mod_role())
|
||||
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||
mod_role = guild.get_role(await ctx.bot.db.guild(guild).mod_role())
|
||||
|
||||
if admin_role in author.roles or mod_role in author.roles:
|
||||
return True
|
||||
@@ -26,7 +25,7 @@ def mod_or_voice_permissions(**perms):
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
return commands.permissions_check(pred)
|
||||
|
||||
|
||||
def admin_or_voice_permissions(**perms):
|
||||
@@ -35,7 +34,7 @@ def admin_or_voice_permissions(**perms):
|
||||
guild = ctx.guild
|
||||
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||
return True
|
||||
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
||||
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||
if admin_role in author.roles:
|
||||
return True
|
||||
for vc in guild.voice_channels:
|
||||
@@ -48,7 +47,7 @@ def admin_or_voice_permissions(**perms):
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
return commands.permissions_check(pred)
|
||||
|
||||
|
||||
def bot_has_voice_permissions(**perms):
|
||||
|
||||
@@ -1,286 +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"
|
||||
|
||||
|
||||
#: ../mod.py:209
|
||||
msgid "Role hierarchy will be checked when moderation commands are issued."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:213
|
||||
msgid "Role hierarchy will be ignored when moderation commands are issued."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:228
|
||||
msgid "Autoban for mention spam enabled. Anyone mentioning {} or more different people in a single message will be autobanned."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:239
|
||||
msgid "Autoban for mention spam disabled."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:249
|
||||
msgid "Messages repeated up to 3 times will be deleted."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:253
|
||||
msgid "Repeated messages will be ignored."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:267
|
||||
msgid "Command deleting disabled."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:270
|
||||
msgid "Delete delay set to {} seconds."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:275
|
||||
msgid "Bot will delete command messages after {} seconds. Set this value to -1 to stop deleting messages"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:279
|
||||
msgid "I will not delete command messages."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:292
|
||||
msgid "Users unbanned with {} will be reinvited."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:295
|
||||
msgid "Users unbanned with {} will not be reinvited."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:309 ../mod.py:349 ../mod.py:506
|
||||
msgid "I cannot let you do that. Self-harm is bad {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:313 ../mod.py:353 ../mod.py:510
|
||||
msgid "I cannot let you do that. You are not higher than the user in the role hierarchy."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:323 ../mod.py:379 ../mod.py:570
|
||||
msgid "I'm not allowed to do that."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:327
|
||||
msgid "Done. That felt good."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:369
|
||||
msgid "Invalid days. Must be between 0 and 7."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:384
|
||||
msgid "Done. It was about time."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:413
|
||||
msgid "User is already banned."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:429
|
||||
msgid "User not found. Have you provided the correct user ID?"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:433
|
||||
msgid "I lack the permissions to do this."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:435
|
||||
msgid "Done. The user will not be able to join this guild."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:472
|
||||
msgid "You have been temporarily banned from {} until {}. Here is an invite for when your ban expires: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:481
|
||||
msgid "I can't do that for some reason."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:483
|
||||
msgid "Something went wrong while banning"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:485
|
||||
msgid "Done. Enough chaos for now"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:525
|
||||
msgid ""
|
||||
"You have been banned and then unbanned as a quick way to delete your messages.\n"
|
||||
"You can now join the guild again. {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:537
|
||||
msgid "My role is not high enough to softban that user."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:553
|
||||
msgid "Done. Enough chaos."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:587
|
||||
msgid "Couldn't find a user with that ID!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:593
|
||||
msgid "It seems that user isn't banned!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:601
|
||||
msgid "Something went wrong while attempting to unban that user"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:604
|
||||
msgid "Unbanned that user from this guild"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:619
|
||||
msgid ""
|
||||
"You've been unbanned from {}.\n"
|
||||
"Here is an invite for that guild: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:623
|
||||
msgid ""
|
||||
"I failed to send an invite to that user. Perhaps you may be able to send it for me?\n"
|
||||
"Here's the invite link: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:629
|
||||
msgid "Something went wrong when attempting to send that useran invite. Here's the link so you can try: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:673 ../mod.py:710
|
||||
msgid "No voice state for that user!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:687
|
||||
msgid "That user is already muted and deafened guild-wide!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:690
|
||||
msgid "User has been banned from speaking or listening in voice channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:722
|
||||
msgid "That user isn't muted or deafened by the guild!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:725
|
||||
msgid "User is now allowed to speak and listen in voice channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:754
|
||||
msgid "I cannot do that, I lack the '{}' permission."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:783
|
||||
msgid "Muted {}#{} in channel {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:797
|
||||
msgid "That user is already muted in {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:800 ../mod.py:932
|
||||
msgid "That user is not in a voice channel right now!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:802 ../mod.py:934
|
||||
msgid "No voice state for the target!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:822
|
||||
msgid "User has been muted in this channel."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:858
|
||||
msgid "User has been muted in this guild."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:919
|
||||
msgid "Unmuted {}#{} in channel {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:929
|
||||
msgid "That user is already unmuted in {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:949
|
||||
msgid "User unmuted in this channel."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:958
|
||||
msgid "Unmute failed. Reason: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:981
|
||||
msgid "User has been unmuted in this guild."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1045
|
||||
msgid "Channel added to ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1047
|
||||
msgid "Channel already in ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1055
|
||||
msgid "This guild has been added to the ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1057
|
||||
msgid "This guild is already being ignored."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1078
|
||||
msgid "Channel removed from ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1080
|
||||
msgid "That channel is not in the ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1088
|
||||
msgid "This guild has been removed from the ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1090
|
||||
msgid "This guild is not in the ignore list."
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1102
|
||||
msgid ""
|
||||
"Currently ignoring:\n"
|
||||
"{} channels\n"
|
||||
"{} guilds\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1133
|
||||
msgid "**Past 20 names**:"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1140
|
||||
msgid "**Past 20 nicknames**:"
|
||||
msgstr ""
|
||||
|
||||
#: ../mod.py:1146
|
||||
msgid "That user doesn't have any recorded name or nickname change."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../mod.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +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"
|
||||
|
||||
|
||||
#: ../modlog.py:36
|
||||
msgid "Mod events will be sent to {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:42
|
||||
msgid "I do not have permissions to send messages in {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:52
|
||||
msgid "Mod log deactivated."
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:63
|
||||
msgid "Current settings:"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:75
|
||||
msgid "That action is not registered"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:82
|
||||
msgid "Case creation for {} actions is now {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:94
|
||||
msgid "Cases have been reset."
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:103
|
||||
msgid "That case does not exist for that guild"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:122
|
||||
msgid "That case does not exist!"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:146
|
||||
msgid "You are not authorized to modify that case!"
|
||||
msgstr ""
|
||||
|
||||
#: ../modlog.py:155
|
||||
msgid "Reason has been updated."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = ["../modlog.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import checks, modlog, commands
|
||||
@@ -9,32 +11,38 @@ _ = Translator("ModLog", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class ModLog:
|
||||
"""Log for mod actions"""
|
||||
class ModLog(commands.Cog):
|
||||
"""Manage log channels for moderation actions."""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
|
||||
@commands.group(autohelp=True)
|
||||
@commands.group()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modlogset(self, ctx: commands.Context):
|
||||
"""Settings for the mod log"""
|
||||
"""Manage modlog settings."""
|
||||
pass
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||
"""Sets a channel as mod log
|
||||
"""Set a channel as the modlog.
|
||||
|
||||
Leaving the channel parameter empty will deactivate it"""
|
||||
Omit `<channel>` to disable the modlog.
|
||||
"""
|
||||
guild = ctx.guild
|
||||
if channel:
|
||||
if channel.permissions_for(guild.me).send_messages:
|
||||
await modlog.set_modlog_channel(guild, channel)
|
||||
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
||||
await ctx.send(
|
||||
_("Mod events will be sent to {channel}").format(channel=channel.mention)
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_("I do not have permissions to send messages in {}!").format(channel.mention)
|
||||
_("I do not have permissions to send messages in {channel}!").format(
|
||||
channel=channel.mention
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
@@ -48,39 +56,36 @@ class ModLog:
|
||||
@modlogset.command(name="cases")
|
||||
@commands.guild_only()
|
||||
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||
"""Enables or disables case creation for each type of mod action"""
|
||||
"""Enable or disable case creation for a mod action."""
|
||||
guild = ctx.guild
|
||||
|
||||
if action is None: # No args given
|
||||
casetypes = await modlog.get_all_casetypes(guild)
|
||||
await ctx.send_help()
|
||||
title = _("Current settings:")
|
||||
msg = ""
|
||||
lines = []
|
||||
for ct in casetypes:
|
||||
enabled = await ct.is_enabled()
|
||||
value = "enabled" if enabled else "disabled"
|
||||
msg += "%s : %s\n" % (ct.name, value)
|
||||
enabled = "enabled" if await ct.is_enabled() else "disabled"
|
||||
lines.append(f"{ct.name} : {enabled}")
|
||||
|
||||
msg = title + "\n" + box(msg)
|
||||
await ctx.send(msg)
|
||||
await ctx.send(_("Current settings:\n") + box("\n".join(lines)))
|
||||
return
|
||||
|
||||
casetype = await modlog.get_casetype(action, guild)
|
||||
if not casetype:
|
||||
await ctx.send(_("That action is not registered"))
|
||||
else:
|
||||
|
||||
enabled = await casetype.is_enabled()
|
||||
await casetype.set_enabled(True if not enabled else False)
|
||||
|
||||
msg = _("Case creation for {} actions is now {}.").format(
|
||||
action, "enabled" if not enabled else "disabled"
|
||||
await casetype.set_enabled(not enabled)
|
||||
await ctx.send(
|
||||
_("Case creation for {action_name} actions is now {enabled}.").format(
|
||||
action_name=action, enabled="enabled" if not enabled else "disabled"
|
||||
)
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def resetcases(self, ctx: commands.Context):
|
||||
"""Resets modlog's cases"""
|
||||
"""Reset all modlog cases in this server."""
|
||||
guild = ctx.guild
|
||||
await modlog.reset_cases(guild)
|
||||
await ctx.send(_("Cases have been reset."))
|
||||
@@ -88,26 +93,33 @@ class ModLog:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def case(self, ctx: commands.Context, number: int):
|
||||
"""Shows the specified case"""
|
||||
"""Show the specified case."""
|
||||
try:
|
||||
case = await modlog.get_case(number, ctx.guild, self.bot)
|
||||
except RuntimeError:
|
||||
await ctx.send(_("That case does not exist for that server"))
|
||||
return
|
||||
else:
|
||||
await ctx.send(embed=await case.get_case_msg_content())
|
||||
if await ctx.embed_requested():
|
||||
await ctx.send(embed=await case.message_content(embed=True))
|
||||
else:
|
||||
await ctx.send(await case.message_content(embed=False))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""):
|
||||
"""Lets you specify a reason for mod-log's cases
|
||||
async def reason(self, ctx: commands.Context, case: Optional[int], *, reason: str):
|
||||
"""Specify a reason for a modlog case.
|
||||
|
||||
Please note that you can only edit cases you are
|
||||
the owner of unless you are a mod/admin or the server owner"""
|
||||
the owner of unless you are a mod, admin or server owner.
|
||||
|
||||
If no case number is specified, the latest case will be used.
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if not reason:
|
||||
await ctx.send_help()
|
||||
return
|
||||
if case is None:
|
||||
# get the latest case
|
||||
case = int(await modlog.get_next_case_number(guild)) - 1
|
||||
try:
|
||||
case_before = await modlog.get_case(case, guild, self.bot)
|
||||
except RuntimeError:
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
from .permissions import Permissions
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Permissions(bot))
|
||||
async def setup(bot):
|
||||
cog = Permissions(bot)
|
||||
await cog.initialize()
|
||||
# It's important that these listeners are added prior to load, so
|
||||
# the permissions commands themselves have rules added.
|
||||
# Automatic listeners being added in add_cog happen in arbitrary
|
||||
# order, so we want to circumvent that.
|
||||
bot.add_listener(cog.cog_added, "on_cog_add")
|
||||
bot.add_listener(cog.command_added, "on_command_add")
|
||||
bot.add_cog(cog)
|
||||
|
||||
@@ -1,29 +1,55 @@
|
||||
from typing import NamedTuple, Union, Optional, cast, Type
|
||||
|
||||
from redbot.core import commands
|
||||
from typing import Tuple
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("PermissionsConverters", __file__)
|
||||
|
||||
|
||||
class CogOrCommand(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||
ret = ctx.bot.get_cog(arg)
|
||||
if ret:
|
||||
return "cogs", ret.__class__.__name__
|
||||
ret = ctx.bot.get_command(arg)
|
||||
if ret:
|
||||
return "commands", ret.qualified_name
|
||||
class CogOrCommand(NamedTuple):
|
||||
type: str
|
||||
name: str
|
||||
obj: Union[commands.Command, commands.Cog]
|
||||
|
||||
# noinspection PyArgumentList
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, arg: str) -> "CogOrCommand":
|
||||
cog = ctx.bot.get_cog(arg)
|
||||
if cog:
|
||||
return cls(type="COG", name=cog.__class__.__name__, obj=cog)
|
||||
cmd = ctx.bot.get_command(arg)
|
||||
if cmd:
|
||||
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
|
||||
|
||||
raise commands.BadArgument(
|
||||
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
||||
"".format(arg=arg)
|
||||
_(
|
||||
'Cog or command "{name}" not found. Please note that this is case sensitive.'
|
||||
).format(name=arg)
|
||||
)
|
||||
|
||||
|
||||
class RuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
def RuleType(arg: str) -> bool:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return True
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return False
|
||||
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||
)
|
||||
raise commands.BadArgument(
|
||||
_('"{arg}" is not a valid rule. Valid rules are "allow" or "deny"').format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
def ClearableRuleType(arg: str) -> Optional[bool]:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return True
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return False
|
||||
if arg.lower() in ("clear", "reset"):
|
||||
return None
|
||||
|
||||
raise commands.BadArgument(
|
||||
_(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
|
||||
"remove the rule"
|
||||
).format(arg=arg)
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,80 +0,0 @@
|
||||
import types
|
||||
import contextlib
|
||||
import asyncio
|
||||
import logging
|
||||
from redbot.core import commands
|
||||
|
||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
||||
|
||||
|
||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
||||
"""
|
||||
Returns the value from a check if it is valid
|
||||
"""
|
||||
|
||||
val = None
|
||||
# let's not spam the console with improperly made 3rd party checks
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(check):
|
||||
val = await check(ctx, level=level)
|
||||
else:
|
||||
val = check(ctx, level=level)
|
||||
except Exception as e:
|
||||
# but still provide a way to view it (run with debug flag)
|
||||
log.debug(str(e))
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
|
||||
"""
|
||||
Resolves models in order.
|
||||
"""
|
||||
|
||||
cmd_name = ctx.command.qualified_name
|
||||
cog_name = ctx.cog.__class__.__name__
|
||||
|
||||
resolved = None
|
||||
|
||||
to_iter = (("commands", cmd_name), ("cogs", cog_name))
|
||||
|
||||
for model_name, ctx_attr in to_iter:
|
||||
if ctx_attr in models.get(model_name, {}):
|
||||
blacklist = models[model_name][ctx_attr].get("deny", [])
|
||||
whitelist = models[model_name][ctx_attr].get("allow", [])
|
||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
resolved = models[model_name][ctx_attr].get("default", None)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
|
||||
"""
|
||||
resolves specific lists
|
||||
"""
|
||||
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
|
||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
# entries now contains the following (in order) (if applicable)
|
||||
# author.id
|
||||
# author.voice.voice_channel.id
|
||||
# channel.id
|
||||
# role.id for each role (highest to lowest)
|
||||
# (implicitly) guild.id because
|
||||
# the @everyone role shares an id with the guild
|
||||
|
||||
for entry in entries:
|
||||
if entry in whitelist:
|
||||
return True
|
||||
if entry in blacklist:
|
||||
return False
|
||||
return None
|
||||
@@ -1,19 +0,0 @@
|
||||
cogs:
|
||||
Admin:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
deny:
|
||||
- 96733288462286848
|
||||
Audio:
|
||||
allow:
|
||||
- 133049272517001216
|
||||
default: deny
|
||||
commands:
|
||||
cleanup bot:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
default: deny
|
||||
ping:
|
||||
deny:
|
||||
- 96733288462286848
|
||||
default: allow
|
||||
@@ -1,67 +0,0 @@
|
||||
import io
|
||||
import yaml
|
||||
import pathlib
|
||||
import discord
|
||||
|
||||
|
||||
def yaml_template() -> dict:
|
||||
template_fp = pathlib.Path(__file__).parent / "template.yaml"
|
||||
|
||||
with template_fp.open() as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
async def yamlset_acl(ctx, *, config, update):
|
||||
_fp = io.BytesIO()
|
||||
await ctx.message.attachments[0].save(_fp)
|
||||
|
||||
try:
|
||||
data = yaml.safe_load(_fp)
|
||||
except yaml.YAMLError:
|
||||
_fp.close()
|
||||
del _fp
|
||||
raise
|
||||
|
||||
old_data = await config()
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k == "default":
|
||||
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
|
||||
|
||||
if not update:
|
||||
continue
|
||||
try:
|
||||
if isinstance(old_data[outer][ok][k], list):
|
||||
data[outer][ok][k].extend(old_data[outer][ok][k])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
await config.set(data)
|
||||
|
||||
|
||||
async def yamlget_acl(ctx, *, config):
|
||||
data = await config()
|
||||
removals = []
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k != "default":
|
||||
continue
|
||||
if v is True:
|
||||
data[outer][ok][k] = "allow"
|
||||
elif v is False:
|
||||
data[outer][ok][k] = "deny"
|
||||
else:
|
||||
removals.append((outer, ok, k))
|
||||
|
||||
for tup in removals:
|
||||
o, i, k = tup
|
||||
data[o][i].pop(k, None)
|
||||
|
||||
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
|
||||
_fp.seek(0)
|
||||
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
|
||||
_fp.close()
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
from datetime import timedelta
|
||||
from copy import copy
|
||||
import contextlib
|
||||
@@ -11,6 +11,7 @@ from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils.antispam import AntiSpam
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
from redbot.core.utils.tunnel import Tunnel
|
||||
|
||||
|
||||
@@ -20,7 +21,7 @@ log = logging.getLogger("red.reports")
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Reports:
|
||||
class Reports(commands.Cog):
|
||||
|
||||
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
||||
|
||||
@@ -40,6 +41,7 @@ class Reports:
|
||||
]
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
||||
self.config.register_guild(**self.default_guild_settings)
|
||||
@@ -56,41 +58,36 @@ class Reports:
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@commands.guild_only()
|
||||
@commands.group(name="reportset", autohelp=True)
|
||||
@commands.group(name="reportset")
|
||||
async def reportset(self, ctx: commands.Context):
|
||||
"""
|
||||
settings for reports
|
||||
"""
|
||||
"""Manage Reports."""
|
||||
pass
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="output")
|
||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""sets the output channel"""
|
||||
async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""Set the channel where reports will be sent."""
|
||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||
await ctx.send(_("Report Channel Set."))
|
||||
await ctx.send(_("The report channel has been set."))
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="toggleactive")
|
||||
async def report_toggle(self, ctx: commands.Context):
|
||||
"""Toggles whether the Reporting tool is enabled or not"""
|
||||
|
||||
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||
async def reportset_toggle(self, ctx: commands.Context):
|
||||
"""Enable or Disable reporting for this server."""
|
||||
active = await self.config.guild(ctx.guild).active()
|
||||
active = not active
|
||||
await self.config.guild(ctx.guild).active.set(active)
|
||||
if active:
|
||||
await ctx.send(_("Reporting now enabled"))
|
||||
await ctx.send(_("Reporting is now enabled"))
|
||||
else:
|
||||
await ctx.send(_("Reporting disabled."))
|
||||
await ctx.send(_("Reporting is now disabled."))
|
||||
|
||||
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||
ret = False
|
||||
if mod:
|
||||
guild = m.guild
|
||||
admin_role = discord.utils.get(
|
||||
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
||||
)
|
||||
mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
|
||||
admin_role = guild.get_role(await self.bot.db.guild(guild).admin_role())
|
||||
mod_role = guild.get_role(await self.bot.db.guild(guild).mod_role())
|
||||
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
||||
if perms:
|
||||
ret |= m.guild_permissions >= perms
|
||||
@@ -105,7 +102,7 @@ class Reports:
|
||||
*,
|
||||
mod: bool = False,
|
||||
permissions: Union[discord.Permissions, dict] = None,
|
||||
prompt: str = ""
|
||||
prompt: str = "",
|
||||
):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
@@ -137,13 +134,14 @@ class Reports:
|
||||
output += "\n{}".format(prompt)
|
||||
|
||||
for page in pagify(output, delims=["\n"]):
|
||||
dm = await author.send(box(page))
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
await author.send(box(page))
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=45)
|
||||
message = await self.bot.wait_for(
|
||||
"message",
|
||||
check=MessagePredicate.same_context(channel=author.dm_channel, user=author),
|
||||
timeout=45,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await author.send(_("You took too long to select. Try again later."))
|
||||
return None
|
||||
@@ -167,7 +165,7 @@ class Reports:
|
||||
if channel is None:
|
||||
return None
|
||||
|
||||
files = await Tunnel.files_from_attatch(msg)
|
||||
files: List[discord.File] = await Tunnel.files_from_attatch(msg)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
@@ -175,7 +173,10 @@ class Reports:
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url
|
||||
name=_("Report from {author}{maybe_nick}").format(
|
||||
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
|
||||
),
|
||||
icon_url=author.avatar_url,
|
||||
)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
send_content = None
|
||||
@@ -200,11 +201,10 @@ class Reports:
|
||||
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||
"""
|
||||
Follow the prompts to make a report
|
||||
"""Send a report.
|
||||
|
||||
optionally use with a report message
|
||||
to use it non interactively
|
||||
Use without arguments for interactive reporting, or do
|
||||
`[p]report <text>` to use it non-interactively.
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -224,14 +224,17 @@ class Reports:
|
||||
if self.antispam[guild.id][author.id].spammy:
|
||||
return await author.send(
|
||||
_(
|
||||
"You've sent a few too many of these recently. "
|
||||
"Contact a server admin to resolve this, or try again "
|
||||
"later."
|
||||
"You've sent too many reports recently. "
|
||||
"Please contact a server admin if this is important matter, "
|
||||
"or please wait and try again later."
|
||||
)
|
||||
)
|
||||
if author.id in self.user_cache:
|
||||
return await author.send(
|
||||
_("Please finish making your prior report before making an additional one")
|
||||
_(
|
||||
"Please finish making your prior report before trying to make an "
|
||||
"additional one!"
|
||||
)
|
||||
)
|
||||
self.user_cache.append(author.id)
|
||||
|
||||
@@ -242,7 +245,7 @@ class Reports:
|
||||
val = await self.send_report(_m, guild)
|
||||
else:
|
||||
try:
|
||||
dm = await author.send(
|
||||
await author.send(
|
||||
_(
|
||||
"Please respond to this message with your Report."
|
||||
"\nYour report should be a single message"
|
||||
@@ -251,11 +254,12 @@ class Reports:
|
||||
except discord.Forbidden:
|
||||
return await ctx.send(_("This requires DMs enabled."))
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
||||
message = await self.bot.wait_for(
|
||||
"message",
|
||||
check=MessagePredicate.same_context(ctx, channel=author.dm_channel),
|
||||
timeout=180,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await author.send(_("You took too long. Try again later."))
|
||||
else:
|
||||
@@ -263,7 +267,9 @@ class Reports:
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
if val is None:
|
||||
await author.send(_("There was an error sending your report."))
|
||||
await author.send(
|
||||
_("There was an error sending your report, please contact a server admin.")
|
||||
)
|
||||
else:
|
||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
@@ -309,17 +315,18 @@ class Reports:
|
||||
if msgs:
|
||||
self.tunnel_store[k]["msgs"] = msgs
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_members=True)
|
||||
@report.command(name="interact")
|
||||
async def response(self, ctx, ticket_number: int):
|
||||
"""
|
||||
opens a message tunnel between things you say in this channel
|
||||
and the ticket opener's direct messages
|
||||
"""Open a message tunnel.
|
||||
|
||||
tunnels do not persist across bot restarts
|
||||
This tunnel will forward things you say in this channel
|
||||
to the ticket opener's direct messages.
|
||||
|
||||
Tunnels do not persist across bot restarts.
|
||||
"""
|
||||
|
||||
# note, mod_or_permissions is an implicit guild_only
|
||||
guild = ctx.guild
|
||||
rec = await self.config.custom("REPORT", guild.id, ticket_number).report()
|
||||
|
||||
@@ -342,23 +349,31 @@ class Reports:
|
||||
)
|
||||
|
||||
big_topic = _(
|
||||
"{who} opened a 2-way communication."
|
||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||
" Anything you say or upload here "
|
||||
"(8MB file size limitation on uploads) "
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded will be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
"You can close a communication at any point by reacting with "
|
||||
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||
"Any message succesfully forwarded will be marked with "
|
||||
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||
"Tunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||
topic = (
|
||||
_(
|
||||
"A moderator in the server `{guild.name}` has opened a 2-way communication about "
|
||||
"ticket number {ticket_number}."
|
||||
).format(guild=guild, ticket_number=ticket_number)
|
||||
+ big_topic
|
||||
)
|
||||
try:
|
||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("User has disabled DMs."))
|
||||
tun.close()
|
||||
await ctx.send(_("That user has DMs disabled."))
|
||||
else:
|
||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You have opened a 2-way communication about ticket number {ticket_number}."
|
||||
).format(ticket_number=ticket_number)
|
||||
+ big_topic
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user