mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
98 Commits
3.0.0b15
...
3.0.0b17.p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf00f5e9a2 | ||
|
|
7685c4d5d5 | ||
|
|
e701ec9617 | ||
|
|
6c1ee096a1 | ||
|
|
2df282222f | ||
|
|
43c7bd48c7 | ||
|
|
86579068d9 | ||
|
|
8e6ab9aa35 | ||
|
|
77566a887a | ||
|
|
9d0eca1914 | ||
|
|
79a3164d9d | ||
|
|
eb73e48192 | ||
|
|
cd6af7f185 | ||
|
|
3d6020b9cf | ||
|
|
461f03aac0 | ||
|
|
35149f8837 | ||
|
|
c0d01f32a6 | ||
|
|
83a0459b6a | ||
|
|
50f6dcef2f | ||
|
|
5c514fd663 | ||
|
|
1c2196f78f | ||
|
|
43cc3c40f3 | ||
|
|
7a6a4cf59d | ||
|
|
3bcf375204 | ||
|
|
a175bdc1c7 | ||
|
|
b557b437a3 | ||
|
|
d1f0b59b5d | ||
|
|
3ece3a1f2b | ||
|
|
1f1a85de18 | ||
|
|
e08c9dafa6 | ||
|
|
ad27607ccc | ||
|
|
c1bcca4432 | ||
|
|
9f2ed694ce | ||
|
|
edadd8f2fd | ||
|
|
afa08713e0 | ||
|
|
d23620727e | ||
|
|
b456c6ad3b | ||
|
|
0298b53803 | ||
|
|
bfd6e4af3f | ||
|
|
31612aae4a | ||
|
|
219367e7c1 | ||
|
|
7b64f10fc7 | ||
|
|
1ad1744054 | ||
|
|
7b825f2cd7 | ||
|
|
3759fce090 | ||
|
|
470521f7c8 | ||
|
|
a070dffb93 | ||
|
|
9e7bc94aab | ||
|
|
033d0113a5 | ||
|
|
d0a53ed2df | ||
|
|
49b80e9fe3 | ||
|
|
d5f5ddbec5 | ||
|
|
17c7dd658d | ||
|
|
ca19ecaefc | ||
|
|
c149f00f82 | ||
|
|
b041d59fc7 | ||
|
|
b983d5904b | ||
|
|
8b15053dd4 | ||
|
|
e15815cd97 | ||
|
|
94a64d8fae | ||
|
|
fd7088de1a | ||
|
|
7d4946560d | ||
|
|
b7c9647e1a | ||
|
|
36b9f64aae | ||
|
|
60a72b2ba4 | ||
|
|
f830f73ae6 | ||
|
|
95f51e1126 | ||
|
|
8916f55d52 | ||
|
|
4aaef9558a | ||
|
|
0b78664792 | ||
|
|
db5d4d5158 | ||
|
|
0dfd8b6453 | ||
|
|
11a2fb1088 | ||
|
|
40feeff442 | ||
|
|
a0a2976e0a | ||
|
|
741f3cbdcc | ||
|
|
a6965c4b5a | ||
|
|
19b05e632c | ||
|
|
8610b47a68 | ||
|
|
2ab8890540 | ||
|
|
5de5a519c3 | ||
|
|
0d193d3e9e | ||
|
|
622382f425 | ||
|
|
c1f09326cc | ||
|
|
ddbbba4aaa | ||
|
|
bcf7ea30c5 | ||
|
|
35e9fab701 | ||
|
|
864b6d313e | ||
|
|
d47d12e961 | ||
|
|
9f0e752318 | ||
|
|
34bd5ead15 | ||
|
|
1fd5dffdc7 | ||
|
|
6d7a900bbb | ||
|
|
fb4f921159 | ||
|
|
14cc701b25 | ||
|
|
d8c4113d24 | ||
|
|
9eb6bb7738 | ||
|
|
de96f8b9f9 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -24,6 +24,7 @@ redbot/core/utils/mod.py @palmtree5
|
|||||||
redbot/core/utils/data_converter.py @mikeshardmind
|
redbot/core/utils/data_converter.py @mikeshardmind
|
||||||
redbot/core/utils/antispam.py @mikeshardmind
|
redbot/core/utils/antispam.py @mikeshardmind
|
||||||
redbot/core/utils/tunnel.py @mikeshardmind
|
redbot/core/utils/tunnel.py @mikeshardmind
|
||||||
|
redbot/core/utils/caching.py @mikeshardmind
|
||||||
|
|
||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
@@ -44,6 +45,7 @@ redbot/cogs/trivia/* @Tobotimus
|
|||||||
redbot/cogs/dataconverter/* @mikeshardmind
|
redbot/cogs/dataconverter/* @mikeshardmind
|
||||||
redbot/cogs/reports/* @mikeshardmind
|
redbot/cogs/reports/* @mikeshardmind
|
||||||
redbot/cogs/permissions/* @mikeshardmind
|
redbot/cogs/permissions/* @mikeshardmind
|
||||||
|
redbot/cogs/warnings/* @palmtree5
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|||||||
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -31,7 +31,7 @@ We love receiving contributions from our community. Any assistance you can provi
|
|||||||
# 2. Ground Rules
|
# 2. Ground Rules
|
||||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||||
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
||||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||||
@@ -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.
|
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:
|
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 (test environment `py36`)
|
||||||
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
- 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`)
|
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||||
|
|
||||||
@@ -94,8 +94,6 @@ Our style checker of choice, [black](https://github.com/ambv/black), actually ha
|
|||||||
|
|
||||||
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>`.
|
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
||||||
|
|
||||||
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
|
|
||||||
|
|
||||||
### 4.4 Make
|
### 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:
|
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||||
1. `make reformat`: Reformat all python files in the project with Black
|
1. `make reformat`: Reformat all python files in the project with Black
|
||||||
|
|||||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,40 +1,15 @@
|
|||||||
# Trivia list repo injection
|
|
||||||
redbot/trivia/
|
|
||||||
|
|
||||||
*.json
|
*.json
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.dll
|
||||||
.data
|
.data
|
||||||
|
!/tests/cogs/dataconverter/data/**/*.json
|
||||||
|
|
||||||
### JetBrains template
|
### JetBrains template
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
# User-specific stuff:
|
# User-specific stuff:
|
||||||
.idea/**/workspace.xml
|
.idea/
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
|
|
||||||
# Sensitive or high-churn files:
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.xml
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
|
|
||||||
# Gradle:
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-debug/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin:
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
## File-based project format:
|
|
||||||
*.iws
|
*.iws
|
||||||
|
|
||||||
## Plugin-specific files:
|
## Plugin-specific files:
|
||||||
|
|||||||
4
Pipfile
4
Pipfile
@@ -4,7 +4,7 @@ verify_ssl = true
|
|||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true}
|
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = '7eb918b19e3e60b56eb9039eb267f8f3477c5e17', editable = true}
|
||||||
"e1839a8" = {path = ".", editable = true}
|
"e1839a8" = {path = ".", editable = true}
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
@@ -14,7 +14,7 @@ pytest-asyncio = "*"
|
|||||||
sphinx = ">1.7"
|
sphinx = ">1.7"
|
||||||
sphinxcontrib-asyncio = "*"
|
sphinxcontrib-asyncio = "*"
|
||||||
sphinx-rtd-theme = "*"
|
sphinx-rtd-theme = "*"
|
||||||
black = {version = "*", python_version = ">= '3.6'"}
|
black = "*"
|
||||||
|
|
||||||
[pipenv]
|
[pipenv]
|
||||||
allow_prereleases = true
|
allow_prereleases = true
|
||||||
|
|||||||
81
Pipfile.lock
generated
81
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d340e4a19777736703970e45766d05d67b973db38b87382b6ef8696cb53abb60"
|
"sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@@ -32,6 +32,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.2.5"
|
"version": "==2.2.5"
|
||||||
},
|
},
|
||||||
|
"aiohttp-json-rpc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
|
||||||
|
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
|
||||||
|
],
|
||||||
|
"version": "==0.8.7"
|
||||||
|
},
|
||||||
"appdirs": {
|
"appdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
@@ -63,7 +70,7 @@
|
|||||||
"discord.py": {
|
"discord.py": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"git": "git://github.com/Rapptz/discord.py",
|
"git": "git://github.com/Rapptz/discord.py",
|
||||||
"ref": "rewrite"
|
"ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
|
||||||
},
|
},
|
||||||
"distro": {
|
"distro": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -76,13 +83,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
"funcsigs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
|
||||||
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
|
||||||
],
|
|
||||||
"version": "==1.0.2"
|
|
||||||
},
|
|
||||||
"fuzzywuzzy": {
|
"fuzzywuzzy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
||||||
@@ -97,19 +97,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.6"
|
"version": "==2.6"
|
||||||
},
|
},
|
||||||
"jsonrpcserver": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
|
|
||||||
],
|
|
||||||
"version": "==3.5.4"
|
|
||||||
},
|
|
||||||
"jsonschema": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
|
|
||||||
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
|
|
||||||
],
|
|
||||||
"version": "==2.6.0"
|
|
||||||
},
|
|
||||||
"multidict": {
|
"multidict": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||||
@@ -166,13 +153,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
|
||||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
|
||||||
],
|
|
||||||
"version": "==1.11.0"
|
|
||||||
},
|
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||||
@@ -248,19 +228,18 @@
|
|||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14",
|
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||||
"sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80"
|
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||||
],
|
],
|
||||||
"version": "==2.5.3"
|
"version": "==2.6.0"
|
||||||
},
|
},
|
||||||
"black": {
|
"black": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4fec2566f9fbbd4a58de50a168cbe3ab952713530410d227e82e4c65d1fad946",
|
"sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
|
||||||
"sha256:5fec0f25486046b9edb97961c946412ced96021247dd1a60ecd9f0567b68b030"
|
"sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.6'",
|
"version": "==18.6b2"
|
||||||
"version": "==18.5b0"
|
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -369,11 +348,11 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d",
|
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
|
||||||
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a"
|
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.6.0"
|
"version": "==3.6.1"
|
||||||
},
|
},
|
||||||
"pytest-asyncio": {
|
"pytest-asyncio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -413,19 +392,19 @@
|
|||||||
},
|
},
|
||||||
"sphinx": {
|
"sphinx": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b",
|
"sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
|
||||||
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10"
|
"sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.7.4"
|
"version": "==1.7.5"
|
||||||
},
|
},
|
||||||
"sphinx-rtd-theme": {
|
"sphinx-rtd-theme": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33",
|
"sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
|
||||||
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d"
|
"sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.3.1"
|
"version": "==0.4.0"
|
||||||
},
|
},
|
||||||
"sphinxcontrib-asyncio": {
|
"sphinxcontrib-asyncio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -436,10 +415,16 @@
|
|||||||
},
|
},
|
||||||
"sphinxcontrib-websupport": {
|
"sphinxcontrib-websupport": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9",
|
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||||
"sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2"
|
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||||
],
|
],
|
||||||
"version": "==1.0.1"
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
|
||||||
|
],
|
||||||
|
"version": "==0.9.4"
|
||||||
},
|
},
|
||||||
"tox": {
|
"tox": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
73
README.rst
73
README.rst
@@ -1,46 +1,47 @@
|
|||||||
.. raw:: html
|
.. class:: center
|
||||||
|
|
||||||
<h1 align="center">
|
.. image:: https://imgur.com/pY1WUFX.png
|
||||||
<br>
|
:target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
|
||||||
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red Discord Bot"></a>
|
:alt: Red Discord Bot
|
||||||
<br>
|
|
||||||
Red Discord Bot
|
|
||||||
<br>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and fully customizable.</h4>
|
.. class:: center
|
||||||
|
|
||||||
.. raw:: html
|
Music, Moderation, Trivia, Stream Alerts and fully customizable.
|
||||||
|
|
||||||
<p align="center">
|
.. class:: center
|
||||||
<a href="https://discord.gg/red">
|
|
||||||
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield">
|
|
||||||
</a>
|
|
||||||
<a href="https://www.patreon.com/Red_Devs">
|
|
||||||
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/Made%20With-Python%203.6-blue.svg?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://crowdin.com/project/red-discordbot">
|
|
||||||
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
|
|
||||||
<img src="https://img.shields.io/badge/discord-py-blue.svg">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
.. raw:: html
|
.. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
|
||||||
|
:target: https://discord.gg/red
|
||||||
|
:alt: Discord server
|
||||||
|
|
||||||
<p align="center">
|
.. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
|
||||||
<a href="#overview">Overview</a> •
|
:target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
|
||||||
<a href="#installation">Installation</a> •
|
:alt: Travis CI status
|
||||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
|
|
||||||
<a href="#plugins"></a> •
|
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
||||||
<a href="#join-the-community">Community</a> •
|
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
||||||
<a href="#license">License</a>
|
:alt: Documentation Status
|
||||||
</p>
|
|
||||||
|
.. image:: https://img.shields.io/badge/discord-py-blue.svg
|
||||||
|
:target: https://github.com/Rapptz/discord.py
|
||||||
|
:alt: discord.py
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||||
|
:target: http://makeapullrequest.com
|
||||||
|
:alt: PRs open
|
||||||
|
|
||||||
==========
|
==========
|
||||||
Overview
|
Overview
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ For each of those, settings have varying priorities (listed below, highest to lo
|
|||||||
7. Role settings (see below)
|
7. Role settings (see below)
|
||||||
8. Server whitelist
|
8. Server whitelist
|
||||||
9. Server blacklist
|
9. Server blacklist
|
||||||
|
10. Default settings
|
||||||
|
|
||||||
For the role whitelist and blacklist settings,
|
For the role whitelist and blacklist settings,
|
||||||
roles will be checked individually in order from highest to lowest role the user has
|
roles will be checked individually in order from highest to lowest role the user has
|
||||||
@@ -73,3 +74,34 @@ An example of the expected format is shown below.
|
|||||||
- 96733288462286848
|
- 96733288462286848
|
||||||
default: allow
|
default: allow
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Example configurations
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Locking Audio cog 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]
|
||||||
|
|
||||||
|
Locking Audio to specific voice channel(s) as a serverowner or admin:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions setguilddefault deny play
|
||||||
|
[p]permissions setguilddefault deny "playlist start"
|
||||||
|
[p]permissions addguildrule allow play [voice channel ID or name]
|
||||||
|
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
|
Allowing extra roles to use cleanup
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions addguildrule allow Cleanup [role ID]
|
||||||
|
|
||||||
|
Preventing cleanup from being used in channels where message history is important:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
[p]permissions addguildrule deny Cleanup [channel ID or mention]
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ RedBase
|
|||||||
:members:
|
:members:
|
||||||
:exclude-members: get_context
|
:exclude-members: get_context
|
||||||
|
|
||||||
|
.. automethod:: register_rpc_handler
|
||||||
|
.. automethod:: unregister_rpc_handler
|
||||||
|
|
||||||
Red
|
Red
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
Commands Package
|
Commands Package
|
||||||
================
|
================
|
||||||
|
|
||||||
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same
|
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||||
attributes. Some of these attributes, however, have been slightly modified, as outlined below.
|
they both have the same attributes. Some of these attributes, however, have been slightly modified,
|
||||||
|
as outlined below.
|
||||||
|
|
||||||
.. autofunction:: redbot.core.commands.command
|
.. autofunction:: redbot.core.commands.command
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Keys common to both repo and cog info.json (case sensitive)
|
|||||||
- ``install_msg`` (string) - The message that gets displayed when a cog
|
- ``install_msg`` (string) - The message that gets displayed when a cog
|
||||||
is installed or a repo is added
|
is installed or a repo is added
|
||||||
|
|
||||||
|
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||||
|
used for installing.
|
||||||
|
|
||||||
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||||
is displayed when a user executes ``!cog list``
|
is displayed when a user executes ``!cog list``
|
||||||
|
|
||||||
@@ -29,7 +32,9 @@ Keys specific to the cog info.json (case sensitive)
|
|||||||
|
|
||||||
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
||||||
|
|
||||||
- ``hidden`` (bool) - Determines if a cog is available for install.
|
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
||||||
|
|
||||||
|
- ``disabled`` (bool) - Determines if a cog is available for install.
|
||||||
|
|
||||||
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
|
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
|
||||||
Downloader will not deal with this functionality but it may be useful for other cogs.
|
Downloader will not deal with this functionality but it may be useful for other cogs.
|
||||||
|
|||||||
@@ -4,36 +4,60 @@
|
|||||||
RPC
|
RPC
|
||||||
===
|
===
|
||||||
|
|
||||||
.. currentmodule:: redbot.core.rpc
|
|
||||||
|
|
||||||
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
||||||
Cogs must register functions to be exposed to RPC clients.
|
Cogs must register functions to be exposed to RPC clients.
|
||||||
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
||||||
|
|
||||||
To begin, register all methods using individual calls to the :func:`Methods.add` method.
|
To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
|
||||||
|
|
||||||
********
|
********
|
||||||
Examples
|
Examples
|
||||||
********
|
********
|
||||||
|
|
||||||
Coming soon to a docs page near you!
|
.. code-block:: Python
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
c = Cog()
|
||||||
|
bot.add_cog(c)
|
||||||
|
bot.register_rpc_handler(c.rpc_method)
|
||||||
|
|
||||||
|
*******************************
|
||||||
|
Interacting with the RPC Server
|
||||||
|
*******************************
|
||||||
|
|
||||||
|
The RPC server opens a websocket bound to port ``6133`` on ``127.0.0.1``.
|
||||||
|
This is not configurable for security reasons as broad access to this server gives anyone complete control over your bot.
|
||||||
|
To access the server you must find a library that implements websocket based JSONRPC in the language of your choice.
|
||||||
|
|
||||||
|
There are a few built-in RPC methods to note:
|
||||||
|
|
||||||
|
* ``GET_METHODS`` - Returns a list of available RPC methods.
|
||||||
|
* ``GET_METHOD_INFO`` - Will return the docstring for an available RPC method. Useful for finding information about the method's parameters and return values.
|
||||||
|
* ``GET_TOPIC`` - Returns a list of available RPC message topics.
|
||||||
|
* ``GET_SUBSCRIPTIONS`` - Returns a list of RPC subscriptions.
|
||||||
|
* ``SUBSCRIBE`` - Subscribes to an available RPC message topic.
|
||||||
|
* ``UNSUBSCRIBE`` - Unsubscribes from an RPC message topic.
|
||||||
|
|
||||||
|
All RPC methods accept a list of parameters.
|
||||||
|
The built-in methods above expect their parameters to be in list format.
|
||||||
|
|
||||||
|
All cog-based methods expect their parameter list to take one argument, a JSON object, in the following format::
|
||||||
|
|
||||||
|
params = [
|
||||||
|
{
|
||||||
|
"args": [], # A list of positional arguments
|
||||||
|
"kwargs": {}, # A dictionary of keyword arguments
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# As an example, here's a call to "get_method_info"
|
||||||
|
rpc_call("GET_METHOD_INFO", ["get_methods",])
|
||||||
|
|
||||||
|
# And here's a call to "core__load"
|
||||||
|
rpc_call("CORE__LOAD", {"args": [["general", "economy", "downloader"],], "kwargs": {}})
|
||||||
|
|
||||||
*************
|
*************
|
||||||
API Reference
|
API Reference
|
||||||
*************
|
*************
|
||||||
|
|
||||||
.. py:attribute:: redbot.core.rpc.methods
|
Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
|
||||||
|
|
||||||
An instance of the :class:`Methods` class.
|
|
||||||
All attempts to register new RPC methods **MUST** use this object.
|
|
||||||
You should never create a new instance of the :class:`Methods` class!
|
|
||||||
|
|
||||||
RPC
|
|
||||||
^^^
|
|
||||||
.. autoclass:: redbot.core.rpc.RPC
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Methods
|
|
||||||
^^^^^^^
|
|
||||||
.. autoclass:: redbot.core.rpc.Methods
|
|
||||||
:members:
|
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ you in the process.
|
|||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
|
||||||
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]`
|
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||||
This will install the latest version of V3.
|
This will install the latest version of V3.
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
-i https://pypi.org/simple
|
-i https://pypi.org/simple
|
||||||
alabaster==0.7.10
|
alabaster==0.7.10
|
||||||
|
appdirs==1.4.3
|
||||||
|
atomicwrites==1.1.5
|
||||||
attrs==18.1.0
|
attrs==18.1.0
|
||||||
babel==2.5.3
|
babel==2.6.0
|
||||||
|
black==18.6b2
|
||||||
certifi==2018.4.16
|
certifi==2018.4.16
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
|
click==6.7
|
||||||
docutils==0.14
|
docutils==0.14
|
||||||
idna==2.6
|
idna==2.6
|
||||||
imagesize==1.0.0
|
imagesize==1.0.0
|
||||||
jinja2==2.10
|
jinja2==2.10
|
||||||
markupsafe==1.0
|
markupsafe==1.0
|
||||||
more-itertools==4.1.0
|
more-itertools==4.2.0
|
||||||
packaging==17.1
|
packaging==17.1
|
||||||
pluggy==0.6.0
|
pluggy==0.6.0
|
||||||
py==1.5.3
|
py==1.5.3
|
||||||
pygments==2.2.0
|
pygments==2.2.0
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
pytest-asyncio==0.8.0
|
pytest-asyncio==0.8.0
|
||||||
pytest==3.5.1
|
pytest==3.6.1
|
||||||
pytz==2018.4
|
pytz==2018.4
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
snowballstemmer==1.2.1
|
snowballstemmer==1.2.1
|
||||||
sphinx-rtd-theme==0.3.1
|
sphinx-rtd-theme==0.4.0
|
||||||
sphinx==1.7.4
|
sphinx==1.7.5
|
||||||
sphinxcontrib-asyncio==0.2.0
|
sphinxcontrib-asyncio==0.2.0
|
||||||
sphinxcontrib-websupport==1.0.1
|
sphinxcontrib-websupport==1.1.0
|
||||||
|
toml==0.9.4
|
||||||
|
tox==3.0.0
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0
|
virtualenv==16.0.0
|
||||||
|
yarl==0.18.0
|
||||||
|
git+https://github.com/Rapptz/discord.py@7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0
|
||||||
|
|||||||
14
generate_strings.py
Normal file → Executable file
14
generate_strings.py
Normal file → Executable file
@@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -13,25 +14,24 @@ def main():
|
|||||||
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
||||||
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
||||||
print(
|
print(
|
||||||
"Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(
|
f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
|
||||||
d
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
exit(1)
|
return 1
|
||||||
else:
|
else:
|
||||||
print("Running 'regen_messages.py' for {}".format(d))
|
print("Running 'regen_messages.py' for {}".format(d))
|
||||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||||
if retval.returncode != 0:
|
if retval.returncode != 0:
|
||||||
exit(1)
|
return 1
|
||||||
os.chdir(root_dir)
|
os.chdir(root_dir)
|
||||||
os.chdir("redbot/core/locales")
|
os.chdir("redbot/core/locales")
|
||||||
print("Running 'regen_messages.py' for core")
|
print("Running 'regen_messages.py' for core")
|
||||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||||
if retval.returncode != 0:
|
if retval.returncode != 0:
|
||||||
exit(1)
|
return 1
|
||||||
os.chdir(root_dir)
|
os.chdir(root_dir)
|
||||||
subprocess.run(["crowdin", "upload"])
|
subprocess.run(["crowdin", "upload"])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|||||||
@@ -12,12 +12,3 @@ if discord.version_info.major < 1:
|
|||||||
" >= 1.0.0."
|
" >= 1.0.0."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if sys.version_info < (3, 6, 0):
|
|
||||||
print(Back.RED + "[DEPRECATION WARNING]")
|
|
||||||
print(
|
|
||||||
Back.RED + "You are currently running Python 3.5."
|
|
||||||
" Support for Python 3.5 will end with the release of beta 16."
|
|
||||||
" Please update your environment to Python 3.6 as soon as possible to avoid"
|
|
||||||
" any interruptions after the beta 16 release."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import sys
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core.bot import Red, ExitCodes
|
from redbot.core.bot import Red, ExitCodes
|
||||||
from redbot.core.cog_manager import CogManagerUI
|
from redbot.core.cog_manager import CogManagerUI
|
||||||
from redbot.core.data_manager import load_basic_configuration, config_file
|
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
||||||
from redbot.core.json_io import JsonIO
|
from redbot.core.json_io import JsonIO
|
||||||
from redbot.core.global_checks import init_global_checks
|
from redbot.core.global_checks import init_global_checks
|
||||||
from redbot.core.events import init_events
|
from redbot.core.events import init_events
|
||||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||||
from redbot.core.core_commands import Core
|
from redbot.core.core_commands import Core
|
||||||
from redbot.core.dev_commands import Dev
|
from redbot.core.dev_commands import Dev
|
||||||
from redbot.core import rpc, __version__
|
from redbot.core import __version__
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import logging
|
import logging
|
||||||
@@ -40,7 +40,7 @@ def init_loggers(cli_flags):
|
|||||||
logger = logging.getLogger("red")
|
logger = logging.getLogger("red")
|
||||||
|
|
||||||
red_format = logging.Formatter(
|
red_format = logging.Formatter(
|
||||||
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: " "%(message)s",
|
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
|
||||||
datefmt="[%d/%m/%Y %H:%M]",
|
datefmt="[%d/%m/%Y %H:%M]",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,12 +106,20 @@ def main():
|
|||||||
elif cli_flags.version:
|
elif cli_flags.version:
|
||||||
print(description)
|
print(description)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif not cli_flags.instance_name:
|
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||||
print("Error: No instance name was provided!")
|
print("Error: No instance name was provided!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if cli_flags.no_instance:
|
||||||
|
print(
|
||||||
|
"\033[1m"
|
||||||
|
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
||||||
|
"\033[0m"
|
||||||
|
)
|
||||||
|
cli_flags.instance_name = "temporary_red"
|
||||||
|
create_temp_config()
|
||||||
load_basic_configuration(cli_flags.instance_name)
|
load_basic_configuration(cli_flags.instance_name)
|
||||||
log, sentry_log = init_loggers(cli_flags)
|
log, sentry_log = init_loggers(cli_flags)
|
||||||
red = Red(cli_flags, description=description, pm_help=None)
|
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
red.add_cog(Core(red))
|
red.add_cog(Core(red))
|
||||||
@@ -122,8 +130,10 @@ def main():
|
|||||||
tmp_data = {}
|
tmp_data = {}
|
||||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||||
|
if cli_flags.token:
|
||||||
|
token = cli_flags.token
|
||||||
prefix = cli_flags.prefix or tmp_data["prefix"]
|
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||||
if token is None or not prefix:
|
if not (token and prefix):
|
||||||
if cli_flags.no_prompt is False:
|
if cli_flags.no_prompt is False:
|
||||||
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
|
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
|
||||||
if new_token:
|
if new_token:
|
||||||
@@ -138,18 +148,16 @@ def main():
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if tmp_data["enable_sentry"]:
|
if tmp_data["enable_sentry"]:
|
||||||
red.enable_sentry()
|
red.enable_sentry()
|
||||||
cleanup_tasks = True
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
cleanup_tasks = False # No login happened, no need for this
|
|
||||||
log.critical(
|
log.critical(
|
||||||
"This token doesn't seem to be valid. If it belongs to "
|
"This token doesn't seem to be valid. If it belongs to "
|
||||||
"a user account, remember that the --not-bot flag "
|
"a user account, remember that the --not-bot flag "
|
||||||
"must be used. For self-bot functionalities instead, "
|
"must be used. For self-bot functionalities instead, "
|
||||||
"--self-bot"
|
"--self-bot"
|
||||||
)
|
)
|
||||||
db_token = red.db.token()
|
db_token = loop.run_until_complete(red.db.token())
|
||||||
if db_token and not cli_flags.no_prompt:
|
if db_token and not cli_flags.no_prompt:
|
||||||
print("\nDo you want to reset the token? (y/n)")
|
print("\nDo you want to reset the token? (y/n)")
|
||||||
if confirm("> "):
|
if confirm("> "):
|
||||||
@@ -164,10 +172,13 @@ def main():
|
|||||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||||
loop.run_until_complete(red.logout())
|
loop.run_until_complete(red.logout())
|
||||||
finally:
|
finally:
|
||||||
if cleanup_tasks:
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||||
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
gathered.cancel()
|
||||||
gathered.cancel()
|
try:
|
||||||
|
red.rpc.server.close()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
sys.exit(red._shutdown_mode.value)
|
sys.exit(red._shutdown_mode.value)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core import Config, checks
|
from redbot.core import Config, checks, commands
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ RUNNING_ANNOUNCEMENT = (
|
|||||||
|
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
|
|
||||||
def __init__(self, config=Config):
|
def __init__(self, config=Config):
|
||||||
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||||
|
|
||||||
@@ -129,8 +127,8 @@ class Admin:
|
|||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Adds a role to a user. If user is left blank it defaults to the
|
Adds a role to a user.
|
||||||
author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -147,8 +145,8 @@ class Admin:
|
|||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Removes a role from a user. If user is left blank it defaults to the
|
Removes a role from a user.
|
||||||
author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -163,8 +161,7 @@ class Admin:
|
|||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def editrole(self, ctx: commands.Context):
|
async def editrole(self, ctx: commands.Context):
|
||||||
"""Edits roles settings"""
|
"""Edits roles settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@editrole.command(name="colour", aliases=["color"])
|
@editrole.command(name="colour", aliases=["color"])
|
||||||
async def editrole_colour(
|
async def editrole_colour(
|
||||||
@@ -265,20 +262,16 @@ class Admin:
|
|||||||
@announce.command(name="ignore")
|
@announce.command(name="ignore")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def announce_ignore(self, ctx, *, guild: discord.Guild = None):
|
async def announce_ignore(self, ctx):
|
||||||
"""
|
"""
|
||||||
Toggles whether the announcements will ignore the given server.
|
Toggles whether the announcements will ignore the current server.
|
||||||
Defaults to the current server if none is provided.
|
|
||||||
"""
|
"""
|
||||||
if guild is None:
|
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||||
guild = ctx.guild
|
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||||
|
|
||||||
ignored = await self.conf.guild(guild).announce_ignore()
|
|
||||||
await self.conf.guild(guild).announce_ignore.set(not ignored)
|
|
||||||
|
|
||||||
verb = "will" if ignored else "will not"
|
verb = "will" if ignored else "will not"
|
||||||
|
|
||||||
await ctx.send("The server {} {} receive announcements.".format(guild.name, verb))
|
await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
|
||||||
|
|
||||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||||
"""
|
"""
|
||||||
@@ -298,11 +291,13 @@ class Admin:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return valid_roles
|
return valid_roles
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""
|
||||||
Add a role to yourself that server admins have configured as
|
Add a role to yourself that server admins have configured as user settable.
|
||||||
user settable.
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, ctx.author, selfrole)
|
await self._addrole(ctx, ctx.author, selfrole)
|
||||||
@@ -311,15 +306,19 @@ class Admin:
|
|||||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""
|
||||||
Removes a selfrole from yourself.
|
Removes a selfrole from yourself.
|
||||||
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, ctx.author, selfrole)
|
await self._removerole(ctx, ctx.author, selfrole)
|
||||||
|
|
||||||
@selfrole.command(name="add")
|
@selfrole.command(name="add")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""
|
"""
|
||||||
Add a role to the list of available selfroles.
|
Add a role to the list of available selfroles.
|
||||||
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
if role.id not in curr_selfroles:
|
if role.id not in curr_selfroles:
|
||||||
@@ -328,10 +327,12 @@ class Admin:
|
|||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send("The selfroles list has been successfully modified.")
|
||||||
|
|
||||||
@selfrole.command(name="delete")
|
@selfrole.command(name="delete")
|
||||||
@commands.has_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||||
"""
|
"""
|
||||||
Removes a role from the list of available selfroles.
|
Removes a role from the list of available selfroles.
|
||||||
|
|
||||||
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
curr_selfroles.remove(role.id)
|
curr_selfroles.remove(role.id)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
|
|
||||||
class Announcer:
|
class Announcer:
|
||||||
|
|
||||||
def __init__(self, ctx: commands.Context, message: str, config=None):
|
def __init__(self, ctx: commands.Context, message: str, config=None):
|
||||||
"""
|
"""
|
||||||
:param ctx:
|
:param ctx:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
|
|
||||||
class MemberDefaultAuthor(commands.Converter):
|
class MemberDefaultAuthor(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
|
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
|
||||||
member_converter = commands.MemberConverter()
|
member_converter = commands.MemberConverter()
|
||||||
try:
|
try:
|
||||||
@@ -17,7 +16,6 @@ class MemberDefaultAuthor(commands.Converter):
|
|||||||
|
|
||||||
|
|
||||||
class SelfRole(commands.Converter):
|
class SelfRole(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
||||||
admin = ctx.command.instance
|
admin = ctx.command.instance
|
||||||
if admin is None:
|
if admin is None:
|
||||||
@@ -30,5 +28,5 @@ class SelfRole(commands.Converter):
|
|||||||
role = await role_converter.convert(ctx, arg)
|
role = await role_converter.convert(ctx, arg)
|
||||||
|
|
||||||
if role.id not in selfroles:
|
if role.id not in selfroles:
|
||||||
raise commands.BadArgument("The provided role is not a valid" " selfrole.")
|
raise commands.BadArgument("The provided role is not a valid selfrole.")
|
||||||
return role
|
return role
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from .alias import Alias
|
from .alias import Alias
|
||||||
from discord.ext import commands
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: Red):
|
||||||
bot.add_cog(Alias(bot))
|
bot.add_cog(Alias(bot))
|
||||||
|
|||||||
@@ -174,16 +174,14 @@ class Alias:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def alias(self, ctx: commands.Context):
|
async def alias(self, ctx: commands.Context):
|
||||||
"""Manage per-server aliases for commands"""
|
"""Manage per-server aliases for commands"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@alias.group(name="global")
|
@alias.group(name="global")
|
||||||
async def global_(self, ctx: commands.Context):
|
async def global_(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Manage global aliases.
|
Manage global aliases.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@checks.mod_or_permissions(manage_guild=True)
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="add")
|
@alias.command(name="add")
|
||||||
@@ -233,9 +231,7 @@ class Alias:
|
|||||||
|
|
||||||
await self.add_alias(ctx, alias_name, command)
|
await self.add_alias(ctx, alias_name, command)
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
|
||||||
_("A new alias with the trigger `{}`" " has been created.").format(alias_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@global_.command(name="add")
|
@global_.command(name="add")
|
||||||
@@ -282,14 +278,14 @@ class Alias:
|
|||||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A new global alias with the trigger `{}`" " has been created.").format(alias_name)
|
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@alias.command(name="help")
|
@alias.command(name="help")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Tries to execute help for the base command of the alias"""
|
"""Tries to execute help for the base command of the alias"""
|
||||||
is_alias, alias = self.is_alias(ctx.guild, alias_name=alias_name)
|
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||||
if is_alias:
|
if is_alias:
|
||||||
base_cmd = alias.command[0]
|
base_cmd = alias.command[0]
|
||||||
|
|
||||||
@@ -307,9 +303,7 @@ class Alias:
|
|||||||
|
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The `{}` alias will execute the" " command `{}`").format(
|
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
|
||||||
alias_name, alias.command
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
||||||
@@ -330,7 +324,7 @@ class Alias:
|
|||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name):
|
if await self.delete_alias(ctx, alias_name):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||||
@@ -350,7 +344,7 @@ class Alias:
|
|||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from redbot.core import commands
|
|||||||
|
|
||||||
|
|
||||||
class AliasEntry:
|
class AliasEntry:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
|
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
|
|
||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
from .manager import start_lavalink_server
|
from .manager import start_lavalink_server
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
import redbot.core
|
import redbot.core
|
||||||
|
|
||||||
|
log = logging.getLogger("red.audio")
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_URL = (
|
LAVALINK_DOWNLOAD_URL = (
|
||||||
"https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar"
|
"https://github.com/Cog-Creators/Red-DiscordBot/releases/download/{}/Lavalink.jar"
|
||||||
).format(redbot.core.__version__)
|
).format(redbot.core.__version__)
|
||||||
|
|
||||||
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
||||||
@@ -33,15 +36,13 @@ async def maybe_download_lavalink(loop, cog):
|
|||||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
jar_exists = LAVALINK_JAR_FILE.exists()
|
||||||
current_build = redbot.core.VersionInfo(*await cog.config.current_build())
|
current_build = redbot.core.VersionInfo(*await cog.config.current_build())
|
||||||
|
|
||||||
session = ClientSession(loop=loop)
|
|
||||||
|
|
||||||
if not jar_exists or current_build < redbot.core.version_info:
|
if not jar_exists or current_build < redbot.core.version_info:
|
||||||
|
log.info("Downloading Lavalink.jar")
|
||||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
await download_lavalink(session)
|
async with ClientSession(loop=loop) as session:
|
||||||
|
await download_lavalink(session)
|
||||||
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
||||||
|
|
||||||
|
|
||||||
@@ -52,4 +53,5 @@ async def setup(bot: commands.Bot):
|
|||||||
await start_lavalink_server(bot.loop)
|
await start_lavalink_server(bot.loop)
|
||||||
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
|
bot.loop.create_task(cog.disconnect_timer())
|
||||||
bot.loop.create_task(cog.init_config())
|
bot.loop.create_task(cog.init_config())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import heapq
|
|||||||
import lavalink
|
import lavalink
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import redbot.core
|
import redbot.core
|
||||||
from redbot.core import Config, commands, checks, bank
|
from redbot.core import Config, commands, checks, bank
|
||||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
|
||||||
@@ -14,13 +15,12 @@ from .manager import shutdown_lavalink_server
|
|||||||
|
|
||||||
_ = Translator("Audio", __file__)
|
_ = Translator("Audio", __file__)
|
||||||
|
|
||||||
__version__ = "0.0.6a"
|
__version__ = "0.0.6c"
|
||||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Audio:
|
class Audio:
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
||||||
@@ -38,6 +38,8 @@ class Audio:
|
|||||||
default_guild = {
|
default_guild = {
|
||||||
"dj_enabled": False,
|
"dj_enabled": False,
|
||||||
"dj_role": None,
|
"dj_role": None,
|
||||||
|
"emptydc_enabled": False,
|
||||||
|
"emptydc_timer": 0,
|
||||||
"jukebox": False,
|
"jukebox": False,
|
||||||
"jukebox_price": 0,
|
"jukebox_price": 0,
|
||||||
"playlists": {},
|
"playlists": {},
|
||||||
@@ -168,8 +170,7 @@ class Audio:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def audioset(self, ctx):
|
async def audioset(self, ctx):
|
||||||
"""Music configuration options."""
|
"""Music configuration options."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
@@ -197,6 +198,26 @@ class Audio:
|
|||||||
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
|
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
|
||||||
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled))
|
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled))
|
||||||
|
|
||||||
|
@audioset.command()
|
||||||
|
@checks.mod_or_permissions(administrator=True)
|
||||||
|
async def emptydisconnect(self, ctx, seconds: int):
|
||||||
|
"""Auto-disconnection after x seconds while stopped. 0 to disable."""
|
||||||
|
if seconds < 0:
|
||||||
|
return await self._embed_msg(ctx, "Can't be less than zero.")
|
||||||
|
if seconds < 10 and seconds > 0:
|
||||||
|
seconds = 10
|
||||||
|
if seconds == 0:
|
||||||
|
enabled = False
|
||||||
|
await self._embed_msg(ctx, "Empty disconnect disabled.")
|
||||||
|
else:
|
||||||
|
enabled = True
|
||||||
|
await self._embed_msg(
|
||||||
|
ctx, "Empty disconnect timer set to {}.".format(self._dynamic_time(seconds))
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.config.guild(ctx.guild).emptydc_timer.set(seconds)
|
||||||
|
await self.config.guild(ctx.guild).emptydc_enabled.set(enabled)
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def role(self, ctx, role_name: discord.Role):
|
async def role(self, ctx, role_name: discord.Role):
|
||||||
@@ -242,12 +263,16 @@ class Audio:
|
|||||||
global_data = await self.config.all()
|
global_data = await self.config.all()
|
||||||
dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"])
|
dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"])
|
||||||
dj_enabled = data["dj_enabled"]
|
dj_enabled = data["dj_enabled"]
|
||||||
|
emptydc_enabled = data["emptydc_enabled"]
|
||||||
|
emptydc_timer = data["emptydc_timer"]
|
||||||
jukebox = data["jukebox"]
|
jukebox = data["jukebox"]
|
||||||
jukebox_price = data["jukebox_price"]
|
jukebox_price = data["jukebox_price"]
|
||||||
jarbuild = redbot.core.__version__
|
jarbuild = redbot.core.__version__
|
||||||
|
|
||||||
vote_percent = data["vote_percent"]
|
vote_percent = data["vote_percent"]
|
||||||
msg = "```ini\n" "----Server Settings----\n"
|
msg = "```ini\n" "----Server Settings----\n"
|
||||||
|
if emptydc_enabled:
|
||||||
|
msg += "Disconnect timer: [{0}]\n".format(self._dynamic_time(emptydc_timer))
|
||||||
if dj_enabled:
|
if dj_enabled:
|
||||||
msg += "DJ Role: [{}]\n".format(dj_role_obj.name)
|
msg += "DJ Role: [{}]\n".format(dj_role_obj.name)
|
||||||
if jukebox:
|
if jukebox:
|
||||||
@@ -426,7 +451,11 @@ class Audio:
|
|||||||
await message.add_reaction(expected[i])
|
await message.add_reaction(expected[i])
|
||||||
|
|
||||||
def check(r, u):
|
def check(r, u):
|
||||||
return r.message.id == message.id and u == ctx.message.author
|
return (
|
||||||
|
r.message.id == message.id
|
||||||
|
and u == ctx.message.author
|
||||||
|
and any(e in str(r.emoji) for e in expected)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0)
|
(r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0)
|
||||||
@@ -557,6 +586,12 @@ class Audio:
|
|||||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -590,7 +625,7 @@ class Audio:
|
|||||||
|
|
||||||
queue_duration = await self._queue_duration(ctx)
|
queue_duration = await self._queue_duration(ctx)
|
||||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||||
before_queue_length = len(player.queue) + 1
|
before_queue_length = len(player.queue)
|
||||||
|
|
||||||
if "list" in query and "ytsearch:" not in query:
|
if "list" in query and "ytsearch:" not in query:
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
@@ -603,7 +638,7 @@ class Audio:
|
|||||||
if not shuffle and queue_duration > 0:
|
if not shuffle and queue_duration > 0:
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text="{} until start of playlist playback: starts at #{} in queue".format(
|
text="{} until start of playlist playback: starts at #{} in queue".format(
|
||||||
queue_total_duration, before_queue_length
|
queue_total_duration, before_queue_length + 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not player.current:
|
if not player.current:
|
||||||
@@ -619,11 +654,11 @@ class Audio:
|
|||||||
if not shuffle and queue_duration > 0:
|
if not shuffle and queue_duration > 0:
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text="{} until track playback: #{} in queue".format(
|
text="{} until track playback: #{} in queue".format(
|
||||||
queue_total_duration, before_queue_length
|
queue_total_duration, before_queue_length + 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif queue_duration > 0:
|
elif queue_duration > 0:
|
||||||
embed.set_footer(text="#{} in queue".format(len(player.queue) + 1))
|
embed.set_footer(text="#{} in queue".format(len(player.queue)))
|
||||||
if not player.current:
|
if not player.current:
|
||||||
await player.play()
|
await player.play()
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
@@ -632,8 +667,7 @@ class Audio:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def playlist(self, ctx):
|
async def playlist(self, ctx):
|
||||||
"""Playlist configuration options."""
|
"""Playlist configuration options."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@playlist.command(name="append")
|
@playlist.command(name="append")
|
||||||
async def _playlist_append(self, ctx, playlist_name, *url):
|
async def _playlist_append(self, ctx, playlist_name, *url):
|
||||||
@@ -680,8 +714,10 @@ class Audio:
|
|||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx, "Playlist name already exists, try again with a different name."
|
ctx, "Playlist name already exists, try again with a different name."
|
||||||
)
|
)
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlist_list = self._to_json(ctx, None, None)
|
playlist_list = self._to_json(ctx, None, None)
|
||||||
playlists[playlist_name] = playlist_list
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlists[playlist_name] = playlist_list
|
||||||
await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name))
|
await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name))
|
||||||
|
|
||||||
@playlist.command(name="delete")
|
@playlist.command(name="delete")
|
||||||
@@ -740,6 +776,7 @@ class Audio:
|
|||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.cooldown(1, 15, discord.ext.commands.BucketType.guild)
|
||||||
@playlist.command(name="queue")
|
@playlist.command(name="queue")
|
||||||
async def _playlist_queue(self, ctx, playlist_name=None):
|
async def _playlist_queue(self, ctx, playlist_name=None):
|
||||||
"""Save the queue to a playlist."""
|
"""Save the queue to a playlist."""
|
||||||
@@ -766,11 +803,11 @@ class Audio:
|
|||||||
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
||||||
|
|
||||||
def check(m):
|
def check(m):
|
||||||
return m.author == ctx.author
|
return m.author == ctx.author and not m.content.startswith(ctx.prefix)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
|
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
|
||||||
playlist_name = str(playlist_name_msg.content)
|
playlist_name = playlist_name_msg.content.split(" ")[0].strip('"')
|
||||||
if len(playlist_name) > 20:
|
if len(playlist_name) > 20:
|
||||||
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
||||||
if playlist_name in playlists:
|
if playlist_name in playlists:
|
||||||
@@ -781,11 +818,12 @@ class Audio:
|
|||||||
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
||||||
playlist_list = self._to_json(ctx, None, tracklist)
|
playlist_list = self._to_json(ctx, None, tracklist)
|
||||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlists[playlist_name] = playlist_list
|
playlists[playlist_name] = playlist_list
|
||||||
await self._embed_msg(
|
await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
"Playlist {} saved from current queue: {} tracks added.".format(
|
"Playlist {} saved from current queue: {} tracks added.".format(
|
||||||
playlist_name, len(tracklist)
|
playlist_name.split(" ")[0].strip('"'), len(tracklist)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -833,6 +871,7 @@ class Audio:
|
|||||||
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
||||||
if tracklist is not None:
|
if tracklist is not None:
|
||||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||||
|
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||||
playlists[playlist_name] = playlist_list
|
playlists[playlist_name] = playlist_list
|
||||||
return await self._embed_msg(
|
return await self._embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -891,8 +930,11 @@ class Audio:
|
|||||||
file_suffix = file_url.rsplit(".", 1)[1]
|
file_suffix = file_url.rsplit(".", 1)[1]
|
||||||
if file_suffix != "txt":
|
if file_suffix != "txt":
|
||||||
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
||||||
async with self.session.request("GET", file_url) as r:
|
try:
|
||||||
v2_playlist = await r.json(content_type="text/plain")
|
async with self.session.request("GET", file_url) as r:
|
||||||
|
v2_playlist = await r.json(content_type="text/plain")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return await self._embed_msg(ctx, "Not a valid playlist file.")
|
||||||
try:
|
try:
|
||||||
v2_playlist_url = v2_playlist["link"]
|
v2_playlist_url = v2_playlist["link"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -961,6 +1003,12 @@ class Audio:
|
|||||||
return False
|
return False
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -1178,6 +1226,12 @@ class Audio:
|
|||||||
"""
|
"""
|
||||||
if not self._player_check(ctx):
|
if not self._player_check(ctx):
|
||||||
try:
|
try:
|
||||||
|
if not ctx.author.voice.channel.permissions_for(
|
||||||
|
ctx.me
|
||||||
|
).connect == True or self._userlimit(ctx.author.voice.channel):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, "I don't have permission to connect to your channel."
|
||||||
|
)
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(ctx.author.voice.channel)
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -1195,10 +1249,10 @@ class Audio:
|
|||||||
|
|
||||||
query = query.strip("<>")
|
query = query.strip("<>")
|
||||||
if query.startswith("list "):
|
if query.startswith("list "):
|
||||||
query = "ytsearch:{}".format(query.lstrip("list "))
|
query = "ytsearch:{}".format(query.replace("list ", ""))
|
||||||
tracks = await player.get_tracks(query)
|
tracks = await player.get_tracks(query)
|
||||||
if not tracks:
|
if not tracks:
|
||||||
return await self._embed_msg(ctx, "Nothing found 👀")
|
return await self._embed_msg(ctx, "Nothing found.")
|
||||||
songembed = discord.Embed(
|
songembed = discord.Embed(
|
||||||
colour=ctx.guild.me.top_role.colour,
|
colour=ctx.guild.me.top_role.colour,
|
||||||
title="Queued {} track(s).".format(len(tracks)),
|
title="Queued {} track(s).".format(len(tracks)),
|
||||||
@@ -1217,12 +1271,12 @@ class Audio:
|
|||||||
await player.play()
|
await player.play()
|
||||||
return await ctx.send(embed=songembed)
|
return await ctx.send(embed=songembed)
|
||||||
if query.startswith("sc "):
|
if query.startswith("sc "):
|
||||||
query = "scsearch:{}".format(query.lstrip("sc "))
|
query = "scsearch:{}".format(query.replace("sc ", ""))
|
||||||
elif not query.startswith("http"):
|
elif not query.startswith("http"):
|
||||||
query = "ytsearch:{}".format(query)
|
query = "ytsearch:{}".format(query)
|
||||||
tracks = await player.get_tracks(query)
|
tracks = await player.get_tracks(query)
|
||||||
if not tracks:
|
if not tracks:
|
||||||
return await self._embed_msg(ctx, "Nothing found 👀")
|
return await self._embed_msg(ctx, "Nothing found.")
|
||||||
|
|
||||||
len_search_pages = math.ceil(len(tracks) / 5)
|
len_search_pages = math.ceil(len(tracks) / 5)
|
||||||
search_page_list = []
|
search_page_list = []
|
||||||
@@ -1485,11 +1539,13 @@ class Audio:
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
async def _skip_action(self, ctx):
|
||||||
async def _skip_action(ctx):
|
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
if not player.queue:
|
if not player.queue:
|
||||||
pos, dur = player.position, player.current.length
|
try:
|
||||||
|
pos, dur = player.position, player.current.length
|
||||||
|
except AttributeError:
|
||||||
|
return await self._embed_msg(ctx, "There's nothing in the queue.")
|
||||||
time_remain = lavalink.utils.format_time(dur - pos)
|
time_remain = lavalink.utils.format_time(dur - pos)
|
||||||
if player.current.is_stream:
|
if player.current.is_stream:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
@@ -1593,8 +1649,7 @@ class Audio:
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def llsetup(self, ctx):
|
async def llsetup(self, ctx):
|
||||||
"""Lavalink server configuration options."""
|
"""Lavalink server configuration options."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@llsetup.command()
|
@llsetup.command()
|
||||||
async def external(self, ctx):
|
async def external(self, ctx):
|
||||||
@@ -1711,6 +1766,34 @@ class Audio:
|
|||||||
if player.volume != volume:
|
if player.volume != volume:
|
||||||
await player.set_volume(volume)
|
await player.set_volume(volume)
|
||||||
|
|
||||||
|
async def disconnect_timer(self):
|
||||||
|
stop_times = {}
|
||||||
|
|
||||||
|
while self == self.bot.get_cog("Audio"):
|
||||||
|
for p in lavalink.players:
|
||||||
|
server = p.channel.guild
|
||||||
|
|
||||||
|
if server.id not in stop_times:
|
||||||
|
stop_times[server.id] = None
|
||||||
|
|
||||||
|
if [self.bot.user] == p.channel.members:
|
||||||
|
if stop_times[server.id] is None:
|
||||||
|
stop_times[server.id] = int(time.time())
|
||||||
|
|
||||||
|
for sid in stop_times:
|
||||||
|
server_obj = self.bot.get_guild(sid)
|
||||||
|
emptydc_enabled = await self.config.guild(server_obj).emptydc_enabled()
|
||||||
|
if emptydc_enabled:
|
||||||
|
if stop_times[sid] is not None and [self.bot.user] == p.channel.members:
|
||||||
|
emptydc_timer = await self.config.guild(server_obj).emptydc_timer()
|
||||||
|
if stop_times[sid] and (
|
||||||
|
int(time.time()) - stop_times[sid] > emptydc_timer
|
||||||
|
):
|
||||||
|
stop_times[sid] = None
|
||||||
|
await lavalink.get_player(sid).disconnect()
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _draw_time(ctx):
|
async def _draw_time(ctx):
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
@@ -1826,6 +1909,15 @@ class Audio:
|
|||||||
track_obj[key] = value
|
track_obj[key] = value
|
||||||
return track_obj
|
return track_obj
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _userlimit(channel):
|
||||||
|
if channel.user_limit == 0:
|
||||||
|
return False
|
||||||
|
if channel.user_limit < len(channel.members) + 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
async def on_voice_state_update(self, member, before, after):
|
async def on_voice_state_update(self, member, before, after):
|
||||||
if after.channel != before.channel:
|
if after.channel != before.channel:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import asyncio
|
|||||||
from subprocess import Popen, DEVNULL, PIPE
|
from subprocess import Popen, DEVNULL, PIPE
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
_JavaVersion = Tuple[int, int]
|
||||||
|
|
||||||
log = logging.getLogger("red.audio.manager")
|
log = logging.getLogger("red.audio.manager")
|
||||||
|
|
||||||
@@ -36,16 +39,16 @@ async def monitor_lavalink_server(loop):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def has_java(loop):
|
async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
|
||||||
java_available = shutil.which("java") is not None
|
java_available = shutil.which("java") is not None
|
||||||
if not java_available:
|
if not java_available:
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
version = await get_java_version(loop)
|
version = await get_java_version(loop)
|
||||||
return version >= (1, 8), version
|
return version >= (1, 8), version
|
||||||
|
|
||||||
|
|
||||||
async def get_java_version(loop):
|
async def get_java_version(loop) -> _JavaVersion:
|
||||||
"""
|
"""
|
||||||
This assumes we've already checked that java exists.
|
This assumes we've already checked that java exists.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
|
|||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
return author == ctx.guild.owner or permissions.administrator
|
return author == ctx.guild.owner or permissions.administrator
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
@@ -36,15 +38,17 @@ def check_global_setting_admin():
|
|||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if await ctx.bot.is_owner(author):
|
|
||||||
return True
|
|
||||||
if not await bank.is_global():
|
if not await bank.is_global():
|
||||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||||
return False
|
return False
|
||||||
|
if await ctx.bot.is_owner(author):
|
||||||
|
return True
|
||||||
permissions = ctx.channel.permissions_for(author)
|
permissions = ctx.channel.permissions_for(author)
|
||||||
is_guild_owner = author == ctx.guild.owner
|
is_guild_owner = author == ctx.guild.owner
|
||||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||||
|
else:
|
||||||
|
return await ctx.bot.is_owner(author)
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.check(pred)
|
||||||
|
|
||||||
@@ -58,8 +62,9 @@ class Bank:
|
|||||||
|
|
||||||
# SECTION commands
|
# SECTION commands
|
||||||
|
|
||||||
@commands.group()
|
@check_global_setting_guildowner()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.group(autohelp=True)
|
||||||
async def bankset(self, ctx: commands.Context):
|
async def bankset(self, ctx: commands.Context):
|
||||||
"""Base command for bank settings"""
|
"""Base command for bank settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
@@ -69,17 +74,15 @@ class Bank:
|
|||||||
default_balance = await bank._conf.default_balance()
|
default_balance = await bank._conf.default_balance()
|
||||||
else:
|
else:
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
await ctx.send_help()
|
|
||||||
return
|
return
|
||||||
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
||||||
currency_name = await bank._conf.guild(ctx.guild).currency()
|
currency_name = await bank._conf.guild(ctx.guild).currency()
|
||||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||||
|
|
||||||
settings = _(
|
settings = _(
|
||||||
"Bank settings:\n\n" "Bank name: {}\n" "Currency: {}\n" "Default balance: {}" ""
|
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
|
||||||
).format(bank_name, currency_name, default_balance)
|
).format(bank_name, currency_name, default_balance)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@bankset.command(name="toggleglobal")
|
@bankset.command(name="toggleglobal")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ class Cleanup:
|
|||||||
to_delete = []
|
to_delete = []
|
||||||
too_old = False
|
too_old = False
|
||||||
|
|
||||||
while not too_old and len(to_delete) - 1 < number:
|
while not too_old and len(to_delete) < number:
|
||||||
message = None
|
message = None
|
||||||
async for message in channel.history(limit=limit, before=before, after=after):
|
async for message in channel.history(limit=limit, before=before, after=after):
|
||||||
if (
|
if (
|
||||||
(not number or len(to_delete) - 1 < number)
|
(not number or len(to_delete) < number)
|
||||||
and check(message)
|
and check(message)
|
||||||
and (ctx.message.created_at - message.created_at).days < 14
|
and (ctx.message.created_at - message.created_at).days < 14
|
||||||
and (delete_pinned or not message.pinned)
|
and (delete_pinned or not message.pinned)
|
||||||
@@ -96,12 +96,10 @@ class Cleanup:
|
|||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def cleanup(self, ctx: commands.Context):
|
async def cleanup(self, ctx: commands.Context):
|
||||||
"""Deletes messages."""
|
"""Deletes messages."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def text(
|
async def text(
|
||||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
@@ -113,6 +111,10 @@ class Cleanup:
|
|||||||
Remember to use double quotes."""
|
Remember to use double quotes."""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ class Cleanup:
|
|||||||
delete_pinned=delete_pinned,
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages " " containing '{}' in channel {}.".format(
|
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
||||||
author.name, author.id, len(to_delete), text, channel.id
|
author.name, author.id, len(to_delete), text, channel.id
|
||||||
)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
@@ -151,7 +153,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def user(
|
async def user(
|
||||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
@@ -160,6 +161,10 @@ class Cleanup:
|
|||||||
Examples:
|
Examples:
|
||||||
cleanup user @\u200bTwentysix 2
|
cleanup user @\u200bTwentysix 2
|
||||||
cleanup user Red 6"""
|
cleanup user Red 6"""
|
||||||
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
|
|
||||||
member = None
|
member = None
|
||||||
try:
|
try:
|
||||||
@@ -172,7 +177,6 @@ class Cleanup:
|
|||||||
else:
|
else:
|
||||||
_id = member.id
|
_id = member.id
|
||||||
|
|
||||||
channel = ctx.channel
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -213,7 +217,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||||
"""Deletes all messages after specified message.
|
"""Deletes all messages after specified message.
|
||||||
|
|
||||||
@@ -225,11 +228,14 @@ class Cleanup:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
if not is_bot:
|
if not is_bot:
|
||||||
await ctx.send(_("This command can only be used on bots with " "bot accounts."))
|
await ctx.send(_("This command can only be used on bots with bot accounts."))
|
||||||
return
|
return
|
||||||
|
|
||||||
after = await channel.get_message(message_id)
|
after = await channel.get_message(message_id)
|
||||||
@@ -242,7 +248,7 @@ class Cleanup:
|
|||||||
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
||||||
)
|
)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}." "".format(
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
author.name, author.id, len(to_delete), channel.name
|
author.name, author.id, len(to_delete), channel.name
|
||||||
)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
@@ -251,7 +257,6 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Deletes last X messages.
|
"""Deletes last X messages.
|
||||||
|
|
||||||
@@ -259,6 +264,9 @@ class Cleanup:
|
|||||||
cleanup messages 26"""
|
cleanup messages 26"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
@@ -273,7 +281,7 @@ class Cleanup:
|
|||||||
)
|
)
|
||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}." "".format(
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
author.name, author.id, number, channel.name
|
author.name, author.id, number, channel.name
|
||||||
)
|
)
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
@@ -285,11 +293,13 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command(name="bot")
|
@cleanup.command(name="bot")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_messages=True)
|
|
||||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Cleans up command messages and messages from the bot."""
|
"""Cleans up command messages and messages from the bot."""
|
||||||
|
|
||||||
channel = ctx.message.channel
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
await ctx.send("I need the Manage Messages permission to do this.")
|
||||||
|
return
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
is_bot = self.bot.user.bot
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
@@ -413,7 +423,7 @@ class Cleanup:
|
|||||||
if author == self.bot.user:
|
if author == self.bot.user:
|
||||||
to_delete.append(ctx.message)
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
if channel.name:
|
if ctx.guild:
|
||||||
channel_name = "channel " + channel.name
|
channel_name = "channel " + channel.name
|
||||||
else:
|
else:
|
||||||
channel_name = str(channel)
|
channel_name = str(channel)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class AlreadyExists(CCError):
|
|||||||
|
|
||||||
|
|
||||||
class CommandObj:
|
class CommandObj:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
config = kwargs.get("config")
|
config = kwargs.get("config")
|
||||||
self.bot = kwargs.get("bot")
|
self.bot = kwargs.get("bot")
|
||||||
@@ -43,7 +42,7 @@ class CommandObj:
|
|||||||
intro = _(
|
intro = _(
|
||||||
"Welcome to the interactive random {} maker!\n"
|
"Welcome to the interactive random {} maker!\n"
|
||||||
"Every message you send will be added as one of the random "
|
"Every message you send will be added as one of the random "
|
||||||
"response to choose from once this {} is "
|
"responses to choose from once this {} is "
|
||||||
"triggered. To exit this interactive menu, type `{}`"
|
"triggered. To exit this interactive menu, type `{}`"
|
||||||
).format("customcommand", "customcommand", "exit()")
|
).format("customcommand", "customcommand", "exit()")
|
||||||
await ctx.send(intro)
|
await ctx.send(intro)
|
||||||
@@ -75,7 +74,7 @@ class CommandObj:
|
|||||||
return ccinfo["response"]
|
return ccinfo["response"]
|
||||||
|
|
||||||
async def create(self, ctx: commands.Context, command: str, response):
|
async def create(self, ctx: commands.Context, command: str, response):
|
||||||
"""Create a customcommand"""
|
"""Create a custom command"""
|
||||||
# Check if this command is already registered as a customcommand
|
# Check if this command is already registered as a customcommand
|
||||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||||
raise AlreadyExists()
|
raise AlreadyExists()
|
||||||
@@ -132,6 +131,7 @@ class CommandObj:
|
|||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class CustomCommands:
|
class CustomCommands:
|
||||||
"""Custom commands
|
"""Custom commands
|
||||||
|
|
||||||
Creates commands used to display text"""
|
Creates commands used to display text"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
@@ -141,12 +141,11 @@ class CustomCommands:
|
|||||||
self.config.register_guild(commands={})
|
self.config.register_guild(commands={})
|
||||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||||
|
|
||||||
@commands.group(aliases=["cc"], no_pm=True)
|
@commands.group(aliases=["cc"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def customcom(self, ctx: commands.Context):
|
async def customcom(self, ctx: commands.Context):
|
||||||
"""Custom commands management"""
|
"""Custom commands management"""
|
||||||
if not ctx.invoked_subcommand:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@customcom.group(name="add")
|
@customcom.group(name="add")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
@@ -166,14 +165,14 @@ class CustomCommands:
|
|||||||
|
|
||||||
{server} message.guild
|
{server} message.guild
|
||||||
"""
|
"""
|
||||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group):
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@cc_add.command(name="random")
|
@cc_add.command(name="random")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_random(self, ctx: commands.Context, command: str):
|
async def cc_add_random(self, ctx: commands.Context, command: str):
|
||||||
"""
|
"""
|
||||||
Create a CC where it will randomly choose a response!
|
Create a CC where it will randomly choose a response!
|
||||||
|
|
||||||
Note: This is interactive
|
Note: This is interactive
|
||||||
"""
|
"""
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
@@ -185,7 +184,7 @@ class CustomCommands:
|
|||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This command already exists. Use " "`{}` to edit it.").format(
|
_("This command already exists. Use `{}` to edit it.").format(
|
||||||
"{}customcom edit".format(ctx.prefix)
|
"{}customcom edit".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -196,6 +195,7 @@ class CustomCommands:
|
|||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_simple(self, ctx, command: str, *, text):
|
async def cc_add_simple(self, ctx, command: str, *, text):
|
||||||
"""Adds a simple custom command
|
"""Adds a simple custom command
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom add simple yourcommand Text you want
|
[p]customcom add simple yourcommand Text you want
|
||||||
"""
|
"""
|
||||||
@@ -209,7 +209,7 @@ class CustomCommands:
|
|||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This command already exists. Use " "`{}` to edit it.").format(
|
_("This command already exists. Use `{}` to edit it.").format(
|
||||||
"{}customcom edit".format(ctx.prefix)
|
"{}customcom edit".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -218,6 +218,7 @@ class CustomCommands:
|
|||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_edit(self, ctx, command: str, *, text=None):
|
async def cc_edit(self, ctx, command: str, *, text=None):
|
||||||
"""Edits a custom command
|
"""Edits a custom command
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom edit yourcommand Text you want
|
[p]customcom edit yourcommand Text you want
|
||||||
"""
|
"""
|
||||||
@@ -229,7 +230,7 @@ class CustomCommands:
|
|||||||
await ctx.send(_("Custom command successfully edited."))
|
await ctx.send(_("Custom command successfully edited."))
|
||||||
except NotFound:
|
except NotFound:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("That command doesn't exist. Use " "`{}` to add it.").format(
|
_("That command doesn't exist. Use `{}` to add it.").format(
|
||||||
"{}customcom add".format(ctx.prefix)
|
"{}customcom add".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -119,11 +119,10 @@ class SpecResolver(object):
|
|||||||
def past_nicknames_conv_spec(self, data: dict):
|
def past_nicknames_conv_spec(self, data: dict):
|
||||||
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
||||||
ret = {}
|
ret = {}
|
||||||
for k, v in flatscoped.items():
|
for config_identifiers, v2data in flatscoped.items():
|
||||||
outerkey, innerkey = (*k[:-1],), (k[-1],)
|
if config_identifiers not in ret:
|
||||||
if outerkey not in ret:
|
ret[config_identifiers] = {}
|
||||||
ret[outerkey] = {}
|
ret[config_identifiers].update({("past_nicks",): v2data})
|
||||||
ret[outerkey].update({innerkey: v})
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def customcom_conv_spec(self, data: dict):
|
def customcom_conv_spec(self, data: dict):
|
||||||
@@ -144,18 +143,28 @@ class SpecResolver(object):
|
|||||||
ret[outerkey].update({innerkey: ccinfo})
|
ret[outerkey].update({innerkey: ccinfo})
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def convert(self, bot: Red, prettyname: str):
|
def get_config_object(self, bot, cogname, attr, _id):
|
||||||
if prettyname not in self.available:
|
|
||||||
raise NotImplementedError("No Conversion Specs for this")
|
|
||||||
|
|
||||||
info = self.available_core_conversions[prettyname]
|
|
||||||
filepath, converter = info["file"], info["converter"]
|
|
||||||
(cogname, attr, _id) = info["cfg"]
|
|
||||||
try:
|
try:
|
||||||
config = getattr(bot.get_cog(cogname), attr)
|
config = getattr(bot.get_cog(cogname), attr)
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
config = Config.get_conf(None, _id, cog_name=cogname)
|
config = Config.get_conf(None, _id, cog_name=cogname)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def get_conversion_info(self, prettyname: str):
|
||||||
|
info = self.available_core_conversions[prettyname]
|
||||||
|
filepath, converter = info["file"], info["converter"]
|
||||||
|
(cogname, attr, _id) = info["cfg"]
|
||||||
|
return filepath, converter, cogname, attr, _id
|
||||||
|
|
||||||
|
async def convert(self, bot: Red, prettyname: str, config=None):
|
||||||
|
if prettyname not in self.available:
|
||||||
|
raise NotImplementedError("No Conversion Specs for this")
|
||||||
|
|
||||||
|
filepath, converter, cogname, attr, _id = self.get_conversion_info(prettyname)
|
||||||
|
if config is None:
|
||||||
|
config = self.get_config_object(bot, cogname, attr, _id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
items = converter(dc.json_load(filepath))
|
items = converter(dc.json_load(filepath))
|
||||||
await dc(config).dict_import(items)
|
await dc(config).dict_import(items)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class DataConverter:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
while resolver.available:
|
while resolver.available:
|
||||||
menu = _("Please select a set of data to import by number" ", or 'exit' to exit")
|
menu = _("Please select a set of data to import by number, or 'exit' to exit")
|
||||||
for index, entry in enumerate(resolver.available, 1):
|
for index, entry in enumerate(resolver.available, 1):
|
||||||
menu += "\n{}. {}".format(index, entry)
|
menu += "\n{}. {}".format(index, entry)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
__all__ = ["install_agreement"]
|
__all__ = ["do_install_agreement"]
|
||||||
|
|
||||||
REPO_INSTALL_MSG = (
|
REPO_INSTALL_MSG = (
|
||||||
"You're about to add a 3rd party repository. The creator of Red"
|
"You're about to add a 3rd party repository. The creator of Red"
|
||||||
@@ -16,33 +16,21 @@ REPO_INSTALL_MSG = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def install_agreement():
|
async def do_install_agreement(ctx: commands.Context):
|
||||||
|
downloader = ctx.cog
|
||||||
async def pred(ctx: commands.Context):
|
if downloader is None or downloader.already_agreed:
|
||||||
downloader = ctx.command.instance
|
|
||||||
if downloader is None:
|
|
||||||
return True
|
|
||||||
elif downloader.already_agreed:
|
|
||||||
return True
|
|
||||||
elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def does_agree(msg: discord.Message):
|
|
||||||
return (
|
|
||||||
ctx.author == msg.author
|
|
||||||
and ctx.channel == msg.channel
|
|
||||||
and msg.content == "I agree"
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(REPO_INSTALL_MSG)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await ctx.send("Your response has timed out, please try again.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
downloader.already_agreed = True
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
def does_agree(msg: discord.Message):
|
||||||
|
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
|
||||||
|
|
||||||
|
await ctx.send(REPO_INSTALL_MSG)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send("Your response has timed out, please try again.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
downloader.already_agreed = True
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from .repo_manager import RepoManager
|
|
||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
|
|
||||||
|
|
||||||
class InstalledCog(commands.Converter):
|
class InstalledCog(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
downloader = ctx.bot.get_cog("Downloader")
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from redbot.core.utils.chat_formatting import box, pagify
|
|||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from .checks import install_agreement
|
from .checks import do_install_agreement
|
||||||
from .converters import InstalledCog
|
from .converters import InstalledCog
|
||||||
from .errors import CloningError, ExistingGitRepo
|
from .errors import CloningError, ExistingGitRepo
|
||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
@@ -27,7 +27,6 @@ _ = Translator("Downloader", __file__)
|
|||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Downloader:
|
class Downloader:
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@@ -211,11 +210,9 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
Command group for managing Downloader repos.
|
Command group for managing Downloader repos.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@repo.command(name="add")
|
@repo.command(name="add")
|
||||||
@install_agreement()
|
|
||||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||||
"""
|
"""
|
||||||
Add a new repo to Downloader.
|
Add a new repo to Downloader.
|
||||||
@@ -223,6 +220,9 @@ class Downloader:
|
|||||||
Name can only contain characters A-z, numbers and underscore
|
Name can only contain characters A-z, numbers and underscore
|
||||||
Branch will default to master if not specified
|
Branch will default to master if not specified
|
||||||
"""
|
"""
|
||||||
|
agreed = await do_install_agreement(ctx)
|
||||||
|
if not agreed:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||||
@@ -234,7 +234,7 @@ class Downloader:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||||
if repo.install_msg is not None:
|
if repo.install_msg is not None:
|
||||||
await ctx.send(repo.install_msg)
|
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@repo.command(name="delete")
|
@repo.command(name="delete")
|
||||||
async def _repo_del(self, ctx, repo_name: Repo):
|
async def _repo_del(self, ctx, repo_name: Repo):
|
||||||
@@ -278,8 +278,7 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
Command group for managing installable Cogs.
|
Command group for managing installable Cogs.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@cog.command(name="install")
|
@cog.command(name="install")
|
||||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
||||||
@@ -289,7 +288,7 @@ class Downloader:
|
|||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
||||||
if cog is None:
|
if cog is None:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Error, there is no cog by the name of" " `{}` in the `{}` repo.").format(
|
_("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
|
||||||
cog_name, repo_name.name
|
cog_name, repo_name.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -306,7 +305,7 @@ class Downloader:
|
|||||||
|
|
||||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Failed to install the required libraries for" " `{}`: `{}`").format(
|
_("Failed to install the required libraries for `{}`: `{}`").format(
|
||||||
cog.name, cog.requirements
|
cog.name, cog.requirements
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -320,7 +319,7 @@ class Downloader:
|
|||||||
|
|
||||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||||
if cog.install_msg is not None:
|
if cog.install_msg is not None:
|
||||||
await ctx.send(cog.install_msg)
|
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@cog.command(name="uninstall")
|
@cog.command(name="uninstall")
|
||||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||||
@@ -381,11 +380,25 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
Lists all available cogs from a single repo.
|
Lists all available cogs from a single repo.
|
||||||
"""
|
"""
|
||||||
|
installed = await self.installed_cogs()
|
||||||
|
installed_str = ""
|
||||||
|
if installed:
|
||||||
|
installed_str = _("Installed Cogs:\n") + "\n".join(
|
||||||
|
[
|
||||||
|
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
||||||
|
for i in installed
|
||||||
|
if i.repo_name == repo_name.name
|
||||||
|
]
|
||||||
|
)
|
||||||
cogs = repo_name.available_cogs
|
cogs = repo_name.available_cogs
|
||||||
cogs = _("Available Cogs:\n") + "\n".join(
|
cogs = _("Available Cogs:\n") + "\n".join(
|
||||||
["+ {}: {}".format(c.name, c.short or "") for c in cogs]
|
[
|
||||||
|
"+ {}: {}".format(c.name, c.short or "")
|
||||||
|
for c in cogs
|
||||||
|
if not (c.hidden or c in installed)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
cogs = cogs + "\n\n" + installed_str
|
||||||
for page in pagify(cogs, ["\n"], shorten_by=16):
|
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
@@ -401,7 +414,9 @@ class Downloader:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
|
msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
|
||||||
|
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
|
||||||
|
)
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
async def is_installed(
|
async def is_installed(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DownloaderException(Exception):
|
|||||||
"""
|
"""
|
||||||
Base class for Downloader exceptions.
|
Base class for Downloader exceptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ class InvalidRepoName(DownloaderException):
|
|||||||
Throw when a repo name is invalid. Check
|
Throw when a repo name is invalid. Check
|
||||||
the message for a more detailed reason.
|
the message for a more detailed reason.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
|
|||||||
Thrown when trying to clone into a folder where a
|
Thrown when trying to clone into a folder where a
|
||||||
git repo already exists.
|
git repo already exists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ class MissingGitRepo(DownloaderException):
|
|||||||
Thrown when a git repo is expected to exist but
|
Thrown when a git repo is expected to exist but
|
||||||
does not.
|
does not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -54,6 +58,7 @@ class CloningError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when git clone returns a non zero exit code.
|
Thrown when git clone returns a non zero exit code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +67,7 @@ class CurrentHashError(GitException):
|
|||||||
Thrown when git returns a non zero exit code attempting
|
Thrown when git returns a non zero exit code attempting
|
||||||
to determine the current commit hash.
|
to determine the current commit hash.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +76,7 @@ class HardResetError(GitException):
|
|||||||
Thrown when there is an issue trying to execute a hard reset
|
Thrown when there is an issue trying to execute a hard reset
|
||||||
(usually prior to a repo update).
|
(usually prior to a repo update).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +84,7 @@ class UpdateError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when git pull returns a non zero error code.
|
Thrown when git pull returns a non zero error code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +92,7 @@ class GitDiffError(GitException):
|
|||||||
"""
|
"""
|
||||||
Thrown when a git diff fails.
|
Thrown when a git diff fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -91,4 +100,5 @@ class PipError(DownloaderException):
|
|||||||
"""
|
"""
|
||||||
Thrown when pip returns a non-zero return code.
|
Thrown when pip returns a non-zero return code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import distutils.dir_util
|
|||||||
import shutil
|
import shutil
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import MutableMapping, Any
|
from typing import MutableMapping, Any, TYPE_CHECKING
|
||||||
|
|
||||||
from redbot.core.utils import TYPE_CHECKING
|
|
||||||
from .log import log
|
from .log import log
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
|
|
||||||
@@ -76,6 +75,7 @@ class Installable(RepoJSONMixin):
|
|||||||
self.bot_version = (3, 0, 0)
|
self.bot_version = (3, 0, 0)
|
||||||
self.min_python_version = (3, 5, 1)
|
self.min_python_version = (3, 5, 1)
|
||||||
self.hidden = False
|
self.hidden = False
|
||||||
|
self.disabled = False
|
||||||
self.required_cogs = {} # Cog name -> repo URL
|
self.required_cogs = {} # Cog name -> repo URL
|
||||||
self.requirements = ()
|
self.requirements = ()
|
||||||
self.tags = ()
|
self.tags = ()
|
||||||
@@ -117,7 +117,7 @@ class Installable(RepoJSONMixin):
|
|||||||
try:
|
try:
|
||||||
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||||
except:
|
except:
|
||||||
log.exception("Error occurred when copying path:" " {}".format(self._location))
|
log.exception("Error occurred when copying path: {}".format(self._location))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -146,9 +146,7 @@ class Installable(RepoJSONMixin):
|
|||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
info = {}
|
info = {}
|
||||||
log.exception(
|
log.exception("Invalid JSON information file at path: {}".format(info_file_path))
|
||||||
"Invalid JSON information file at path:" " {}".format(info_file_path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._info = info
|
self._info = info
|
||||||
|
|
||||||
@@ -176,6 +174,12 @@ class Installable(RepoJSONMixin):
|
|||||||
hidden = False
|
hidden = False
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
|
||||||
|
try:
|
||||||
|
disabled = bool(info.get("disabled", False))
|
||||||
|
except ValueError:
|
||||||
|
disabled = False
|
||||||
|
self.disabled = disabled
|
||||||
|
|
||||||
self.required_cogs = info.get("required_cogs", {})
|
self.required_cogs = info.get("required_cogs", {})
|
||||||
|
|
||||||
self.requirements = info.get("requirements", ())
|
self.requirements = info.get("requirements", ())
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ import asyncio
|
|||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import shutil
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import run as sp_run, PIPE
|
from subprocess import run as sp_run, PIPE
|
||||||
from sys import executable
|
from sys import executable
|
||||||
from typing import Tuple, MutableMapping, Union
|
from typing import Tuple, MutableMapping, Union
|
||||||
|
|
||||||
from discord.ext import commands
|
from redbot.core import data_manager, commands
|
||||||
|
|
||||||
from redbot.core import Config
|
|
||||||
from redbot.core import data_manager
|
|
||||||
from redbot.core.utils import safe_delete
|
from redbot.core.utils import safe_delete
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .installable import Installable, InstallableType
|
from .installable import Installable, InstallableType
|
||||||
@@ -27,10 +23,8 @@ class Repo(RepoJSONMixin):
|
|||||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
GIT_PULL = "git -C {path} pull -q --ff-only"
|
||||||
GIT_DIFF_FILE_STATUS = (
|
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
||||||
"git -C {path} diff --no-commit-id --name-status" " {old_hash} {new_hash}"
|
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
|
||||||
)
|
|
||||||
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.." " {relative_file_path}"
|
|
||||||
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
||||||
|
|
||||||
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
||||||
@@ -98,7 +92,7 @@ class Repo(RepoJSONMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitDiffError("Git diff failed for repo at path:" " {}".format(self.folder_path))
|
raise GitDiffError("Git diff failed for repo at path: {}".format(self.folder_path))
|
||||||
|
|
||||||
stdout = p.stdout.strip().decode().split("\n")
|
stdout = p.stdout.strip().decode().split("\n")
|
||||||
|
|
||||||
@@ -222,7 +216,7 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitException(
|
raise GitException(
|
||||||
"Could not determine current branch" " at path: {}".format(self.folder_path)
|
"Could not determine current branch at path: {}".format(self.folder_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
@@ -472,7 +466,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(
|
return tuple(
|
||||||
[m for m in self.available_modules if m.type == InstallableType.COG and not m.hidden]
|
[m for m in self.available_modules if m.type == InstallableType.COG and not m.disabled]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -495,7 +489,6 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
|
|
||||||
class RepoManager:
|
class RepoManager:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self._repos = {}
|
self._repos = {}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import discord
|
|||||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||||
from redbot.core import Config, bank, commands
|
from redbot.core import Config, bank, commands
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
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
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
@@ -74,7 +75,6 @@ SLOT_PAYOUTS_MSG = _(
|
|||||||
|
|
||||||
|
|
||||||
def guild_only_check():
|
def guild_only_check():
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
return True
|
return True
|
||||||
@@ -87,7 +87,6 @@ def guild_only_check():
|
|||||||
|
|
||||||
|
|
||||||
class SetParser:
|
class SetParser:
|
||||||
|
|
||||||
def __init__(self, argument):
|
def __init__(self, argument):
|
||||||
allowed = ("+", "-")
|
allowed = ("+", "-")
|
||||||
self.sum = int(argument)
|
self.sum = int(argument)
|
||||||
@@ -139,11 +138,11 @@ class Economy:
|
|||||||
self.config.register_role(**self.default_role_settings)
|
self.config.register_role(**self.default_role_settings)
|
||||||
self.slot_register = defaultdict(dict)
|
self.slot_register = defaultdict(dict)
|
||||||
|
|
||||||
|
@guild_only_check()
|
||||||
@commands.group(name="bank")
|
@commands.group(name="bank")
|
||||||
async def _bank(self, ctx: commands.Context):
|
async def _bank(self, ctx: commands.Context):
|
||||||
"""Bank operations"""
|
"""Bank operations"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||||
@@ -212,7 +211,6 @@ class Economy:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
@guild_only_check()
|
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def reset(self, ctx, confirmation: bool = False):
|
async def reset(self, ctx, confirmation: bool = False):
|
||||||
"""Deletes bank accounts"""
|
"""Deletes bank accounts"""
|
||||||
@@ -228,13 +226,13 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
await bank.wipe_bank()
|
await bank.wipe_bank()
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("All bank accounts for {} have been " "deleted.").format(
|
_("All bank accounts for {} have been deleted.").format(
|
||||||
self.bot.user.name if await bank.is_global() else "this server"
|
self.bot.user.name if await bank.is_global() else "this server"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
|
@commands.command()
|
||||||
async def payday(self, ctx: commands.Context):
|
async def payday(self, ctx: commands.Context):
|
||||||
"""Get some free currency"""
|
"""Get some free currency"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
@@ -254,7 +252,7 @@ class Economy:
|
|||||||
_(
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||||
"You currently have {3} {1}.\n\n"
|
"You currently have {3} {1}.\n\n"
|
||||||
"You are currently #{4} on the leaderboard!"
|
"You are currently #{4} on the global leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author,
|
author,
|
||||||
credits_name,
|
credits_name,
|
||||||
@@ -267,7 +265,7 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to" " wait {}.").format(
|
_("{} Too soon. For your next payday you have to wait {}.").format(
|
||||||
author.mention, dtime
|
author.mention, dtime
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -301,7 +299,7 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to" " wait {}.").format(
|
_("{} Too soon. For your next payday you have to wait {}.").format(
|
||||||
author.mention, dtime
|
author.mention, dtime
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -312,8 +310,8 @@ class Economy:
|
|||||||
"""Prints out the leaderboard
|
"""Prints out the leaderboard
|
||||||
|
|
||||||
Defaults to top 10"""
|
Defaults to top 10"""
|
||||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
author = ctx.author
|
||||||
if top < 1:
|
if top < 1:
|
||||||
top = 10
|
top = 10
|
||||||
if (
|
if (
|
||||||
@@ -323,25 +321,25 @@ class Economy:
|
|||||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||||
if len(bank_sorted) < top:
|
if len(bank_sorted) < top:
|
||||||
top = len(bank_sorted)
|
top = len(bank_sorted)
|
||||||
highscore = ""
|
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
||||||
for pos, acc in enumerate(bank_sorted, 1):
|
highscores = [
|
||||||
pos = pos
|
(
|
||||||
poswidth = 2
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||||
name = acc[1]["name"]
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
namewidth = 35
|
|
||||||
balance = acc[1]["balance"]
|
|
||||||
balwidth = 2
|
|
||||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
|
||||||
pos=pos,
|
|
||||||
poswidth=poswidth,
|
|
||||||
name=name,
|
|
||||||
namewidth=namewidth,
|
|
||||||
balance=balance,
|
|
||||||
balwidth=balwidth,
|
|
||||||
)
|
)
|
||||||
if highscore != "":
|
if acc[0] != author.id
|
||||||
for page in pagify(highscore, shorten_by=12):
|
else (
|
||||||
await ctx.send(box(page, lang="py"))
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
||||||
|
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||||
|
)
|
||||||
|
for pos, acc in enumerate(bank_sorted, 1)
|
||||||
|
]
|
||||||
|
if highscores:
|
||||||
|
pages = [
|
||||||
|
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
||||||
|
for x in range(0, len(highscores), 10)
|
||||||
|
]
|
||||||
|
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no accounts in the bank."))
|
await ctx.send(_("There are no accounts in the bank."))
|
||||||
|
|
||||||
@@ -427,7 +425,7 @@ class Economy:
|
|||||||
now = then - bid + pay
|
now = then - bid + pay
|
||||||
await bank.set_balance(author, now)
|
await bank.set_balance(author, now)
|
||||||
await channel.send(
|
await channel.send(
|
||||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!" "").format(
|
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format(
|
||||||
slot, author.mention, payout["phrase"], bid, then, now
|
slot, author.mention, payout["phrase"], bid, then, now
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -436,7 +434,7 @@ class Economy:
|
|||||||
await bank.withdraw_credits(author, bid)
|
await bank.withdraw_credits(author, bid)
|
||||||
now = then - bid
|
now = then - bid
|
||||||
await channel.send(
|
await channel.send(
|
||||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!" "").format(
|
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format(
|
||||||
slot, author.mention, bid, then, now
|
slot, author.mention, bid, then, now
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -448,7 +446,6 @@ class Economy:
|
|||||||
"""Changes economy module settings"""
|
"""Changes economy module settings"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
slot_min = await self.config.SLOT_MIN()
|
slot_min = await self.config.SLOT_MIN()
|
||||||
slot_max = await self.config.SLOT_MAX()
|
slot_max = await self.config.SLOT_MAX()
|
||||||
@@ -497,7 +494,7 @@ class Economy:
|
|||||||
"""Maximum slot machine bid"""
|
"""Maximum slot machine bid"""
|
||||||
slot_min = await self.config.SLOT_MIN()
|
slot_min = await self.config.SLOT_MIN()
|
||||||
if bid < 1 or bid < slot_min:
|
if bid < 1 or bid < slot_min:
|
||||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater" " than slotmin."))
|
await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
@@ -526,9 +523,7 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Value modified. At least {} seconds must pass " "between each payday.").format(
|
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
|
||||||
seconds
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
@@ -543,7 +538,7 @@ class Economy:
|
|||||||
await self.config.PAYDAY_CREDITS.set(creds)
|
await self.config.PAYDAY_CREDITS.set(creds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(_("Every payday will now give {} {}." "").format(creds, credits_name))
|
await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||||
@@ -555,7 +550,7 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Every payday will now give {} {} to people with the role {}." "").format(
|
_("Every payday will now give {} {} to people with the role {}.").format(
|
||||||
creds, credits_name, role.name
|
creds, credits_name, role.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -569,7 +564,7 @@ class Economy:
|
|||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
await bank.set_default_balance(creds, guild)
|
await bank.set_default_balance(creds, guild)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Registering an account will now give {} {}." "").format(creds, credits_name)
|
_("Registering an account will now give {} {}.").format(creds, credits_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
# What would I ever do without stackoverflow?
|
# What would I ever do without stackoverflow?
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ class Filter:
|
|||||||
Using this command with no subcommands will send
|
Using this command with no subcommands will send
|
||||||
the list of the server's filtered words."""
|
the list of the server's filtered words."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
word_list = await self.settings.guild(server).filter()
|
word_list = await self.settings.guild(server).filter()
|
||||||
@@ -124,36 +123,35 @@ class Filter:
|
|||||||
|
|
||||||
@_filter.command(name="names")
|
@_filter.command(name="names")
|
||||||
async def filter_names(self, ctx: commands.Context):
|
async def filter_names(self, ctx: commands.Context):
|
||||||
"""
|
"""Toggles whether or not to check names and nicknames against the filter
|
||||||
Toggles whether or not to check names and nicknames against the filter
|
|
||||||
This is disabled by default
|
This is disabled by default
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
current_setting = await self.settings.guild(guild).filter_names()
|
current_setting = await self.settings.guild(guild).filter_names()
|
||||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await ctx.send(
|
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
|
||||||
_("Names and nicknames will no longer be " "checked against the filter")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Names and nicknames will now be checked against " "the filter"))
|
await ctx.send(_("Names and nicknames will now be checked against the filter."))
|
||||||
|
|
||||||
@_filter.command(name="defaultname")
|
@_filter.command(name="defaultname")
|
||||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||||
"""
|
"""Sets the default name to use if filtering names is enabled
|
||||||
Sets the default name to use if filtering names is enabled
|
|
||||||
Note that this has no effect if filtering names is disabled
|
Note that this has no effect if filtering names is disabled
|
||||||
|
|
||||||
The default name used is John Doe
|
The default name used is John Doe
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await self.settings.guild(guild).filter_default_name.set(name)
|
await self.settings.guild(guild).filter_default_name.set(name)
|
||||||
await ctx.send(_("The name to use on filtered names has been set"))
|
await ctx.send(_("The name to use on filtered names has been set."))
|
||||||
|
|
||||||
@_filter.command(name="ban")
|
@_filter.command(name="ban")
|
||||||
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||||
"""
|
"""Autobans if the specified number of messages are filtered in the timeframe
|
||||||
Sets up an autoban if the specified number of messages are
|
|
||||||
filtered in the specified amount of time (in seconds)
|
The timeframe is represented by seconds.
|
||||||
"""
|
"""
|
||||||
if (count <= 0) != (timeframe <= 0):
|
if (count <= 0) != (timeframe <= 0):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@@ -223,7 +221,7 @@ class Filter:
|
|||||||
user_count >= filter_count
|
user_count >= filter_count
|
||||||
and message.created_at.timestamp() < next_reset_time
|
and message.created_at.timestamp() < next_reset_time
|
||||||
):
|
):
|
||||||
reason = "Autoban (too many filtered messages)"
|
reason = "Autoban (too many filtered messages.)"
|
||||||
try:
|
try:
|
||||||
await server.ban(author, reason=reason)
|
await server.ban(author, reason=reason)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class RPS(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class RPSParser:
|
class RPSParser:
|
||||||
|
|
||||||
def __init__(self, argument):
|
def __init__(self, argument):
|
||||||
argument = argument.lower()
|
argument = argument.lower()
|
||||||
if argument == "rock":
|
if argument == "rock":
|
||||||
@@ -98,7 +97,7 @@ class General:
|
|||||||
msg = ""
|
msg = ""
|
||||||
if user.id == ctx.bot.user.id:
|
if user.id == ctx.bot.user.id:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
msg = _("Nice try. You think this is funny?\n" "How about *this* instead:\n\n")
|
msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n")
|
||||||
char = "abcdefghijklmnopqrstuvwxyz"
|
char = "abcdefghijklmnopqrstuvwxyz"
|
||||||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||||||
table = str.maketrans(char, tran)
|
table = str.maketrans(char, tran)
|
||||||
@@ -192,18 +191,12 @@ class General:
|
|||||||
async def serverinfo(self, ctx):
|
async def serverinfo(self, ctx):
|
||||||
"""Shows server's informations"""
|
"""Shows server's informations"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
online = len(
|
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||||
[
|
|
||||||
m.status
|
|
||||||
for m in guild.members
|
|
||||||
if m.status == discord.Status.online or m.status == discord.Status.idle
|
|
||||||
]
|
|
||||||
)
|
|
||||||
total_users = len(guild.members)
|
total_users = len(guild.members)
|
||||||
text_channels = len(guild.text_channels)
|
text_channels = len(guild.text_channels)
|
||||||
voice_channels = len(guild.voice_channels)
|
voice_channels = len(guild.voice_channels)
|
||||||
passed = (ctx.message.created_at - guild.created_at).days
|
passed = (ctx.message.created_at - guild.created_at).days
|
||||||
created_at = _("Since {}. That's over {} days ago!" "").format(
|
created_at = _("Since {}. That's over {} days ago!").format(
|
||||||
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -228,7 +221,7 @@ class General:
|
|||||||
try:
|
try:
|
||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(_("I need the `Embed links` permission " "to send this."))
|
await ctx.send(_("I need the `Embed links` permission to send this."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
||||||
@@ -265,7 +258,7 @@ class General:
|
|||||||
definition = item_list[pos]["definition"]
|
definition = item_list[pos]["definition"]
|
||||||
example = item_list[pos]["example"]
|
example = item_list[pos]["example"]
|
||||||
defs = len(item_list)
|
defs = len(item_list)
|
||||||
msg = "**Definition #{} out of {}:\n**{}\n\n" "**Example:\n**{}".format(
|
msg = "**Definition #{} out of {}:\n**{}\n\n**Example:\n**{}".format(
|
||||||
pos + 1, defs, definition, example
|
pos + 1, defs, definition, example
|
||||||
)
|
)
|
||||||
msg = pagify(msg, ["\n"])
|
msg = pagify(msg, ["\n"])
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
|||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Image:
|
class Image:
|
||||||
"""Image related commands."""
|
"""Image related commands."""
|
||||||
|
|
||||||
default_global = {"imgur_client_id": None}
|
default_global = {"imgur_client_id": None}
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
@@ -31,8 +32,7 @@ class Image:
|
|||||||
|
|
||||||
Make sure to set the client ID using
|
Make sure to set the client ID using
|
||||||
[p]imgurcreds"""
|
[p]imgurcreds"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@_imgur.command(name="search")
|
@_imgur.command(name="search")
|
||||||
async def imgur_search(self, ctx, *, term: str):
|
async def imgur_search(self, ctx, *, term: str):
|
||||||
@@ -42,7 +42,7 @@ class Image:
|
|||||||
imgur_client_id = await self.settings.imgur_client_id()
|
imgur_client_id = await self.settings.imgur_client_id()
|
||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}").format(
|
_("A client ID has not been set! Please set one with {}.").format(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)
|
"`{}imgurcreds`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -54,7 +54,7 @@ class Image:
|
|||||||
if data["success"]:
|
if data["success"]:
|
||||||
results = data["data"]
|
results = data["data"]
|
||||||
if not results:
|
if not results:
|
||||||
await ctx.send(_("Your search returned no results"))
|
await ctx.send(_("Your search returned no results."))
|
||||||
return
|
return
|
||||||
shuffle(results)
|
shuffle(results)
|
||||||
msg = _("Search results...\n")
|
msg = _("Search results...\n")
|
||||||
@@ -63,7 +63,7 @@ class Image:
|
|||||||
msg += "\n"
|
msg += "\n"
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||||
|
|
||||||
@_imgur.command(name="subreddit")
|
@_imgur.command(name="subreddit")
|
||||||
async def imgur_subreddit(
|
async def imgur_subreddit(
|
||||||
@@ -91,7 +91,7 @@ class Image:
|
|||||||
imgur_client_id = await self.settings.imgur_client_id()
|
imgur_client_id = await self.settings.imgur_client_id()
|
||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}").format(
|
_("A client ID has not been set! Please set one with {}.").format(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)
|
"`{}imgurcreds`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -116,12 +116,13 @@ class Image:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||||
"""Sets the imgur client id
|
"""Sets the imgur client id
|
||||||
|
|
||||||
You will need an account on Imgur to get this
|
You will need an account on Imgur to get this
|
||||||
|
|
||||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
||||||
@@ -130,7 +131,7 @@ class Image:
|
|||||||
set the authorization callback url to 'https://localhost'
|
set the authorization callback url to 'https://localhost'
|
||||||
leave the app website blank, enter a valid email address, and
|
leave the app website blank, enter a valid email address, and
|
||||||
enter a description. Check the box for the captcha, then click Next.
|
enter a description. Check the box for the captcha, then click Next.
|
||||||
Your client ID will be on the page that loads"""
|
Your client ID will be on the page that loads."""
|
||||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||||
await ctx.send(_("Set the imgur client id!"))
|
await ctx.send(_("Set the imgur client id!"))
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ class Image:
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}" "".format(
|
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
|
||||||
GIPHY_API_KEY, keywords
|
GIPHY_API_KEY, keywords
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class Image:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Error contacting the API"))
|
await ctx.send(_("Error contacting the API."))
|
||||||
|
|
||||||
@commands.command(pass_context=True, no_pm=True)
|
@commands.command(pass_context=True, no_pm=True)
|
||||||
async def gifr(self, ctx, *keywords):
|
async def gifr(self, ctx, *keywords):
|
||||||
@@ -166,7 +167,7 @@ class Image:
|
|||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}" "".format(
|
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
|
||||||
GIPHY_API_KEY, keywords
|
GIPHY_API_KEY, keywords
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,4 +179,4 @@ class Image:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Error contacting the API"))
|
await ctx.send(_("Error contacting the API."))
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
|
|
||||||
def mod_or_voice_permissions(**perms):
|
def mod_or_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -31,7 +30,6 @@ def mod_or_voice_permissions(**perms):
|
|||||||
|
|
||||||
|
|
||||||
def admin_or_voice_permissions(**perms):
|
def admin_or_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -54,7 +52,6 @@ def admin_or_voice_permissions(**perms):
|
|||||||
|
|
||||||
|
|
||||||
def bot_has_voice_permissions(**perms):
|
def bot_has_voice_permissions(**perms):
|
||||||
|
|
||||||
async def pred(ctx: commands.Context):
|
async def pred(ctx: commands.Context):
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
|
|||||||
@@ -168,8 +168,6 @@ class Mod:
|
|||||||
"""Manages server administration settings."""
|
"""Manages server administration settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
# Display current settings
|
# Display current settings
|
||||||
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
||||||
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
||||||
@@ -199,12 +197,12 @@ class Mod:
|
|||||||
if not toggled:
|
if not toggled:
|
||||||
await self.settings.guild(guild).respect_hierarchy.set(True)
|
await self.settings.guild(guild).respect_hierarchy.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Role hierarchy will be checked when " "moderation commands are issued.")
|
_("Role hierarchy will be checked when moderation commands are issued.")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.settings.guild(guild).respect_hierarchy.set(False)
|
await self.settings.guild(guild).respect_hierarchy.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Role hierarchy will be ignored when " "moderation commands are issued.")
|
_("Role hierarchy will be ignored when moderation commands are issued.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@modset.command()
|
@modset.command()
|
||||||
@@ -241,7 +239,7 @@ class Mod:
|
|||||||
cur_setting = await self.settings.guild(guild).delete_repeats()
|
cur_setting = await self.settings.guild(guild).delete_repeats()
|
||||||
if not cur_setting:
|
if not cur_setting:
|
||||||
await self.settings.guild(guild).delete_repeats.set(True)
|
await self.settings.guild(guild).delete_repeats.set(True)
|
||||||
await ctx.send(_("Messages repeated up to 3 times will " "be deleted."))
|
await ctx.send(_("Messages repeated up to 3 times will be deleted."))
|
||||||
else:
|
else:
|
||||||
await self.settings.guild(guild).delete_repeats.set(False)
|
await self.settings.guild(guild).delete_repeats.set(False)
|
||||||
await ctx.send(_("Repeated messages will be ignored."))
|
await ctx.send(_("Repeated messages will be ignored."))
|
||||||
@@ -304,7 +302,7 @@ class Mod:
|
|||||||
|
|
||||||
if author == user:
|
if author == user:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}")
|
_("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||||
@@ -316,6 +314,9 @@ class Mod:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
|
||||||
|
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
|
||||||
|
return
|
||||||
audit_reason = get_audit_reason(author, reason)
|
audit_reason = get_audit_reason(author, reason)
|
||||||
try:
|
try:
|
||||||
await guild.kick(user, reason=audit_reason)
|
await guild.kick(user, reason=audit_reason)
|
||||||
@@ -357,7 +358,7 @@ class Mod:
|
|||||||
|
|
||||||
if author == user:
|
if author == user:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}")
|
_("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||||
@@ -369,6 +370,9 @@ class Mod:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
|
||||||
|
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
|
||||||
|
return
|
||||||
|
|
||||||
if days:
|
if days:
|
||||||
if days.isdigit():
|
if days.isdigit():
|
||||||
@@ -451,15 +455,15 @@ class Mod:
|
|||||||
self.ban_queue.append(queue_entry)
|
self.ban_queue.append(queue_entry)
|
||||||
try:
|
try:
|
||||||
await guild.ban(user, reason=audit_reason)
|
await guild.ban(user, reason=audit_reason)
|
||||||
log.info("{}({}) hackbanned {}" "".format(author.name, author.id, user_id))
|
log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
self.ban_queue.remove(queue_entry)
|
self.ban_queue.remove(queue_entry)
|
||||||
await ctx.send(_("User not found. Have you provided the " "correct user ID?"))
|
await ctx.send(_("User not found. Have you provided the correct user ID?"))
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
self.ban_queue.remove(queue_entry)
|
self.ban_queue.remove(queue_entry)
|
||||||
await ctx.send(_("I lack the permissions to do this."))
|
await ctx.send(_("I lack the permissions to do this."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Done. The user will not be able to join this " "server."))
|
await ctx.send(_("Done. The user will not be able to join this server."))
|
||||||
|
|
||||||
user_info = await self.bot.get_user_info(user_id)
|
user_info = await self.bot.get_user_info(user_id)
|
||||||
try:
|
try:
|
||||||
@@ -547,7 +551,7 @@ class Mod:
|
|||||||
|
|
||||||
if author == user:
|
if author == user:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}")
|
_("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||||
@@ -624,7 +628,6 @@ class Mod:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
||||||
"""Unbans the target user.
|
"""Unbans the target user.
|
||||||
|
|
||||||
@@ -632,13 +635,17 @@ class Mod:
|
|||||||
1. Copy it from the mod log case (if one was created), or
|
1. Copy it from the mod log case (if one was created), or
|
||||||
2. enable developer mode, go to Bans in this server's settings, right-
|
2. enable developer mode, go to Bans in this server's settings, right-
|
||||||
click the user and select 'Copy ID'."""
|
click the user and select 'Copy ID'."""
|
||||||
|
channel = ctx.channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).ban_members:
|
||||||
|
await ctx.send("I need the Ban Members permission to do this.")
|
||||||
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
user = await self.bot.get_user_info(user_id)
|
user = await self.bot.get_user_info(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
await ctx.send(_("Couldn't find a user with that ID!"))
|
await ctx.send(_("Couldn't find a user with that ID!"))
|
||||||
return
|
return
|
||||||
reason = get_audit_reason(ctx.author, reason)
|
audit_reason = get_audit_reason(ctx.author, reason)
|
||||||
bans = await guild.bans()
|
bans = await guild.bans()
|
||||||
bans = [be.user for be in bans]
|
bans = [be.user for be in bans]
|
||||||
if user not in bans:
|
if user not in bans:
|
||||||
@@ -647,7 +654,7 @@ class Mod:
|
|||||||
queue_entry = (guild.id, user.id)
|
queue_entry = (guild.id, user.id)
|
||||||
self.unban_queue.append(queue_entry)
|
self.unban_queue.append(queue_entry)
|
||||||
try:
|
try:
|
||||||
await guild.unban(user, reason=reason)
|
await guild.unban(user, reason=audit_reason)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
self.unban_queue.remove(queue_entry)
|
self.unban_queue.remove(queue_entry)
|
||||||
await ctx.send(_("Something went wrong while attempting to unban that user"))
|
await ctx.send(_("Something went wrong while attempting to unban that user"))
|
||||||
@@ -753,7 +760,7 @@ class Mod:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("That user is already muted and deafened server-wide!"))
|
await ctx.send(_("That user is already muted and deafened server-wide!"))
|
||||||
return
|
return
|
||||||
await ctx.send(_("User has been banned from speaking or " "listening in voice channels"))
|
await ctx.send(_("User has been banned from speaking or listening in voice channels"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await modlog.create_case(
|
await modlog.create_case(
|
||||||
@@ -825,7 +832,7 @@ class Mod:
|
|||||||
await ctx.send("Done.")
|
await ctx.send("Done.")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot do that, I lack the " "'{}' permission.").format("Manage Nicknames")
|
_("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@@ -833,8 +840,7 @@ class Mod:
|
|||||||
@checks.mod_or_permissions(manage_channel=True)
|
@checks.mod_or_permissions(manage_channel=True)
|
||||||
async def mute(self, ctx: commands.Context):
|
async def mute(self, ctx: commands.Context):
|
||||||
"""Mutes user in the channel/server"""
|
"""Mutes user in the channel/server"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@mute.command(name="voice")
|
@mute.command(name="voice")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -1002,8 +1008,7 @@ class Mod:
|
|||||||
"""Unmutes user in the channel/server
|
"""Unmutes user in the channel/server
|
||||||
|
|
||||||
Defaults to channel"""
|
Defaults to channel"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@unmute.command(name="voice")
|
@unmute.command(name="voice")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -1168,7 +1173,6 @@ class Mod:
|
|||||||
async def ignore(self, ctx: commands.Context):
|
async def ignore(self, ctx: commands.Context):
|
||||||
"""Adds servers/channels to ignorelist"""
|
"""Adds servers/channels to ignorelist"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
await ctx.send(await self.count_ignored())
|
await ctx.send(await self.count_ignored())
|
||||||
|
|
||||||
@ignore.command(name="channel")
|
@ignore.command(name="channel")
|
||||||
@@ -1185,7 +1189,7 @@ class Mod:
|
|||||||
await ctx.send(_("Channel already in ignore list."))
|
await ctx.send(_("Channel already in ignore list."))
|
||||||
|
|
||||||
@ignore.command(name="server", aliases=["guild"])
|
@ignore.command(name="server", aliases=["guild"])
|
||||||
@commands.has_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
async def ignore_guild(self, ctx: commands.Context):
|
async def ignore_guild(self, ctx: commands.Context):
|
||||||
"""Ignores current server"""
|
"""Ignores current server"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -1201,7 +1205,6 @@ class Mod:
|
|||||||
async def unignore(self, ctx: commands.Context):
|
async def unignore(self, ctx: commands.Context):
|
||||||
"""Removes servers/channels from ignorelist"""
|
"""Removes servers/channels from ignorelist"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
await ctx.send(await self.count_ignored())
|
await ctx.send(await self.count_ignored())
|
||||||
|
|
||||||
@unignore.command(name="channel")
|
@unignore.command(name="channel")
|
||||||
@@ -1219,7 +1222,7 @@ class Mod:
|
|||||||
await ctx.send(_("That channel is not in the ignore list."))
|
await ctx.send(_("That channel is not in the ignore list."))
|
||||||
|
|
||||||
@unignore.command(name="server", aliases=["guild"])
|
@unignore.command(name="server", aliases=["guild"])
|
||||||
@commands.has_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
async def unignore_guild(self, ctx: commands.Context):
|
async def unignore_guild(self, ctx: commands.Context):
|
||||||
"""Removes current guild from ignore list"""
|
"""Removes current guild from ignore list"""
|
||||||
guild = ctx.message.guild
|
guild = ctx.message.guild
|
||||||
@@ -1319,7 +1322,7 @@ class Mod:
|
|||||||
value="{0.name} (ID {0.id})".format(voice_state.channel),
|
value="{0.name} (ID {0.id})".format(voice_state.channel),
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
data.set_footer(text=_("Member #{} | User ID: {}" "").format(member_number, user.id))
|
data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
|
||||||
|
|
||||||
name = str(user)
|
name = str(user)
|
||||||
name = " ~ ".join((name, user.nick)) if user.nick else name
|
name = " ~ ".join((name, user.nick)) if user.nick else name
|
||||||
@@ -1335,7 +1338,7 @@ class Mod:
|
|||||||
try:
|
try:
|
||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(_("I need the `Embed links` permission " "to send this."))
|
await ctx.send(_("I need the `Embed links` permission to send this."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def names(self, ctx: commands.Context, user: discord.Member):
|
async def names(self, ctx: commands.Context, user: discord.Member):
|
||||||
@@ -1355,15 +1358,15 @@ class Mod:
|
|||||||
if msg:
|
if msg:
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That user doesn't have any recorded name or " "nickname change."))
|
await ctx.send(_("That user doesn't have any recorded name or nickname change."))
|
||||||
|
|
||||||
async def get_names_and_nicks(self, user):
|
async def get_names_and_nicks(self, user):
|
||||||
names = await self.settings.user(user).past_names()
|
names = await self.settings.user(user).past_names()
|
||||||
nicks = await self.settings.member(user).past_nicks()
|
nicks = await self.settings.member(user).past_nicks()
|
||||||
if names:
|
if names:
|
||||||
names = [escape(name, mass_mentions=True) for name in names]
|
names = [escape(name, mass_mentions=True) for name in names if name]
|
||||||
if nicks:
|
if nicks:
|
||||||
nicks = [escape(nick, mass_mentions=True) for nick in nicks]
|
nicks = [escape(nick, mass_mentions=True) for nick in nicks if nick]
|
||||||
return names, nicks
|
return names, nicks
|
||||||
|
|
||||||
async def check_tempban_expirations(self):
|
async def check_tempban_expirations(self):
|
||||||
@@ -1418,7 +1421,7 @@ class Mod:
|
|||||||
await guild.ban(author, reason="Mention spam (Autoban)")
|
await guild.ban(author, reason="Mention spam (Autoban)")
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
log.info(
|
log.info(
|
||||||
"Failed to ban member for mention spam in " "server {}.".format(guild.id)
|
"Failed to ban member for mention spam in server {}.".format(guild.id)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -1439,7 +1442,13 @@ class Mod:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def on_command(self, ctx: commands.Context):
|
async def on_command_completion(self, ctx: commands.Context):
|
||||||
|
await self._delete_delay(ctx)
|
||||||
|
|
||||||
|
async def on_command_error(self, ctx: commands.Context, error):
|
||||||
|
await self._delete_delay(ctx)
|
||||||
|
|
||||||
|
async def _delete_delay(self, ctx: commands.Context):
|
||||||
"""Currently used for:
|
"""Currently used for:
|
||||||
* delete delay"""
|
* delete delay"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -1595,18 +1604,22 @@ class Mod:
|
|||||||
if entry.target == target:
|
if entry.target == target:
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
async def on_member_update(self, before, after):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
if before.name != after.name:
|
if before.name != after.name:
|
||||||
async with self.settings.user(before).past_names() as name_list:
|
async with self.settings.user(before).past_names() as name_list:
|
||||||
if after.nick in name_list:
|
while None in name_list: # clean out null entries from a bug
|
||||||
|
name_list.remove(None)
|
||||||
|
if after.name in name_list:
|
||||||
# Ensure order is maintained without duplicates occuring
|
# Ensure order is maintained without duplicates occuring
|
||||||
name_list.remove(after.nick)
|
name_list.remove(after.name)
|
||||||
name_list.append(after.nick)
|
name_list.append(after.name)
|
||||||
while len(name_list) > 20:
|
while len(name_list) > 20:
|
||||||
name_list.pop(0)
|
name_list.pop(0)
|
||||||
|
|
||||||
if before.nick != after.nick and after.nick is not None:
|
if before.nick != after.nick and after.nick is not None:
|
||||||
async with self.settings.member(before).past_nicks() as nick_list:
|
async with self.settings.member(before).past_nicks() as nick_list:
|
||||||
|
while None in nick_list: # clean out null entries from a bug
|
||||||
|
nick_list.remove(None)
|
||||||
if after.nick in nick_list:
|
if after.nick in nick_list:
|
||||||
nick_list.remove(after.nick)
|
nick_list.remove(after.nick)
|
||||||
nick_list.append(after.nick)
|
nick_list.append(after.nick)
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class ModLog:
|
|||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def modlogset(self, ctx: commands.Context):
|
async def modlogset(self, ctx: commands.Context):
|
||||||
"""Settings for the mod log"""
|
"""Settings for the mod log"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@modlogset.command()
|
@modlogset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -35,9 +34,7 @@ class ModLog:
|
|||||||
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I do not have permissions to " "send messages in {}!").format(
|
_("I do not have permissions to send messages in {}!").format(channel.mention)
|
||||||
channel.mention
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import Tuple
|
|||||||
|
|
||||||
|
|
||||||
class CogOrCommand(commands.Converter):
|
class CogOrCommand(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||||
ret = ctx.bot.get_cog(arg)
|
ret = ctx.bot.get_cog(arg)
|
||||||
if ret:
|
if ret:
|
||||||
@@ -12,15 +11,34 @@ class CogOrCommand(commands.Converter):
|
|||||||
if ret:
|
if ret:
|
||||||
return "commands", ret.qualified_name
|
return "commands", ret.qualified_name
|
||||||
|
|
||||||
raise commands.BadArgument()
|
raise commands.BadArgument(
|
||||||
|
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
||||||
|
"".format(arg=arg)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RuleType(commands.Converter):
|
class RuleType(commands.Converter):
|
||||||
|
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
return "allow"
|
return "allow"
|
||||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
return "deny"
|
return "deny"
|
||||||
|
|
||||||
raise commands.BadArgument()
|
raise commands.BadArgument(
|
||||||
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClearableRuleType(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"
|
||||||
|
if arg.lower() in ("clear", "reset"):
|
||||||
|
return "clear"
|
||||||
|
|
||||||
|
raise commands.BadArgument(
|
||||||
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
||||||
|
"".format(arg=arg)
|
||||||
|
)
|
||||||
|
|||||||
102
redbot/cogs/permissions/mass_resolution.py
Normal file
102
redbot/cogs/permissions/mass_resolution.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from redbot.core import commands
|
||||||
|
from redbot.core.config import Config
|
||||||
|
from .resolvers import entries_from_ctx, resolve_lists
|
||||||
|
|
||||||
|
# This has optimizations in it that may not hold True if other parts of the permission
|
||||||
|
# model are changed from the state they are in currently.
|
||||||
|
# (commit hash ~ 3bcf375204c22271ad3ed1fc059b598b751aa03f)
|
||||||
|
#
|
||||||
|
# This is primarily to help with the performance of the help formatter
|
||||||
|
|
||||||
|
# This is less efficient if only checking one command,
|
||||||
|
# but is much faster for checking all of them.
|
||||||
|
|
||||||
|
|
||||||
|
async def mass_resolve(*, ctx: commands.Context, config: Config):
|
||||||
|
"""
|
||||||
|
Get's all the permission cog interactions for all loaded commands
|
||||||
|
in the given context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
owner_settings = await config.owner_models()
|
||||||
|
guild_owner_settings = await config.guild(ctx.guild).owner_models() if ctx.guild else None
|
||||||
|
|
||||||
|
ret = {"allowed": [], "denied": [], "default": []}
|
||||||
|
|
||||||
|
for cogname, cog in ctx.bot.cogs.items():
|
||||||
|
|
||||||
|
cog_setting = resolve_cog_or_command(
|
||||||
|
objname=cogname, models=owner_settings, ctx=ctx, typ="cogs"
|
||||||
|
)
|
||||||
|
if cog_setting is None and guild_owner_settings:
|
||||||
|
cog_setting = resolve_cog_or_command(
|
||||||
|
objname=cogname, models=guild_owner_settings, ctx=ctx, typ="cogs"
|
||||||
|
)
|
||||||
|
|
||||||
|
for command in [c for c in ctx.bot.all_commands.values() if c.instance is cog]:
|
||||||
|
resolution = recursively_resolve(
|
||||||
|
com_or_group=command,
|
||||||
|
o_models=owner_settings,
|
||||||
|
g_models=guild_owner_settings,
|
||||||
|
ctx=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
for com, resolved in resolution:
|
||||||
|
if resolved is None:
|
||||||
|
resolved = cog_setting
|
||||||
|
if resolved is True:
|
||||||
|
ret["allowed"].append(com)
|
||||||
|
elif resolved is False:
|
||||||
|
ret["denied"].append(com)
|
||||||
|
else:
|
||||||
|
ret["default"].append(com)
|
||||||
|
|
||||||
|
ret = {k: set(v) for k, v in ret.items()}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def recursively_resolve(*, com_or_group, o_models, g_models, ctx, override=False):
|
||||||
|
ret = []
|
||||||
|
if override:
|
||||||
|
current = False
|
||||||
|
else:
|
||||||
|
current = resolve_cog_or_command(
|
||||||
|
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||||
|
)
|
||||||
|
if current is None and g_models:
|
||||||
|
current = resolve_cog_or_command(
|
||||||
|
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||||
|
)
|
||||||
|
ret.append((com_or_group, current))
|
||||||
|
if isinstance(com_or_group, commands.Group):
|
||||||
|
for com in com_or_group.commands:
|
||||||
|
ret.extend(
|
||||||
|
recursively_resolve(
|
||||||
|
com_or_group=com,
|
||||||
|
o_models=o_models,
|
||||||
|
g_models=g_models,
|
||||||
|
ctx=ctx,
|
||||||
|
override=(current is False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cog_or_command(*, typ, ctx, objname, models: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Resolves models in order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
resolved = None
|
||||||
|
|
||||||
|
if objname in models.get(typ, {}):
|
||||||
|
blacklist = models[typ][objname].get("deny", [])
|
||||||
|
whitelist = models[typ][objname].get("allow", [])
|
||||||
|
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||||
|
if resolved is not None:
|
||||||
|
return resolved
|
||||||
|
resolved = models[typ][objname].get("default", None)
|
||||||
|
if resolved is not None:
|
||||||
|
return resolved
|
||||||
|
return None
|
||||||
@@ -7,16 +7,19 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core import checks
|
from redbot.core import checks
|
||||||
from redbot.core.config import Config
|
from redbot.core.config import Config
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.caching import LRUDict
|
||||||
|
|
||||||
from .resolvers import val_if_check_is_valid, resolve_models
|
from .resolvers import val_if_check_is_valid, resolve_models, entries_from_ctx
|
||||||
from .yaml_handler import yamlset_acl, yamlget_acl
|
from .yaml_handler import yamlset_acl, yamlget_acl
|
||||||
from .converters import CogOrCommand, RuleType
|
from .converters import CogOrCommand, RuleType, ClearableRuleType
|
||||||
|
from .mass_resolution import mass_resolve
|
||||||
|
|
||||||
_models = ["owner", "guildowner", "admin", "mod"]
|
_models = ["owner", "guildowner", "admin", "mod", "all"]
|
||||||
|
|
||||||
_ = Translator("Permissions", __file__)
|
_ = Translator("Permissions", __file__)
|
||||||
|
|
||||||
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
||||||
|
Y_OR_N = {"y": True, "yes": True, "n": False, "no": False}
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@@ -32,61 +35,34 @@ class Permissions:
|
|||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||||
self._before = []
|
|
||||||
self._after = []
|
|
||||||
self.config.register_global(owner_models={})
|
self.config.register_global(owner_models={})
|
||||||
self.config.register_guild(owner_models={})
|
self.config.register_guild(owner_models={})
|
||||||
|
self.cache = LRUDict(size=25000) # This can be tuned later
|
||||||
|
|
||||||
def add_check(self, check_obj: object, before_or_after: str):
|
async def get_user_ctx_overrides(self, ctx: commands.Context) -> dict:
|
||||||
"""
|
"""
|
||||||
adds a check to the check ordering
|
This takes a context object, and returns a dict of
|
||||||
|
|
||||||
checks should be a function taking 2 arguments:
|
allowed: list of commands
|
||||||
ctx: commands.Context
|
denied: list of commands
|
||||||
level: str
|
default: list of commands
|
||||||
|
|
||||||
and returning:
|
representing how permissions interacts with the
|
||||||
None: do not interfere
|
user, channel, guild, and (possibly) voice channel
|
||||||
True: command should be allowed even if they dont
|
for all commands on the bot (not just the one in the context object)
|
||||||
have role or perm requirements for the check
|
|
||||||
False: command should be blocked
|
|
||||||
|
|
||||||
before_or_after:
|
This mainly exists for use by the help formatter,
|
||||||
Should literally be a str equaling 'before' or 'after'
|
but others may find it useful
|
||||||
This should be based on if this should take priority
|
|
||||||
over set rules or not
|
|
||||||
|
|
||||||
3rd party cogs adding checks using this should only allow
|
Unlike the rest of the permission system, if other models are added later,
|
||||||
the owner to add checks before, and ensure only the owner
|
due to optimizations made for this, this needs to be adjusted accordingly
|
||||||
can add checks recieving the level 'owner'
|
|
||||||
|
|
||||||
3rd party cogs should keep a copy of of any checks they registered
|
This does not account for before and after permission hooks,
|
||||||
and deregister then on unload
|
these need to be checked seperately
|
||||||
"""
|
"""
|
||||||
|
return await mass_resolve(ctx=ctx, config=self.config)
|
||||||
|
|
||||||
if before_or_after == "before":
|
async def __global_check(self, ctx: commands.Context) -> bool:
|
||||||
self._before.append(check_obj)
|
|
||||||
elif before_or_after == "after":
|
|
||||||
self._after.append(check_obj)
|
|
||||||
else:
|
|
||||||
raise TypeError("RTFM")
|
|
||||||
|
|
||||||
def remove_check(self, check_obj: object, before_or_after: str):
|
|
||||||
"""
|
|
||||||
removes a previously registered check object
|
|
||||||
|
|
||||||
3rd party cogs should keep a copy of of any checks they registered
|
|
||||||
and deregister then on unload
|
|
||||||
"""
|
|
||||||
|
|
||||||
if before_or_after == "before":
|
|
||||||
self._before.remove(check_obj)
|
|
||||||
elif before_or_after == "after":
|
|
||||||
self._after.remove(check_obj)
|
|
||||||
else:
|
|
||||||
raise TypeError("RTFM")
|
|
||||||
|
|
||||||
async def __global_check(self, ctx):
|
|
||||||
"""
|
"""
|
||||||
Yes, this is needed on top of hooking into checks.py
|
Yes, this is needed on top of hooking into checks.py
|
||||||
to ensure that unchecked commands can still be managed by permissions
|
to ensure that unchecked commands can still be managed by permissions
|
||||||
@@ -94,7 +70,7 @@ class Permissions:
|
|||||||
defering to check logic
|
defering to check logic
|
||||||
This works since all checks must be True to run
|
This works since all checks must be True to run
|
||||||
"""
|
"""
|
||||||
v = await self.check_overrides(ctx, "mod")
|
v = await self.check_overrides(ctx, "all")
|
||||||
|
|
||||||
if v is False:
|
if v is False:
|
||||||
return False
|
return False
|
||||||
@@ -109,7 +85,7 @@ class Permissions:
|
|||||||
ctx: `redbot.core.context.commands.Context`
|
ctx: `redbot.core.context.commands.Context`
|
||||||
The context of the command
|
The context of the command
|
||||||
level: `str`
|
level: `str`
|
||||||
One of 'owner', 'guildowner', 'admin', 'mod'
|
One of 'owner', 'guildowner', 'admin', 'mod', 'all'
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -119,25 +95,44 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
return True
|
return True
|
||||||
voice_channel = None
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
voice_channel = ctx.author.voice.voice_channel
|
|
||||||
entries = [x for x in (ctx.author, voice_channel, ctx.channel) if x]
|
|
||||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
|
||||||
entries.extend([x.id for x in roles])
|
|
||||||
|
|
||||||
for check in self._before:
|
before = [
|
||||||
|
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
|
||||||
|
for cog in ctx.bot.cogs.values()
|
||||||
|
]
|
||||||
|
for check in before:
|
||||||
|
if check is None:
|
||||||
|
continue
|
||||||
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||||
if override is not None:
|
if override is not None:
|
||||||
return override
|
return override
|
||||||
|
|
||||||
for model in self.resolution_order[level]:
|
# checked ids + configureable to be checked against
|
||||||
override_model = getattr(self, model + "_model", None)
|
cache_tup = entries_from_ctx(ctx) + (
|
||||||
override = await override_model(ctx) if override_model else None
|
ctx.cog.__class__.__name__,
|
||||||
|
ctx.command.qualified_name,
|
||||||
|
)
|
||||||
|
if cache_tup in self.cache:
|
||||||
|
override = self.cache[cache_tup]
|
||||||
if override is not None:
|
if override is not None:
|
||||||
return override
|
return override
|
||||||
|
else:
|
||||||
|
for model in self.resolution_order[level]:
|
||||||
|
if ctx.guild is None and model != "owner":
|
||||||
|
break
|
||||||
|
override_model = getattr(self, model + "_model", None)
|
||||||
|
override = await override_model(ctx) if override_model else None
|
||||||
|
if override is not None:
|
||||||
|
self.cache[cache_tup] = override
|
||||||
|
return override
|
||||||
|
# This is intentional not being in an else block
|
||||||
|
self.cache[cache_tup] = None
|
||||||
|
|
||||||
for check in self._after:
|
after = [
|
||||||
|
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
|
||||||
|
for cog in ctx.bot.cogs.values()
|
||||||
|
]
|
||||||
|
for check in after:
|
||||||
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||||
if override is not None:
|
if override is not None:
|
||||||
return override
|
return override
|
||||||
@@ -156,7 +151,8 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
Handles guild level overrides
|
Handles guild level overrides
|
||||||
"""
|
"""
|
||||||
|
if ctx.guild is None:
|
||||||
|
return None
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
return resolve_models(ctx=ctx, models=models)
|
return resolve_models(ctx=ctx, models=models)
|
||||||
|
|
||||||
@@ -171,8 +167,7 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
Permission management tools
|
Permission management tools
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@permissions.command()
|
@permissions.command()
|
||||||
async def explain(self, ctx: commands.Context):
|
async def explain(self, ctx: commands.Context):
|
||||||
@@ -206,10 +201,10 @@ class Permissions:
|
|||||||
"\n"
|
"\n"
|
||||||
"1. Rules about a user.\n"
|
"1. Rules about a user.\n"
|
||||||
"2. Rules about the voice channel a user is in.\n"
|
"2. Rules about the voice channel a user is in.\n"
|
||||||
"3. Rules about the text channel a command was issued in\n"
|
"3. Rules about the text channel a command was issued in.\n"
|
||||||
"4. Rules about a role the user has "
|
"4. Rules about a role the user has "
|
||||||
"(The highest role they have with a rule will be used)\n"
|
"(The highest role they have with a rule will be used).\n"
|
||||||
"5. Rules about the guild a user is in (Owner level only)"
|
"5. Rules about the guild a user is in (Owner level only)."
|
||||||
"\n\nFor more details, please read the official documentation."
|
"\n\nFor more details, please read the official documentation."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -236,7 +231,9 @@ class Permissions:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
testcontext = await self.bot.get_context(message, cls=commands.Context)
|
testcontext = await self.bot.get_context(message, cls=commands.Context)
|
||||||
can = await com.can_run(testcontext)
|
can = await com.can_run(testcontext) and all(
|
||||||
|
[await p.can_run(testcontext) for p in com.parents]
|
||||||
|
)
|
||||||
except commands.CheckFailure:
|
except commands.CheckFailure:
|
||||||
can = False
|
can = False
|
||||||
|
|
||||||
@@ -254,15 +251,16 @@ class Permissions:
|
|||||||
Take a YAML file upload to set permissions from
|
Take a YAML file upload to set permissions from
|
||||||
"""
|
"""
|
||||||
if not ctx.message.attachments:
|
if not ctx.message.attachments:
|
||||||
return await ctx.send(_("You must upload a file"))
|
return await ctx.send(_("You must upload a file."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
|
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return await ctx.send(_("Inalid syntax"))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="getglobalacl")
|
@permissions.command(name="getglobalacl")
|
||||||
@@ -280,15 +278,16 @@ class Permissions:
|
|||||||
Take a YAML file upload to set permissions from
|
Take a YAML file upload to set permissions from
|
||||||
"""
|
"""
|
||||||
if not ctx.message.attachments:
|
if not ctx.message.attachments:
|
||||||
return await ctx.send(_("You must upload a file"))
|
return await ctx.send(_("You must upload a file."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
|
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return await ctx.send(_("Inalid syntax"))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -309,15 +308,16 @@ class Permissions:
|
|||||||
Use this to not lose existing rules
|
Use this to not lose existing rules
|
||||||
"""
|
"""
|
||||||
if not ctx.message.attachments:
|
if not ctx.message.attachments:
|
||||||
return await ctx.send(_("You must upload a file"))
|
return await ctx.send(_("You must upload a file."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
|
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return await ctx.send(_("Inalid syntax"))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="updateglobalacl")
|
@permissions.command(name="updateglobalacl")
|
||||||
@@ -328,15 +328,16 @@ class Permissions:
|
|||||||
Use this to not lose existing rules
|
Use this to not lose existing rules
|
||||||
"""
|
"""
|
||||||
if not ctx.message.attachments:
|
if not ctx.message.attachments:
|
||||||
return await ctx.send(_("You must upload a file"))
|
return await ctx.send(_("You must upload a file."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
|
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return await ctx.send(_("Inalid syntax"))
|
return await ctx.send(_("Invalid syntax."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Rules set."))
|
await ctx.send(_("Rules set."))
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="addglobalrule")
|
@permissions.command(name="addglobalrule")
|
||||||
@@ -348,7 +349,7 @@ class Permissions:
|
|||||||
who_or_what: str,
|
who_or_what: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
adds something to the rules
|
Adds something to the rules
|
||||||
|
|
||||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
@@ -363,7 +364,7 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
obj = self.find_object_uniquely(who_or_what)
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
if not obj:
|
if not obj:
|
||||||
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.owner_models() as models:
|
async with self.config.owner_models() as models:
|
||||||
data = {k: v for k, v in models.items()}
|
data = {k: v for k, v in models.items()}
|
||||||
@@ -380,6 +381,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].append(obj)
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule added."))
|
await ctx.send(_("Rule added."))
|
||||||
|
self.invalidate_cache(type_name, obj)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -392,7 +394,7 @@ class Permissions:
|
|||||||
who_or_what: str,
|
who_or_what: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
adds something to the rules
|
Adds something to the rules
|
||||||
|
|
||||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||||
|
|
||||||
@@ -407,7 +409,7 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
obj = self.find_object_uniquely(who_or_what)
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
if not obj:
|
if not obj:
|
||||||
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
data = {k: v for k, v in models.items()}
|
data = {k: v for k, v in models.items()}
|
||||||
@@ -424,6 +426,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].append(obj)
|
data[model_type][type_name][allow_or_deny].append(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule added."))
|
await ctx.send(_("Rule added."))
|
||||||
|
self.invalidate_cache(type_name, obj)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="removeglobalrule")
|
@permissions.command(name="removeglobalrule")
|
||||||
@@ -450,7 +453,7 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
obj = self.find_object_uniquely(who_or_what)
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
if not obj:
|
if not obj:
|
||||||
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.owner_models() as models:
|
async with self.config.owner_models() as models:
|
||||||
data = {k: v for k, v in models.items()}
|
data = {k: v for k, v in models.items()}
|
||||||
@@ -467,6 +470,7 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule removed."))
|
await ctx.send(_("Rule removed."))
|
||||||
|
self.invalidate_cache(obj, type_name)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@@ -494,7 +498,7 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
obj = self.find_object_uniquely(who_or_what)
|
obj = self.find_object_uniquely(who_or_what)
|
||||||
if not obj:
|
if not obj:
|
||||||
return await ctx.send(_("No unique matches. Try using an ID or mention"))
|
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
data = {k: v for k, v in models.items()}
|
data = {k: v for k, v in models.items()}
|
||||||
@@ -511,23 +515,18 @@ class Permissions:
|
|||||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Rule removed."))
|
await ctx.send(_("Rule removed."))
|
||||||
|
self.invalidate_cache(obj, type_name)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@permissions.command(name="setdefaultguildrule")
|
@permissions.command(name="setdefaultguildrule")
|
||||||
async def set_default_guild_rule(
|
async def set_default_guild_rule(
|
||||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Sets the default behavior for a cog or command if no rule is set
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
Use with a cog or command and no setting to clear the default and defer to
|
|
||||||
normal check logic
|
|
||||||
"""
|
"""
|
||||||
if allow_or_deny:
|
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
|
||||||
else:
|
|
||||||
val_to_set = None
|
|
||||||
|
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||||
@@ -540,24 +539,18 @@ class Permissions:
|
|||||||
data[model_type][type_name]["default"] = val_to_set
|
data[model_type][type_name]["default"] = val_to_set
|
||||||
|
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Defualt set."))
|
await ctx.send(_("Default set."))
|
||||||
|
self.invalidate_cache(type_name)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="setdefaultglobalrule")
|
@permissions.command(name="setdefaultglobalrule")
|
||||||
async def set_default_global_rule(
|
async def set_default_global_rule(
|
||||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Sets the default behavior for a cog or command if no rule is set
|
Sets the default behavior for a cog or command if no rule is set
|
||||||
|
|
||||||
Use with a cog or command and no setting to clear the default and defer to
|
|
||||||
normal check logic
|
|
||||||
"""
|
"""
|
||||||
|
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||||
if allow_or_deny:
|
|
||||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
|
||||||
else:
|
|
||||||
val_to_set = None
|
|
||||||
|
|
||||||
model_type, type_name = cog_or_command
|
model_type, type_name = cog_or_command
|
||||||
async with self.config.owner_models() as models:
|
async with self.config.owner_models() as models:
|
||||||
@@ -570,33 +563,18 @@ class Permissions:
|
|||||||
data[model_type][type_name]["default"] = val_to_set
|
data[model_type][type_name]["default"] = val_to_set
|
||||||
|
|
||||||
models.update(data)
|
models.update(data)
|
||||||
await ctx.send(_("Defualt set."))
|
await ctx.send(_("Default set."))
|
||||||
|
self.invalidate_cache(type_name)
|
||||||
|
|
||||||
@commands.bot_has_permissions(add_reactions=True)
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@permissions.command(name="clearglobalsettings")
|
@permissions.command(name="clearglobalsettings")
|
||||||
async def clear_globals(self, ctx: commands.Context):
|
async def clear_globals(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Clears all global rules.
|
Clears all global rules.
|
||||||
"""
|
"""
|
||||||
|
await self._confirm_then_clear_rules(ctx, is_guild=False)
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
m = await ctx.send("Are you sure?")
|
|
||||||
for r in REACTS.keys():
|
|
||||||
await m.add_reaction(r)
|
|
||||||
try:
|
|
||||||
reaction, user = await self.bot.wait_for(
|
|
||||||
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
|
||||||
|
|
||||||
if REACTS.get(str(reaction)):
|
|
||||||
await self.config.owner_models.clear()
|
|
||||||
await ctx.send(_("Global settings cleared"))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Okay."))
|
|
||||||
|
|
||||||
@commands.bot_has_permissions(add_reactions=True)
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@permissions.command(name="clearguildsettings")
|
@permissions.command(name="clearguildsettings")
|
||||||
@@ -604,23 +582,61 @@ class Permissions:
|
|||||||
"""
|
"""
|
||||||
Clears all guild rules.
|
Clears all guild rules.
|
||||||
"""
|
"""
|
||||||
|
await self._confirm_then_clear_rules(ctx, is_guild=True)
|
||||||
|
self.invalidate_cache(ctx.guild.id)
|
||||||
|
|
||||||
m = await ctx.send("Are you sure?")
|
async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
|
||||||
for r in REACTS.keys():
|
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
|
||||||
await m.add_reaction(r)
|
m = await ctx.send(_("Are you sure?"))
|
||||||
try:
|
for r in REACTS.keys():
|
||||||
reaction, user = await self.bot.wait_for(
|
await m.add_reaction(r)
|
||||||
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
|
try:
|
||||||
)
|
reaction, user = await self.bot.wait_for(
|
||||||
except asyncio.TimeoutError:
|
"reaction_add",
|
||||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
check=lambda r, u: u == ctx.author and str(r) in REACTS,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||||
|
|
||||||
if REACTS.get(str(reaction)):
|
agreed = REACTS.get(str(reaction))
|
||||||
await self.config.guild(ctx.guild).owner_models.clear()
|
else:
|
||||||
await ctx.send(_("Guild settings cleared"))
|
await ctx.send(_("Are you sure? (y/n)"))
|
||||||
|
try:
|
||||||
|
message = await self.bot.wait_for(
|
||||||
|
"message",
|
||||||
|
check=lambda m: m.author == ctx.author and m.content in Y_OR_N,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return await ctx.send(_("Ok, try responding with yes or no next time."))
|
||||||
|
|
||||||
|
agreed = Y_OR_N.get(message.content.lower())
|
||||||
|
|
||||||
|
if agreed:
|
||||||
|
if is_guild:
|
||||||
|
await self.config.guild(ctx.guild).owner_models.clear()
|
||||||
|
await ctx.send(_("Guild settings cleared."))
|
||||||
|
else:
|
||||||
|
await self.config.owner_models.clear()
|
||||||
|
await ctx.send(_("Global settings cleared."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Okay."))
|
await ctx.send(_("Okay."))
|
||||||
|
|
||||||
|
def invalidate_cache(self, *to_invalidate):
|
||||||
|
"""
|
||||||
|
Either invalidates the entire cache (if given no objects)
|
||||||
|
or does a partial invalidation based on passed objects
|
||||||
|
"""
|
||||||
|
if len(to_invalidate) == 0:
|
||||||
|
self.cache.clear()
|
||||||
|
return
|
||||||
|
# LRUDict inherits from ordered dict, hence the syntax below
|
||||||
|
stil_valid = [
|
||||||
|
(k, v) for k, v in self.cache.items() if not any(obj in k for obj in to_invalidate)
|
||||||
|
]
|
||||||
|
self.cache = LRUDict(*stil_valid, size=self.cache.size)
|
||||||
|
|
||||||
def find_object_uniquely(self, info: str) -> int:
|
def find_object_uniquely(self, info: str) -> int:
|
||||||
"""
|
"""
|
||||||
Finds an object uniquely, returns it's id or returns None
|
Finds an object uniquely, returns it's id or returns None
|
||||||
|
|||||||
@@ -7,26 +7,32 @@ from redbot.core import commands
|
|||||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
||||||
|
|
||||||
|
|
||||||
|
def entries_from_ctx(ctx: commands.Context) -> tuple:
|
||||||
|
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
|
||||||
|
return tuple(entries)
|
||||||
|
|
||||||
|
|
||||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
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
|
Returns the value from a check if it is valid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Non staticmethods should not be run without their parent
|
|
||||||
# class, even if the parent class did not deregister them
|
|
||||||
if check.__module__ is None:
|
|
||||||
pass
|
|
||||||
elif isinstance(check, types.FunctionType):
|
|
||||||
if (
|
|
||||||
next(filter(lambda x: check.__module__ == x.__module__, ctx.bot.cogs.values()), None)
|
|
||||||
is None
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
|
|
||||||
val = None
|
val = None
|
||||||
# let's not spam the console with improperly made 3rd party checks
|
# let's not spam the console with improperly made 3rd party checks
|
||||||
try:
|
try:
|
||||||
if asyncio.iscoroutine(check) or asyncio.iscoroutinefunction(check):
|
if asyncio.iscoroutinefunction(check):
|
||||||
val = await check(ctx, level=level)
|
val = await check(ctx, level=level)
|
||||||
else:
|
else:
|
||||||
val = check(ctx, level=level)
|
val = check(ctx, level=level)
|
||||||
@@ -67,23 +73,7 @@ def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) ->
|
|||||||
"""
|
"""
|
||||||
resolves specific lists
|
resolves specific lists
|
||||||
"""
|
"""
|
||||||
|
for entry in entries_from_ctx(ctx):
|
||||||
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:
|
if entry in whitelist:
|
||||||
return True
|
return True
|
||||||
if entry in blacklist:
|
if entry in blacklist:
|
||||||
|
|||||||
@@ -59,29 +59,29 @@ class Reports:
|
|||||||
@commands.group(name="reportset")
|
@commands.group(name="reportset")
|
||||||
async def reportset(self, ctx: commands.Context):
|
async def reportset(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
settings for reports
|
Settings for the report system.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="output")
|
@reportset.command(name="output")
|
||||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""sets the output channel"""
|
"""Set the channel where reports will show up"""
|
||||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
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)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="toggleactive")
|
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||||
async def report_toggle(self, ctx: commands.Context):
|
async def report_toggle(self, ctx: commands.Context):
|
||||||
"""Toggles whether the Reporting tool is enabled or not"""
|
"""Enables or Disables reporting for the server"""
|
||||||
|
|
||||||
active = await self.config.guild(ctx.guild).active()
|
active = await self.config.guild(ctx.guild).active()
|
||||||
active = not active
|
active = not active
|
||||||
await self.config.guild(ctx.guild).active.set(active)
|
await self.config.guild(ctx.guild).active.set(active)
|
||||||
if active:
|
if active:
|
||||||
await ctx.send(_("Reporting now enabled"))
|
await ctx.send(_("Reporting is now enabled"))
|
||||||
else:
|
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):
|
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||||
ret = False
|
ret = False
|
||||||
@@ -105,7 +105,7 @@ class Reports:
|
|||||||
*,
|
*,
|
||||||
mod: bool = False,
|
mod: bool = False,
|
||||||
permissions: Union[discord.Permissions, dict] = None,
|
permissions: Union[discord.Permissions, dict] = None,
|
||||||
prompt: str = ""
|
prompt: str = "",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
discovers which of shared guilds between the bot
|
discovers which of shared guilds between the bot
|
||||||
@@ -175,7 +175,10 @@ class Reports:
|
|||||||
if await self.bot.embed_requested(channel, author):
|
if await self.bot.embed_requested(channel, author):
|
||||||
em = discord.Embed(description=report)
|
em = discord.Embed(description=report)
|
||||||
em.set_author(
|
em.set_author(
|
||||||
name=_("Report from {0.display_name}").format(author), 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))
|
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||||
send_content = None
|
send_content = None
|
||||||
@@ -201,10 +204,10 @@ class Reports:
|
|||||||
@commands.group(name="report", invoke_without_command=True)
|
@commands.group(name="report", invoke_without_command=True)
|
||||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||||
"""
|
"""
|
||||||
Follow the prompts to make a report
|
Send a report.
|
||||||
|
|
||||||
optionally use with a report message
|
Use without arguments for interactive reporting, or do
|
||||||
to use it non interactively
|
[p]report <text> to use it non-interactively.
|
||||||
"""
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -212,11 +215,6 @@ class Reports:
|
|||||||
guild = await self.discover_guild(
|
guild = await self.discover_guild(
|
||||||
author, prompt=_("Select a server to make a report in by number.")
|
author, prompt=_("Select a server to make a report in by number.")
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
await ctx.message.delete()
|
|
||||||
except discord.Forbidden:
|
|
||||||
pass
|
|
||||||
if guild is None:
|
if guild is None:
|
||||||
return
|
return
|
||||||
g_active = await self.config.guild(guild).active()
|
g_active = await self.config.guild(guild).active()
|
||||||
@@ -229,22 +227,18 @@ class Reports:
|
|||||||
if self.antispam[guild.id][author.id].spammy:
|
if self.antispam[guild.id][author.id].spammy:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_(
|
_(
|
||||||
"You've sent a few too many of these recently. "
|
"You've sent too many reports recently. "
|
||||||
"Contact a server admin to resolve this, or try again "
|
"Please contact a server admin if this is important matter, "
|
||||||
"later."
|
"or please wait and try again later."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if author.id in self.user_cache:
|
if author.id in self.user_cache:
|
||||||
return await author.send(
|
return await author.send(
|
||||||
_("Finish making your prior report " "before making an additional one")
|
_(
|
||||||
|
"Please finish making your prior report before trying to make an "
|
||||||
|
"additional one!"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if ctx.guild:
|
|
||||||
try:
|
|
||||||
await ctx.message.delete()
|
|
||||||
except (discord.Forbidden, discord.HTTPException):
|
|
||||||
pass
|
|
||||||
self.user_cache.append(author.id)
|
self.user_cache.append(author.id)
|
||||||
|
|
||||||
if _report:
|
if _report:
|
||||||
@@ -261,9 +255,7 @@ class Reports:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("This requires DMs enabled."))
|
return await ctx.send(_("This requires DMs enabled."))
|
||||||
self.user_cache.remove(author.id)
|
|
||||||
return
|
|
||||||
|
|
||||||
def pred(m):
|
def pred(m):
|
||||||
return m.author == author and m.channel == dm.channel
|
return m.author == author and m.channel == dm.channel
|
||||||
@@ -271,18 +263,32 @@ class Reports:
|
|||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await author.send(_("You took too long. Try again later."))
|
return await author.send(_("You took too long. Try again later."))
|
||||||
else:
|
else:
|
||||||
val = await self.send_report(message, guild)
|
val = await self.send_report(message, guild)
|
||||||
|
|
||||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||||
if val is None:
|
if val is None:
|
||||||
await author.send(_("There was an error sending your report."))
|
await author.send(
|
||||||
|
_("There was an error sending your report, please contact a server admin.")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||||
self.antispam[guild.id][author.id].stamp()
|
self.antispam[guild.id][author.id].stamp()
|
||||||
|
|
||||||
self.user_cache.remove(author.id)
|
@report.after_invoke
|
||||||
|
async def report_cleanup(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
The logic is cleaner this way
|
||||||
|
"""
|
||||||
|
if ctx.author.id in self.user_cache:
|
||||||
|
self.user_cache.remove(ctx.author.id)
|
||||||
|
if ctx.guild and ctx.invoked_subcommand is None:
|
||||||
|
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
|
||||||
|
try:
|
||||||
|
await ctx.message.delete()
|
||||||
|
except discord.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
async def on_raw_reaction_add(self, payload):
|
async def on_raw_reaction_add(self, payload):
|
||||||
"""
|
"""
|
||||||
@@ -315,10 +321,12 @@ class Reports:
|
|||||||
@report.command(name="interact")
|
@report.command(name="interact")
|
||||||
async def response(self, ctx, ticket_number: int):
|
async def response(self, ctx, ticket_number: int):
|
||||||
"""
|
"""
|
||||||
opens a message tunnel between things you say in this channel
|
Open a message tunnel.
|
||||||
and the ticket opener's direct messages
|
|
||||||
|
|
||||||
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
|
# note, mod_or_permissions is an implicit guild_only
|
||||||
@@ -344,14 +352,15 @@ class Reports:
|
|||||||
)
|
)
|
||||||
|
|
||||||
big_topic = _(
|
big_topic = _(
|
||||||
"{who} opened a 2-way communication."
|
"{who} opened a 2-way communication "
|
||||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||||
"(8MB file size limitation on uploads) "
|
"(8MB file size limitation on uploads) "
|
||||||
"will be forwarded to them until the communication is closed.\n"
|
"will be forwarded to them until the communication is closed.\n"
|
||||||
"You can close a communication at any point "
|
"You can close a communication at any point by reacting with "
|
||||||
"by reacting with the X to the last message recieved. "
|
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||||
"\nAny message succesfully forwarded will be marked with a check."
|
"Any message succesfully forwarded will be marked with "
|
||||||
"\nTunnels are not persistent across bot restarts."
|
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||||
|
"Tunnels are not persistent across bot restarts."
|
||||||
)
|
)
|
||||||
topic = big_topic.format(
|
topic = big_topic.format(
|
||||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||||
@@ -359,8 +368,7 @@ class Reports:
|
|||||||
try:
|
try:
|
||||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("User has disabled DMs."))
|
await ctx.send(_("That user has DMs disabled."))
|
||||||
tun.close()
|
|
||||||
else:
|
else:
|
||||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .streams import Streams
|
from .streams import Streams
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
async def setup(bot):
|
||||||
bot.add_cog(Streams(bot))
|
cog = Streams(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
|
|||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from .streamtypes import (
|
from .streamtypes import (
|
||||||
|
Stream,
|
||||||
TwitchStream,
|
TwitchStream,
|
||||||
HitboxStream,
|
HitboxStream,
|
||||||
MixerStream,
|
MixerStream,
|
||||||
@@ -25,6 +26,7 @@ from . import streamtypes as StreamClasses
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
CHECK_DELAY = 60
|
CHECK_DELAY = 60
|
||||||
|
|
||||||
@@ -50,9 +52,11 @@ class Streams:
|
|||||||
|
|
||||||
self.db.register_role(**self.role_defaults)
|
self.db.register_role(**self.role_defaults)
|
||||||
|
|
||||||
self.bot = bot
|
self.bot: Red = bot
|
||||||
|
|
||||||
self.bot.loop.create_task(self._initialize_lists())
|
self.streams: List[Stream] = []
|
||||||
|
self.communities: List[TwitchCommunity] = []
|
||||||
|
self.task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
|
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
|
||||||
|
|
||||||
@@ -62,7 +66,8 @@ class Streams:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _initialize_lists(self):
|
async def initialize(self) -> None:
|
||||||
|
"""Should be called straight after cog instantiation."""
|
||||||
self.streams = await self.load_streams()
|
self.streams = await self.load_streams()
|
||||||
self.communities = await self.load_communities()
|
self.communities = await self.load_communities()
|
||||||
|
|
||||||
@@ -70,16 +75,14 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def twitch(self, ctx: commands.Context, channel_name: str):
|
async def twitch(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Twitch channel is streaming"""
|
"""Checks if a Twitch channel is live"""
|
||||||
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
||||||
stream = TwitchStream(name=channel_name, token=token)
|
stream = TwitchStream(name=channel_name, token=token)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
||||||
"""
|
"""Checks if a Youtube channel is live"""
|
||||||
Checks if a Youtube channel is streaming
|
|
||||||
"""
|
|
||||||
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
||||||
is_name = self.check_name_or_id(channel_id_or_name)
|
is_name = self.check_name_or_id(channel_id_or_name)
|
||||||
if is_name:
|
if is_name:
|
||||||
@@ -90,19 +93,19 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Hitbox channel is streaming"""
|
"""Checks if a Hitbox channel is live"""
|
||||||
stream = HitboxStream(name=channel_name)
|
stream = HitboxStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def mixer(self, ctx: commands.Context, channel_name: str):
|
async def mixer(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Mixer channel is streaming"""
|
"""Checks if a Mixer channel is live"""
|
||||||
stream = MixerStream(name=channel_name)
|
stream = MixerStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def picarto(self, ctx: commands.Context, channel_name: str):
|
async def picarto(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Picarto channel is streaming"""
|
"""Checks if a Picarto channel is live"""
|
||||||
stream = PicartoStream(name=channel_name)
|
stream = PicartoStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@@ -110,24 +113,24 @@ class Streams:
|
|||||||
try:
|
try:
|
||||||
embed = await stream.is_online()
|
embed = await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
await ctx.send(_("The stream is offline."))
|
await ctx.send(_("That user is offline."))
|
||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
await ctx.send(_("The channel doesn't seem to exist."))
|
await ctx.send(_("That channel doesn't seem to exist."))
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. " "See `{}`.").format(
|
_("The twitch token is either invalid or has not been set. See `{}`.").format(
|
||||||
"{}streamset twitchtoken".format(ctx.prefix)
|
"{}streamset twitchtoken".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The Youtube API key is either invalid or has not been set. " "See {}.").format(
|
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
|
||||||
"`{}streamset youtubekey`".format(ctx.prefix)
|
"`{}streamset youtubekey`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
_("Something went wrong whilst trying to contact the stream service's API.")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
@@ -136,50 +139,46 @@ class Streams:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamalert(self, ctx: commands.Context):
|
async def streamalert(self, ctx: commands.Context):
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@streamalert.group(name="twitch")
|
@streamalert.group(name="twitch")
|
||||||
async def _twitch(self, ctx: commands.Context):
|
async def _twitch(self, ctx: commands.Context):
|
||||||
"""Twitch stream alerts"""
|
"""Twitch stream alerts"""
|
||||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@_twitch.command(name="channel")
|
@_twitch.command(name="channel")
|
||||||
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Twitch stream alert notification in the channel"""
|
"""Sets a Twitch alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
||||||
|
|
||||||
@_twitch.command(name="community")
|
@_twitch.command(name="community")
|
||||||
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
||||||
"""Sets a Twitch stream alert notification in the channel
|
"""Sets an alert notification in the channel for the specified twitch community."""
|
||||||
for the specified community."""
|
|
||||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||||
|
|
||||||
@streamalert.command(name="youtube")
|
@streamalert.command(name="youtube")
|
||||||
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
||||||
"""Sets a Youtube stream alert notification in the channel"""
|
"""Sets a Youtube alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
||||||
|
|
||||||
@streamalert.command(name="hitbox")
|
@streamalert.command(name="hitbox")
|
||||||
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Hitbox stream alert notification in the channel"""
|
"""Sets a Hitbox alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, HitboxStream, channel_name)
|
await self.stream_alert(ctx, HitboxStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="mixer")
|
@streamalert.command(name="mixer")
|
||||||
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Mixer stream alert notification in the channel"""
|
"""Sets a Mixer alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, MixerStream, channel_name)
|
await self.stream_alert(ctx, MixerStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="picarto")
|
@streamalert.command(name="picarto")
|
||||||
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Picarto stream alert notification in the channel"""
|
"""Sets a Picarto alert notification in the channel"""
|
||||||
await self.stream_alert(ctx, PicartoStream, channel_name)
|
await self.stream_alert(ctx, PicartoStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="stop")
|
@streamalert.command(name="stop")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Stops all stream notifications in the channel
|
"""Stops all stream notifications in the channel
|
||||||
|
|
||||||
Adding 'yes' will disable all notifications in the server"""
|
Adding 'yes' will disable all notifications in the server"""
|
||||||
streams = self.streams.copy()
|
streams = self.streams.copy()
|
||||||
local_channel_ids = [c.id for c in ctx.guild.channels]
|
local_channel_ids = [c.id for c in ctx.guild.channels]
|
||||||
@@ -202,7 +201,7 @@ class Streams:
|
|||||||
self.streams = streams
|
self.streams = streams
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
|
|
||||||
msg = _("All {}'s stream alerts have been disabled." "").format(
|
msg = _("All the alerts in the {} have been disabled.").format(
|
||||||
"server" if _all else "channel"
|
"server" if _all else "channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -212,7 +211,7 @@ class Streams:
|
|||||||
async def streamalert_list(self, ctx: commands.Context):
|
async def streamalert_list(self, ctx: commands.Context):
|
||||||
streams_list = defaultdict(list)
|
streams_list = defaultdict(list)
|
||||||
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
||||||
msg = _("Active stream alerts:\n\n")
|
msg = _("Active alerts:\n\n")
|
||||||
|
|
||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
for channel_id in stream.channels:
|
for channel_id in stream.channels:
|
||||||
@@ -220,7 +219,7 @@ class Streams:
|
|||||||
streams_list[channel_id].append(stream.name.lower())
|
streams_list[channel_id].append(stream.name.lower())
|
||||||
|
|
||||||
if not streams_list:
|
if not streams_list:
|
||||||
await ctx.send(_("There are no active stream alerts in this server."))
|
await ctx.send(_("There are no active alerts in this server."))
|
||||||
return
|
return
|
||||||
|
|
||||||
for channel_id, streams in streams_list.items():
|
for channel_id, streams in streams_list.items():
|
||||||
@@ -243,7 +242,7 @@ class Streams:
|
|||||||
exists = await self.check_exists(stream)
|
exists = await self.check_exists(stream)
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. " "See {}.").format(
|
_("Your twitch token is either invalid or has not been set. See {}.").format(
|
||||||
"`{}streamset twitchtoken`".format(ctx.prefix)
|
"`{}streamset twitchtoken`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -251,13 +250,13 @@ class Streams:
|
|||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"The Youtube API key is either invalid or has not been set. " "See {}."
|
"Your Youtube API key is either invalid or has not been set. See {}."
|
||||||
).format("`{}streamset youtubekey`".format(ctx.prefix))
|
).format("`{}streamset youtubekey`".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
_("Something went wrong whilst trying to contact the stream service's API.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -276,7 +275,7 @@ class Streams:
|
|||||||
await community.get_community_streams()
|
await community.get_community_streams()
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. " "See {}.").format(
|
_("The twitch token is either invalid or has not been set. See {}.").format(
|
||||||
"`{}streamset twitchtoken`".format(ctx.prefix)
|
"`{}streamset twitchtoken`".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -286,7 +285,7 @@ class Streams:
|
|||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Something went wrong whilst trying to contact the " "stream service's API.")
|
_("Something went wrong whilst trying to contact the stream service's API.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except OfflineCommunity:
|
except OfflineCommunity:
|
||||||
@@ -297,14 +296,12 @@ class Streams:
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamset(self, ctx: commands.Context):
|
async def streamset(self, ctx: commands.Context):
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||||
"""Set the Client ID for twitch.
|
"""Set the Client ID for twitch.
|
||||||
|
|
||||||
To do this, follow these steps:
|
To do this, follow these steps:
|
||||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
||||||
2. Click *Register Your Application*
|
2. Click *Register Your Application*
|
||||||
@@ -312,7 +309,6 @@ class Streams:
|
|||||||
select an Application Category of your choosing.
|
select an Application Category of your choosing.
|
||||||
4. Click *Register*, and on the following page, copy the Client ID.
|
4. Click *Register*, and on the following page, copy the Client ID.
|
||||||
5. Paste the Client ID into this command. Done!
|
5. Paste the Client ID into this command. Done!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||||
@@ -322,14 +318,11 @@ class Streams:
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||||
"""Sets the API key for Youtube.
|
"""Sets the API key for Youtube.
|
||||||
|
|
||||||
To get one, do the following:
|
To get one, do the following:
|
||||||
|
|
||||||
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
||||||
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
||||||
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
||||||
4. Copy your API key and paste it into this command. Done!
|
4. Copy your API key and paste it into this command. Done!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||||
await ctx.send(_("Youtube key set."))
|
await ctx.send(_("Youtube key set."))
|
||||||
@@ -337,9 +330,8 @@ class Streams:
|
|||||||
@streamset.group()
|
@streamset.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def mention(self, ctx: commands.Context):
|
async def mention(self, ctx: commands.Context):
|
||||||
"""Sets mentions for stream alerts."""
|
"""Sets mentions for alerts."""
|
||||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@mention.command(aliases=["everyone"])
|
@mention.command(aliases=["everyone"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -350,17 +342,16 @@ class Streams:
|
|||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_everyone.set(False)
|
await self.db.guild(guild).mention_everyone.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} will no longer be mentioned " "for a stream alert.").format(
|
_("{} will no longer be mentioned when a stream or community is live").format(
|
||||||
"@\u200beveryone"
|
"@\u200beveryone"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_everyone.set(True)
|
await self.db.guild(guild).mention_everyone.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200beveryone"
|
||||||
"comes online, {} will be mentioned"
|
)
|
||||||
).format("@\u200beveryone")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command(aliases=["here"])
|
@mention.command(aliases=["here"])
|
||||||
@@ -371,16 +362,13 @@ class Streams:
|
|||||||
current_setting = await self.db.guild(guild).mention_here()
|
current_setting = await self.db.guild(guild).mention_here()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_here.set(False)
|
await self.db.guild(guild).mention_here.set(False)
|
||||||
await ctx.send(
|
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
|
||||||
_("{} will no longer be mentioned " "for a stream alert.").format("@\u200bhere")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_here.set(True)
|
await self.db.guild(guild).mention_here.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200bhere"
|
||||||
"comes online, {} will be mentioned"
|
)
|
||||||
).format("@\u200bhere")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command()
|
@mention.command()
|
||||||
@@ -394,18 +382,16 @@ class Streams:
|
|||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.role(role).mention.set(False)
|
await self.db.role(role).mention.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} will no longer be mentioned " "for a stream alert").format(
|
_("{} will no longer be mentioned for an alert.").format(
|
||||||
"@\u200b{}".format(role.name)
|
"@\u200b{}".format(role.name)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.role(role).mention.set(True)
|
await self.db.role(role).mention.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("When a stream or community " "is live, {} will be mentioned." "").format(
|
||||||
"When a stream configured for stream alerts "
|
"@\u200b{}".format(role.name)
|
||||||
"comes online, {} will be mentioned"
|
)
|
||||||
""
|
|
||||||
).format("@\u200b{}".format(role.name))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@@ -414,7 +400,7 @@ class Streams:
|
|||||||
"""Toggles automatic deletion of notifications for streams that go offline"""
|
"""Toggles automatic deletion of notifications for streams that go offline"""
|
||||||
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
||||||
if on_off:
|
if on_off:
|
||||||
await ctx.send("The notifications will be deleted once " "streams go offline.")
|
await ctx.send("The notifications will be deleted once streams go offline.")
|
||||||
else:
|
else:
|
||||||
await ctx.send("Notifications will never be deleted.")
|
await ctx.send("Notifications will never be deleted.")
|
||||||
|
|
||||||
@@ -424,7 +410,7 @@ class Streams:
|
|||||||
if stream not in self.streams:
|
if stream not in self.streams:
|
||||||
self.streams.append(stream)
|
self.streams.append(stream)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I'll send a notification in this channel when {} " "is online.").format(
|
_("I'll now send a notification in this channel when {} is live.").format(
|
||||||
stream.name
|
stream.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -433,7 +419,7 @@ class Streams:
|
|||||||
if not stream.channels:
|
if not stream.channels:
|
||||||
self.streams.remove(stream)
|
self.streams.remove(stream)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I won't send notifications about {} in this " "channel anymore.").format(
|
_("I won't send notifications about {} in this channel anymore.").format(
|
||||||
stream.name
|
stream.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -448,7 +434,7 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I'll send a notification in this channel when a "
|
"I'll send a notification in this channel when a "
|
||||||
"channel is streaming to the {} community"
|
"channel is live in the {} community."
|
||||||
""
|
""
|
||||||
).format(community.name)
|
).format(community.name)
|
||||||
)
|
)
|
||||||
@@ -459,7 +445,7 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I won't send notifications about channels streaming "
|
"I won't send notifications about channels streaming "
|
||||||
"to the {} community in this channel anymore"
|
"in the {} community in this channel anymore."
|
||||||
""
|
""
|
||||||
).format(community.name)
|
).format(community.name)
|
||||||
)
|
)
|
||||||
@@ -534,9 +520,9 @@ class Streams:
|
|||||||
mention_str = await self._get_mention_str(channel.guild)
|
mention_str = await self._get_mention_str(channel.guild)
|
||||||
|
|
||||||
if mention_str:
|
if mention_str:
|
||||||
content = "{}, {} is online!".format(mention_str, stream.name)
|
content = "{}, {} is live!".format(mention_str, stream.name)
|
||||||
else:
|
else:
|
||||||
content = "{} is online!".format(stream.name)
|
content = "{} is live!".format(stream.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
m = await channel.send(content, embed=embed)
|
m = await channel.send(content, embed=embed)
|
||||||
@@ -562,7 +548,7 @@ class Streams:
|
|||||||
try:
|
try:
|
||||||
stream_list = await community.get_community_streams()
|
stream_list = await community.get_community_streams()
|
||||||
except CommunityNotFound:
|
except CommunityNotFound:
|
||||||
print(_("Community {} not found!").format(community.name))
|
print(_("The Community {} was not found!").format(community.name))
|
||||||
continue
|
continue
|
||||||
except OfflineCommunity:
|
except OfflineCommunity:
|
||||||
for message in community._messages_cache:
|
for message in community._messages_cache:
|
||||||
@@ -671,4 +657,5 @@ class Streams:
|
|||||||
await self.db.communities.set(raw_communities)
|
await self.db.communities.set(raw_communities)
|
||||||
|
|
||||||
def __unload(self):
|
def __unload(self):
|
||||||
self.task.cancel()
|
if self.task:
|
||||||
|
self.task.cancel()
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ def rnd(url):
|
|||||||
|
|
||||||
|
|
||||||
class TwitchCommunity:
|
class TwitchCommunity:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.name = kwargs.pop("name")
|
self.name = kwargs.pop("name")
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
@@ -119,7 +118,6 @@ class TwitchCommunity:
|
|||||||
|
|
||||||
|
|
||||||
class Stream:
|
class Stream:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.name = kwargs.pop("name", None)
|
self.name = kwargs.pop("name", None)
|
||||||
self.channels = kwargs.pop("channels", [])
|
self.channels = kwargs.pop("channels", [])
|
||||||
@@ -148,7 +146,6 @@ class Stream:
|
|||||||
|
|
||||||
|
|
||||||
class YoutubeStream(Stream):
|
class YoutubeStream(Stream):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
self._token = kwargs.pop("token", None)
|
self._token = kwargs.pop("token", None)
|
||||||
@@ -183,7 +180,7 @@ class YoutubeStream(Stream):
|
|||||||
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
|
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
|
||||||
title = vid_data["snippet"]["title"]
|
title = vid_data["snippet"]["title"]
|
||||||
thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"]
|
thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"]
|
||||||
channel_title = data["snippet"]["channelTitle"]
|
channel_title = vid_data["snippet"]["channelTitle"]
|
||||||
embed = discord.Embed(title=title, url=video_url)
|
embed = discord.Embed(title=title, url=video_url)
|
||||||
embed.set_author(name=channel_title)
|
embed.set_author(name=channel_title)
|
||||||
embed.set_image(url=rnd(thumbnail))
|
embed.set_image(url=rnd(thumbnail))
|
||||||
@@ -213,7 +210,6 @@ class YoutubeStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class TwitchStream(Stream):
|
class TwitchStream(Stream):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.id = kwargs.pop("id", None)
|
self.id = kwargs.pop("id", None)
|
||||||
self._token = kwargs.pop("token", None)
|
self._token = kwargs.pop("token", None)
|
||||||
@@ -266,7 +262,7 @@ class TwitchStream(Stream):
|
|||||||
url = channel["url"]
|
url = channel["url"]
|
||||||
logo = channel["logo"]
|
logo = channel["logo"]
|
||||||
if logo is None:
|
if logo is None:
|
||||||
logo = "https://static-cdn.jtvnw.net/" "jtv_user_pictures/xarth/404_user_70x70.png"
|
logo = "https://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_70x70.png"
|
||||||
status = channel["status"]
|
status = channel["status"]
|
||||||
if not status:
|
if not status:
|
||||||
status = "Untitled broadcast"
|
status = "Untitled broadcast"
|
||||||
@@ -288,7 +284,6 @@ class TwitchStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class HitboxStream(Stream):
|
class HitboxStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://api.hitbox.tv/media/live/" + self.name
|
url = "https://api.hitbox.tv/media/live/" + self.name
|
||||||
|
|
||||||
@@ -326,7 +321,6 @@ class HitboxStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class MixerStream(Stream):
|
class MixerStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://mixer.com/api/v1/channels/" + self.name
|
url = "https://mixer.com/api/v1/channels/" + self.name
|
||||||
|
|
||||||
@@ -348,7 +342,7 @@ class MixerStream(Stream):
|
|||||||
raise APIError()
|
raise APIError()
|
||||||
|
|
||||||
def make_embed(self, data):
|
def make_embed(self, data):
|
||||||
default_avatar = "https://mixer.com/_latest/assets/images/main/" "avatars/default.jpg"
|
default_avatar = "https://mixer.com/_latest/assets/images/main/avatars/default.jpg"
|
||||||
user = data["user"]
|
user = data["user"]
|
||||||
url = "https://mixer.com/" + data["token"]
|
url = "https://mixer.com/" + data["token"]
|
||||||
embed = discord.Embed(title=data["name"], url=url)
|
embed = discord.Embed(title=data["name"], url=url)
|
||||||
@@ -368,7 +362,6 @@ class MixerStream(Stream):
|
|||||||
|
|
||||||
|
|
||||||
class PicartoStream(Stream):
|
class PicartoStream(Stream):
|
||||||
|
|
||||||
async def is_online(self):
|
async def is_online(self):
|
||||||
url = "https://api.picarto.tv/v1/channel/name/" + self.name
|
url = "https://api.picarto.tv/v1/channel/name/" + self.name
|
||||||
|
|
||||||
@@ -390,7 +383,7 @@ class PicartoStream(Stream):
|
|||||||
|
|
||||||
def make_embed(self, data):
|
def make_embed(self, data):
|
||||||
avatar = rnd(
|
avatar = rnd(
|
||||||
"https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" "".format(data["name"].lower())
|
"https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg".format(data["name"].lower())
|
||||||
)
|
)
|
||||||
url = "https://picarto.tv/" + data["name"]
|
url = "https://picarto.tv/" + data["name"]
|
||||||
thumbnail = data["thumbnails"]["web"]
|
thumbnail = data["thumbnails"]["web"]
|
||||||
@@ -412,5 +405,5 @@ class PicartoStream(Stream):
|
|||||||
data["adult"] = ""
|
data["adult"] = ""
|
||||||
|
|
||||||
embed.color = 0x4C90F3
|
embed.color = 0x4C90F3
|
||||||
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" "".format(**data))
|
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}".format(**data))
|
||||||
return embed
|
return embed
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
import yaml
|
import yaml
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
from redbot.ext import trivia as ext_trivia
|
from redbot.ext import trivia as ext_trivia
|
||||||
from redbot.core import Config, checks
|
from redbot.core import Config, checks
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
@@ -18,6 +18,7 @@ UNIQUE_ID = 0xb3c0e453
|
|||||||
|
|
||||||
class InvalidListError(Exception):
|
class InvalidListError(Exception):
|
||||||
"""A Trivia list file is in invalid format."""
|
"""A Trivia list file is in invalid format."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +47,6 @@ class Trivia:
|
|||||||
async def triviaset(self, ctx: commands.Context):
|
async def triviaset(self, ctx: commands.Context):
|
||||||
"""Manage trivia settings."""
|
"""Manage trivia settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
await ctx.send_help()
|
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
settings_dict = await settings.all()
|
settings_dict = await settings.all()
|
||||||
msg = box(
|
msg = box(
|
||||||
@@ -81,7 +81,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.delay.set(seconds)
|
await settings.delay.set(seconds)
|
||||||
await ctx.send("Done. Maximum seconds to answer set to {}." "".format(seconds))
|
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
|
||||||
|
|
||||||
@triviaset.command(name="stopafter")
|
@triviaset.command(name="stopafter")
|
||||||
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
||||||
@@ -160,7 +160,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
await settings.payout_multiplier.set(multiplier)
|
await settings.payout_multiplier.set(multiplier)
|
||||||
if not multiplier:
|
if not multiplier:
|
||||||
await ctx.send("Done. I will no longer reward the winner with a" " payout.")
|
await ctx.send("Done. I will no longer reward the winner with a payout.")
|
||||||
return
|
return
|
||||||
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
if not trivia_dict:
|
if not trivia_dict:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"The trivia list was parsed successfully, however" " it appears to be empty!"
|
"The trivia list was parsed successfully, however it appears to be empty!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
settings = await self.conf.guild(ctx.guild).all()
|
settings = await self.conf.guild(ctx.guild).all()
|
||||||
@@ -245,13 +245,13 @@ class Trivia:
|
|||||||
"""List available trivia categories."""
|
"""List available trivia categories."""
|
||||||
lists = set(p.stem for p in self._all_lists())
|
lists = set(p.stem for p in self._all_lists())
|
||||||
|
|
||||||
msg = box("**Available trivia lists**\n\n{}" "".format(", ".join(sorted(lists))))
|
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
|
||||||
if len(msg) > 1000:
|
if len(msg) > 1000:
|
||||||
await ctx.author.send(msg)
|
await ctx.author.send(msg)
|
||||||
return
|
return
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@trivia.group(name="leaderboard", aliases=["lboard"])
|
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
|
||||||
async def trivia_leaderboard(self, ctx: commands.Context):
|
async def trivia_leaderboard(self, ctx: commands.Context):
|
||||||
"""Leaderboard for trivia.
|
"""Leaderboard for trivia.
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@ class Trivia:
|
|||||||
try:
|
try:
|
||||||
priority.remove(key)
|
priority.remove(key)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("{} is not a valid key".format(key))
|
raise ValueError("{} is not a valid key.".format(key))
|
||||||
# Put key last in reverse priority
|
# Put key last in reverse priority
|
||||||
priority.append(key)
|
priority.append(key)
|
||||||
items = data.items()
|
items = data.items()
|
||||||
@@ -480,13 +480,13 @@ class Trivia:
|
|||||||
try:
|
try:
|
||||||
path = next(p for p in self._all_lists() if p.stem == category)
|
path = next(p for p in self._all_lists() if p.stem == category)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise FileNotFoundError("Could not find the `{}` category" "".format(category))
|
raise FileNotFoundError("Could not find the `{}` category.".format(category))
|
||||||
|
|
||||||
with path.open(encoding="utf-8") as file:
|
with path.open(encoding="utf-8") as file:
|
||||||
try:
|
try:
|
||||||
dict_ = yaml.load(file)
|
dict_ = yaml.load(file)
|
||||||
except yaml.error.YAMLError as exc:
|
except yaml.error.YAMLError as exc:
|
||||||
raise InvalidListError("YAML parsing failed") from exc
|
raise InvalidListError("YAML parsing failed.") from exc
|
||||||
else:
|
else:
|
||||||
return dict_
|
return dict_
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async def warning_points_add_check(
|
|||||||
act = a
|
act = a
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if act: # some action needs to be taken
|
if act and act["exceed_command"] is not None: # some action needs to be taken
|
||||||
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async def warning_points_remove_check(
|
|||||||
act = a
|
act = a
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if act: # some action needs to be taken
|
if act and act["drop_command"] is not None: # some action needs to be taken
|
||||||
await create_and_invoke_context(ctx, act["drop_command"], user)
|
await create_and_invoke_context(ctx, act["drop_command"], user)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
|
|||||||
check_str = inspect.getsource(checks.is_owner)
|
check_str = inspect.getsource(checks.is_owner)
|
||||||
if any(inspect.getsource(x) in check_str for x in com.checks):
|
if any(inspect.getsource(x) in check_str for x in com.checks):
|
||||||
# command the user specified has the is_owner check
|
# command the user specified has the is_owner check
|
||||||
return None, _(
|
return (
|
||||||
"That command requires bot owner. I can't " "allow you to use that for an action"
|
None,
|
||||||
|
_("That command requires bot owner. I can't allow you to use that for an action"),
|
||||||
)
|
)
|
||||||
return "{prefix}" + orig, None
|
return "{prefix}" + orig, None
|
||||||
|
|
||||||
@@ -80,10 +81,11 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
the points threshold for the action"""
|
the points threshold for the action"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Enter the command to be run when the user exceeds the points for "
|
"Enter the command to be run when the user **exceeds the points for "
|
||||||
"this action to occur.\nEnter it exactly as you would if you were "
|
"this action to occur.**\n**If you do not wish to have a command run, enter** "
|
||||||
|
"`none`.\n\nEnter it exactly as you would if you were "
|
||||||
"actually trying to run the command, except don't put a prefix and "
|
"actually trying to run the command, except don't put a prefix and "
|
||||||
"use {user} in place of any user/member arguments\n\n"
|
"use `{user}` in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response."
|
"Please wait 15 seconds before entering your response."
|
||||||
@@ -99,8 +101,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
if msg.content == "none":
|
||||||
|
return None
|
||||||
|
|
||||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||||
if command is None:
|
if command is None:
|
||||||
@@ -120,12 +124,13 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
"""
|
"""
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Enter the command to be run when the user returns to a value below "
|
"Enter the command to be run when the user **returns to a value below "
|
||||||
"the points for this action to occur. Please note that this is "
|
"the points for this action to occur.** Please note that this is "
|
||||||
"intended to be used for reversal of the action taken when the user "
|
"intended to be used for reversal of the action taken when the user "
|
||||||
"exceeded the action's point value\nEnter it exactly as you would "
|
"exceeded the action's point value.\n**If you do not wish to have a command run "
|
||||||
|
"on dropping points, enter** `none`.\n\nEnter it exactly as you would "
|
||||||
"if you were actually trying to run the command, except don't put a prefix "
|
"if you were actually trying to run the command, except don't put a prefix "
|
||||||
"and use {user} in place of any user/member arguments\n\n"
|
"and use `{user}` in place of any user/member arguments\n\n"
|
||||||
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
"WARNING: The command entered will be run without regard to checks or cooldowns. "
|
||||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||||
"Please wait 15 seconds before entering your response."
|
"Please wait 15 seconds before entering your response."
|
||||||
@@ -141,9 +146,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
if msg.content == "none":
|
||||||
|
return None
|
||||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||||
if command is None:
|
if command is None:
|
||||||
await ctx.send(m)
|
await ctx.send(m)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.mod import is_admin_or_superior
|
from redbot.core.utils.mod import is_admin_or_superior
|
||||||
from redbot.core.utils.chat_formatting import warning, pagify
|
from redbot.core.utils.chat_formatting import warning, pagify
|
||||||
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
|
||||||
_ = Translator("Warnings", __file__)
|
_ = Translator("Warnings", __file__)
|
||||||
|
|
||||||
@@ -46,17 +47,16 @@ class Warnings:
|
|||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warningset(self, ctx: commands.Context):
|
async def warningset(self, ctx: commands.Context):
|
||||||
"""Warning settings"""
|
"""Warning settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@warningset.command()
|
@warningset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
|
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
|
||||||
"""Allow or disallow custom reasons for a warning"""
|
"""Enable or Disable custom reasons for a warning"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Custom reasons have been {}").format(_("enabled") if allowed else _("disabled"))
|
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@@ -64,8 +64,7 @@ class Warnings:
|
|||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnaction(self, ctx: commands.Context):
|
async def warnaction(self, ctx: commands.Context):
|
||||||
"""Action management"""
|
"""Action management"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@warnaction.command(name="add")
|
@warnaction.command(name="add")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -76,27 +75,9 @@ class Warnings:
|
|||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await ctx.send("Would you like to enter commands to be run? (y/n)")
|
exceed_command = await get_command_for_exceeded_points(ctx)
|
||||||
|
drop_command = await get_command_for_dropping_points(ctx)
|
||||||
|
|
||||||
def same_author_check(m):
|
|
||||||
return m.author == ctx.author
|
|
||||||
|
|
||||||
try:
|
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await ctx.send(_("Ok then"))
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg.content.lower() == "y":
|
|
||||||
exceed_command = await get_command_for_exceeded_points(ctx)
|
|
||||||
if exceed_command is None:
|
|
||||||
return
|
|
||||||
drop_command = await get_command_for_dropping_points(ctx)
|
|
||||||
if drop_command is None:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
exceed_command = None
|
|
||||||
drop_command = None
|
|
||||||
to_add = {
|
to_add = {
|
||||||
"action_name": name,
|
"action_name": name,
|
||||||
"points": points,
|
"points": points,
|
||||||
@@ -116,7 +97,7 @@ class Warnings:
|
|||||||
# Sort in descending order by point count for ease in
|
# Sort in descending order by point count for ease in
|
||||||
# finding the highest possible action to take
|
# finding the highest possible action to take
|
||||||
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
||||||
await ctx.tick()
|
await ctx.send(_("Action {name} has been added.").format(name=name))
|
||||||
|
|
||||||
@warnaction.command(name="del")
|
@warnaction.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -141,8 +122,7 @@ class Warnings:
|
|||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnreason(self, ctx: commands.Context):
|
async def warnreason(self, ctx: commands.Context):
|
||||||
"""Add reasons for warnings"""
|
"""Add reasons for warnings"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@warnreason.command(name="add")
|
@warnreason.command(name="add")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -161,7 +141,7 @@ class Warnings:
|
|||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
registered_reasons.update(completed)
|
registered_reasons.update(completed)
|
||||||
|
|
||||||
await ctx.send(_("That reason has been registered"))
|
await ctx.send(_("That reason has been registered."))
|
||||||
|
|
||||||
@warnreason.command(name="del")
|
@warnreason.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -173,7 +153,7 @@ class Warnings:
|
|||||||
if registered_reasons.pop(reason_name.lower(), None):
|
if registered_reasons.pop(reason_name.lower(), None):
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That is not a registered reason name"))
|
await ctx.send(_("That is not a registered reason name."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -185,13 +165,20 @@ class Warnings:
|
|||||||
msg_list = []
|
msg_list = []
|
||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
for r, v in registered_reasons.items():
|
for r, v in registered_reasons.items():
|
||||||
msg_list.append(
|
if ctx.embed_requested():
|
||||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
em = discord.Embed(
|
||||||
r, v["points"], v["description"]
|
title=_("Reason: {name}").format(name=r), description=v["description"]
|
||||||
|
)
|
||||||
|
em.add_field(name=_("Points"), value=str(v["points"]))
|
||||||
|
msg_list.append(em)
|
||||||
|
else:
|
||||||
|
msg_list.append(
|
||||||
|
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||||
|
r, v["points"], v["description"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await ctx.send_interactive(msg_list)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no reasons configured!"))
|
await ctx.send(_("There are no reasons configured!"))
|
||||||
|
|
||||||
@@ -205,14 +192,21 @@ class Warnings:
|
|||||||
msg_list = []
|
msg_list = []
|
||||||
async with guild_settings.actions() as registered_actions:
|
async with guild_settings.actions() as registered_actions:
|
||||||
for r in registered_actions:
|
for r in registered_actions:
|
||||||
msg_list.append(
|
if await ctx.embed_requested():
|
||||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
em = discord.Embed(title=_("Action: {name}").format(name=r["action_name"]))
|
||||||
"Drop command: {}".format(
|
em.add_field(name=_("Points"), value="{}".format(r["points"]), inline=False)
|
||||||
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
em.add_field(name=_("Exceed command"), value=r["exceed_command"], inline=False)
|
||||||
|
em.add_field(name=_("Drop command"), value=r["drop_command"], inline=False)
|
||||||
|
msg_list.append(em)
|
||||||
|
else:
|
||||||
|
msg_list.append(
|
||||||
|
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||||
|
"Drop command: {}".format(
|
||||||
|
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await ctx.send_interactive(msg_list)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There are no actions configured!"))
|
await ctx.send(_("There are no actions configured!"))
|
||||||
|
|
||||||
@@ -224,13 +218,16 @@ class Warnings:
|
|||||||
|
|
||||||
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
||||||
"""
|
"""
|
||||||
|
if user == ctx.author:
|
||||||
|
await ctx.send(_("You cannot warn yourself."))
|
||||||
|
return
|
||||||
if reason.lower() == "custom":
|
if reason.lower() == "custom":
|
||||||
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
||||||
if not custom_allowed:
|
if not custom_allowed:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Custom reasons are not allowed! Please see {} for "
|
"Custom reasons are not allowed! Please see {} for "
|
||||||
"a complete list of valid reasons"
|
"a complete list of valid reasons."
|
||||||
).format("`{}reasonlist`".format(ctx.prefix))
|
).format("`{}reasonlist`".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -259,7 +256,27 @@ class Warnings:
|
|||||||
await member_settings.total_points.set(current_point_count)
|
await member_settings.total_points.set(current_point_count)
|
||||||
|
|
||||||
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
||||||
await ctx.tick()
|
try:
|
||||||
|
em = discord.Embed(
|
||||||
|
title=_("Warning from {mod_name}#{mod_discrim}").format(
|
||||||
|
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
|
||||||
|
),
|
||||||
|
description=reason_type["description"],
|
||||||
|
)
|
||||||
|
em.add_field(name=_("Points"), value=str(reason_type["points"]))
|
||||||
|
await user.send(
|
||||||
|
_("You have received a warning in {guild_name}.").format(
|
||||||
|
guild_name=ctx.guild.name
|
||||||
|
),
|
||||||
|
embed=em,
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await ctx.send(
|
||||||
|
_("User {user_name}#{user_discrim} has been warned.").format(
|
||||||
|
user_name=user.display_name, user_discrim=user.discriminator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -275,7 +292,7 @@ class Warnings:
|
|||||||
else:
|
else:
|
||||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
warning(_("You are not allowed to check " "warnings for other users!"))
|
warning(_("You are not allowed to check warnings for other users!"))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -306,6 +323,9 @@ class Warnings:
|
|||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||||
"""Removes the specified warning from the user specified"""
|
"""Removes the specified warning from the user specified"""
|
||||||
|
if user_id == ctx.author.id:
|
||||||
|
await ctx.send(_("You cannot remove warnings from yourself."))
|
||||||
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
member = guild.get_member(user_id)
|
member = guild.get_member(user_id)
|
||||||
if member is None: # no longer in guild, but need a "member" object
|
if member is None: # no longer in guild, but need a "member" object
|
||||||
@@ -336,7 +356,7 @@ class Warnings:
|
|||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then"))
|
await ctx.send(_("Ok then."))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
int(msg.content)
|
int(msg.content)
|
||||||
@@ -349,11 +369,11 @@ class Warnings:
|
|||||||
return
|
return
|
||||||
to_add["points"] = int(msg.content)
|
to_add["points"] = int(msg.content)
|
||||||
|
|
||||||
await ctx.send(_("Enter a description for this reason"))
|
await ctx.send(_("Enter a description for this reason."))
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then"))
|
await ctx.send(_("Ok then."))
|
||||||
return
|
return
|
||||||
to_add["description"] = msg.content
|
to_add["description"] = msg.content
|
||||||
return to_add
|
return to_add
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ __all__ = ["Config", "__version__"]
|
|||||||
|
|
||||||
|
|
||||||
class VersionInfo:
|
class VersionInfo:
|
||||||
|
|
||||||
def __init__(self, major, minor, micro, releaselevel, serial):
|
def __init__(self, major, minor, micro, releaselevel, serial):
|
||||||
self._levels = ["alpha", "beta", "final"]
|
self._levels = ["alpha", "beta", "final"]
|
||||||
self.major = major
|
self.major = major
|
||||||
@@ -37,5 +36,5 @@ class VersionInfo:
|
|||||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.0.0b15"
|
__version__ = "3.0.0b17"
|
||||||
version_info = VersionInfo(3, 0, 0, "beta", 15)
|
version_info = VersionInfo(3, 0, 0, "beta", 17)
|
||||||
|
|||||||
@@ -508,9 +508,7 @@ async def set_bank_name(name: str, guild: discord.Guild = None) -> str:
|
|||||||
elif guild is not None:
|
elif guild is not None:
|
||||||
await _conf.guild(guild).bank_name.set(name)
|
await _conf.guild(guild).bank_name.set(name)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError("Guild must be provided if setting the name of a guild-specific bank.")
|
||||||
"Guild must be provided if setting the name of a guild" "-specific bank."
|
|
||||||
)
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
@@ -570,7 +568,7 @@ async def set_currency_name(name: str, guild: discord.Guild = None) -> str:
|
|||||||
await _conf.guild(guild).currency.set(name)
|
await _conf.guild(guild).currency.set(name)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Guild must be provided if setting the currency" " name of a guild-specific bank."
|
"Guild must be provided if setting the currency name of a guild-specific bank."
|
||||||
)
|
)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ from discord.voice_client import VoiceClient
|
|||||||
VoiceClient.warn_nacl = False
|
VoiceClient.warn_nacl = False
|
||||||
|
|
||||||
from .cog_manager import CogManager
|
from .cog_manager import CogManager
|
||||||
from . import Config, i18n, commands, rpc
|
from . import Config, i18n, commands
|
||||||
|
from .rpc import RPCMixin
|
||||||
from .help_formatter import Help, help as help_
|
from .help_formatter import Help, help as help_
|
||||||
from .sentry import SentryManager
|
from .sentry import SentryManager
|
||||||
|
|
||||||
|
|
||||||
class RedBase(BotBase):
|
class RedBase(BotBase, RPCMixin):
|
||||||
"""Mixin for the main bot class.
|
"""Mixin for the main bot class.
|
||||||
|
|
||||||
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
||||||
@@ -33,7 +34,7 @@ class RedBase(BotBase):
|
|||||||
Selfbots should inherit from this mixin along with `discord.Client`.
|
Selfbots should inherit from this mixin along with `discord.Client`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cli_flags, bot_dir: Path = Path.cwd(), **kwargs):
|
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
|
||||||
self._shutdown_mode = ExitCodes.CRITICAL
|
self._shutdown_mode = ExitCodes.CRITICAL
|
||||||
self.db = Config.get_core_conf(force_registration=True)
|
self.db = Config.get_core_conf(force_registration=True)
|
||||||
self._co_owners = cli_flags.co_owner
|
self._co_owners = cli_flags.co_owner
|
||||||
@@ -50,6 +51,7 @@ class RedBase(BotBase):
|
|||||||
locale="en",
|
locale="en",
|
||||||
embeds=True,
|
embeds=True,
|
||||||
color=15158332,
|
color=15158332,
|
||||||
|
fuzzy=False,
|
||||||
help__page_char_limit=1000,
|
help__page_char_limit=1000,
|
||||||
help__max_pages_in_guild=2,
|
help__max_pages_in_guild=2,
|
||||||
help__tagline="",
|
help__tagline="",
|
||||||
@@ -63,6 +65,7 @@ class RedBase(BotBase):
|
|||||||
mod_role=None,
|
mod_role=None,
|
||||||
embeds=None,
|
embeds=None,
|
||||||
use_bot_color=False,
|
use_bot_color=False,
|
||||||
|
fuzzy=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.register_user(embeds=None)
|
self.db.register_user(embeds=None)
|
||||||
@@ -99,16 +102,13 @@ class RedBase(BotBase):
|
|||||||
|
|
||||||
self.counter = Counter()
|
self.counter = Counter()
|
||||||
self.uptime = None
|
self.uptime = None
|
||||||
self.color = None
|
self.color = discord.Embed.Empty # This is needed or color ends up 0x000000
|
||||||
|
|
||||||
self.main_dir = bot_dir
|
self.main_dir = bot_dir
|
||||||
|
|
||||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||||
|
|
||||||
super().__init__(formatter=Help(), **kwargs)
|
super().__init__(*args, formatter=Help(), **kwargs)
|
||||||
|
|
||||||
if self.rpc_enabled:
|
|
||||||
self.rpc = rpc.RPC(self)
|
|
||||||
|
|
||||||
self.remove_command("help")
|
self.remove_command("help")
|
||||||
|
|
||||||
@@ -233,12 +233,24 @@ class RedBase(BotBase):
|
|||||||
lib_name = lib.__name__ # Thank you
|
lib_name = lib.__name__ # Thank you
|
||||||
|
|
||||||
# find all references to the module
|
# find all references to the module
|
||||||
|
cog_names = []
|
||||||
|
|
||||||
# remove the cogs registered from the module
|
# remove the cogs registered from the module
|
||||||
for cogname, cog in self.cogs.copy().items():
|
for cogname, cog in self.cogs.copy().items():
|
||||||
if cog.__module__.startswith(lib_name):
|
if cog.__module__.startswith(lib_name):
|
||||||
self.remove_cog(cogname)
|
self.remove_cog(cogname)
|
||||||
|
|
||||||
|
cog_names.append(cogname)
|
||||||
|
|
||||||
|
# remove all rpc handlers
|
||||||
|
for cogname in cog_names:
|
||||||
|
if cogname.upper() in self.rpc_handlers:
|
||||||
|
methods = self.rpc_handlers[cogname]
|
||||||
|
for meth in methods:
|
||||||
|
self.unregister_rpc_handler(meth)
|
||||||
|
|
||||||
|
del self.rpc_handlers[cogname]
|
||||||
|
|
||||||
# first remove all the commands from the module
|
# first remove all the commands from the module
|
||||||
for cmd in self.all_commands.copy().values():
|
for cmd in self.all_commands.copy().values():
|
||||||
if cmd.module.startswith(lib_name):
|
if cmd.module.startswith(lib_name):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
|
|
||||||
async def check_overrides(ctx, *, level):
|
async def check_overrides(ctx, *, level):
|
||||||
@@ -16,7 +16,6 @@ async def check_overrides(ctx, *, level):
|
|||||||
|
|
||||||
|
|
||||||
def is_owner(**kwargs):
|
def is_owner(**kwargs):
|
||||||
|
|
||||||
async def check(ctx):
|
async def check(ctx):
|
||||||
override = await check_overrides(ctx, level="owner")
|
override = await check_overrides(ctx, level="owner")
|
||||||
return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs)
|
return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||||
@@ -73,7 +72,6 @@ async def is_admin_or_superior(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def mod_or_permissions(**perms):
|
def mod_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
override = await check_overrides(ctx, level="mod")
|
override = await check_overrides(ctx, level="mod")
|
||||||
return (
|
return (
|
||||||
@@ -86,7 +84,6 @@ def mod_or_permissions(**perms):
|
|||||||
|
|
||||||
|
|
||||||
def admin_or_permissions(**perms):
|
def admin_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
override = await check_overrides(ctx, level="admin")
|
override = await check_overrides(ctx, level="admin")
|
||||||
return (
|
return (
|
||||||
@@ -99,7 +96,6 @@ def admin_or_permissions(**perms):
|
|||||||
|
|
||||||
|
|
||||||
def bot_in_a_guild(**kwargs):
|
def bot_in_a_guild(**kwargs):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
return len(ctx.bot.guilds) > 0
|
return len(ctx.bot.guilds) > 0
|
||||||
|
|
||||||
@@ -107,7 +103,6 @@ def bot_in_a_guild(**kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def guildowner_or_permissions(**perms):
|
def guildowner_or_permissions(**perms):
|
||||||
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||||
if ctx.guild is None:
|
if ctx.guild is None:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def interactive_config(red, token_set, prefix_set):
|
|||||||
while not prefix:
|
while not prefix:
|
||||||
prefix = input("Prefix> ")
|
prefix = input("Prefix> ")
|
||||||
if len(prefix) > 10:
|
if len(prefix) > 10:
|
||||||
print("Your prefix seems overly long. Are you sure it " "is correct? (y/n)")
|
print("Your prefix seems overly long. Are you sure that it's correct? (y/n)")
|
||||||
if not confirm("> "):
|
if not confirm("> "):
|
||||||
prefix = ""
|
prefix = ""
|
||||||
if prefix:
|
if prefix:
|
||||||
@@ -72,7 +72,7 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--list-instances",
|
"--list-instances",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="List all instance names setup " "with 'redbot-setup'",
|
help="List all instance names setup with 'redbot-setup'",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--owner",
|
"--owner",
|
||||||
@@ -117,7 +117,7 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--not-bot",
|
"--not-bot",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Specifies if the token used belongs to a bot " "account.",
|
help="Specifies if the token used belongs to a bot account.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
@@ -131,12 +131,22 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mentionable",
|
"--mentionable",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Allows mentioning the bot as an alternative " "to using the bot prefix",
|
help="Allows mentioning the bot as an alternative to using the bot prefix",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--rpc",
|
"--rpc",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enables the built-in RPC server. Please read the docs" "prior to enabling this!",
|
help="Enables the built-in RPC server. Please read the docs prior to enabling this!",
|
||||||
|
)
|
||||||
|
parser.add_argument("--token", type=str, help="Run Red with the given token.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-instance",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Run Red without any existing instance. "
|
||||||
|
"The data will be saved under a temporary folder "
|
||||||
|
"and deleted on next system restart."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class CogManager:
|
|||||||
mod = import_module(real_name, package="redbot.cogs")
|
mod = import_module(real_name, package="redbot.cogs")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No core cog by the name of '{}' could" "be found.".format(name)
|
"No core cog by the name of '{}' could be found.".format(name)
|
||||||
) from e
|
) from e
|
||||||
return mod.__spec__
|
return mod.__spec__
|
||||||
|
|
||||||
@@ -342,9 +342,7 @@ class CogManagerUI:
|
|||||||
Add a path to the list of available cog paths.
|
Add a path to the list of available cog paths.
|
||||||
"""
|
"""
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
await ctx.send(
|
await ctx.send(_("That path does not exist or does not point to a valid directory."))
|
||||||
_("That path does not exist or does not" " point to a valid directory.")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -419,7 +417,7 @@ class CogManagerUI:
|
|||||||
|
|
||||||
install_path = await ctx.bot.cog_mgr.install_path()
|
install_path = await ctx.bot.cog_mgr.install_path()
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The bot will install new cogs to the `{}`" " directory.").format(install_path)
|
_("The bot will install new cogs to the `{}` directory.").format(install_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
from discord.ext.commands import *
|
from discord.ext.commands import *
|
||||||
from .commands import *
|
from .commands import *
|
||||||
from .context import *
|
from .context import *
|
||||||
|
from .errors import *
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ This module contains extended classes and functions which are intended to
|
|||||||
replace those from the `discord.ext.commands` module.
|
replace those from the `discord.ext.commands` module.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from .errors import ConversionFailure
|
||||||
|
from ..i18n import Translator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
__all__ = ["Command", "Group", "command", "group"]
|
__all__ = ["Command", "Group", "command", "group"]
|
||||||
|
|
||||||
|
_ = Translator("commands.commands", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Command(commands.Command):
|
class Command(commands.Command):
|
||||||
"""Command class for Red.
|
"""Command class for Red.
|
||||||
@@ -48,6 +56,78 @@ class Command(commands.Command):
|
|||||||
# We don't want our help property to be overwritten, namely by super()
|
# We don't want our help property to be overwritten, namely by super()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parents(self):
|
||||||
|
"""
|
||||||
|
Returns all parent commands of this command.
|
||||||
|
|
||||||
|
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
||||||
|
If the command has no parents, this will be an empty list.
|
||||||
|
"""
|
||||||
|
cmd = self.parent
|
||||||
|
entries = []
|
||||||
|
while cmd is not None:
|
||||||
|
entries.append(cmd)
|
||||||
|
cmd = cmd.parent
|
||||||
|
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
||||||
|
|
||||||
|
async def do_conversion(self, ctx: "Context", converter, argument: str):
|
||||||
|
"""Convert an argument according to its type annotation.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ConversionFailure
|
||||||
|
If doing the conversion failed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Any
|
||||||
|
The converted argument.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Let's not worry about all of this junk if it's just a str converter
|
||||||
|
if converter is str:
|
||||||
|
return argument
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await super().do_conversion(ctx, converter, argument)
|
||||||
|
except commands.BadArgument as exc:
|
||||||
|
raise ConversionFailure(converter, argument, *exc.args) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
# Some common converters need special treatment...
|
||||||
|
if converter in (int, float):
|
||||||
|
message = _('"{argument}" is not a number.').format(argument=argument)
|
||||||
|
raise ConversionFailure(converter, argument, message) from exc
|
||||||
|
|
||||||
|
# We should expose anything which might be a bug in the converter
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
def command(self, cls=None, *args, **kwargs):
|
||||||
|
"""A shortcut decorator that invokes :func:`.command` and adds it to
|
||||||
|
the internal command list via :meth:`~.GroupMixin.add_command`.
|
||||||
|
"""
|
||||||
|
cls = cls or self.__class__
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
result = command(*args, **kwargs)(func)
|
||||||
|
self.add_command(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def group(self, cls=None, *args, **kwargs):
|
||||||
|
"""A shortcut decorator that invokes :func:`.group` and adds it to
|
||||||
|
the internal command list via :meth:`~.GroupMixin.add_command`.
|
||||||
|
"""
|
||||||
|
cls = None or Group
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
result = group(*args, **kwargs)(func)
|
||||||
|
self.add_command(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Group(Command, commands.Group):
|
class Group(Command, commands.Group):
|
||||||
"""Group command class for Red.
|
"""Group command class for Red.
|
||||||
@@ -55,7 +135,28 @@ class Group(Command, commands.Group):
|
|||||||
This class inherits from `discord.ext.commands.Group`, with `Command` mixed
|
This class inherits from `discord.ext.commands.Group`, with `Command` mixed
|
||||||
in.
|
in.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.autohelp = kwargs.pop("autohelp", True)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def invoke(self, ctx):
|
||||||
|
view = ctx.view
|
||||||
|
previous = view.index
|
||||||
|
view.skip_ws()
|
||||||
|
trigger = view.get_word()
|
||||||
|
if trigger:
|
||||||
|
ctx.subcommand_passed = trigger
|
||||||
|
ctx.invoked_subcommand = self.all_commands.get(trigger, None)
|
||||||
|
view.index = previous
|
||||||
|
view.previous = previous
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
||||||
|
if self.autohelp and not self.invoke_without_command:
|
||||||
|
await self._verify_checks(ctx)
|
||||||
|
await ctx.send_help()
|
||||||
|
|
||||||
|
await super().invoke(ctx)
|
||||||
|
|
||||||
|
|
||||||
# decorators
|
# decorators
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ class Context(commands.Context):
|
|||||||
else:
|
else:
|
||||||
return self.bot.color
|
return self.bot.color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embed_color(self):
|
||||||
|
# Rather than double awaiting.
|
||||||
|
return self.embed_colour
|
||||||
|
|
||||||
async def embed_requested(self):
|
async def embed_requested(self):
|
||||||
"""
|
"""
|
||||||
Simple helper to call bot.embed_requested
|
Simple helper to call bot.embed_requested
|
||||||
|
|||||||
13
redbot/core/commands/errors.py
Normal file
13
redbot/core/commands/errors.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""Errors module for the commands package."""
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
__all__ = ["ConversionFailure"]
|
||||||
|
|
||||||
|
|
||||||
|
class ConversionFailure(commands.BadArgument):
|
||||||
|
"""Raised when converting an argument fails."""
|
||||||
|
|
||||||
|
def __init__(self, converter, argument: str, *args):
|
||||||
|
self.converter = converter
|
||||||
|
self.argument = argument
|
||||||
|
super().__init__(*args)
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import collections
|
import collections
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Union, Tuple
|
from typing import Union, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from .data_manager import cog_data_path, core_data_path
|
from .data_manager import cog_data_path, core_data_path
|
||||||
from .drivers import get_driver
|
from .drivers import get_driver
|
||||||
|
|
||||||
from .utils import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .drivers.red_base import BaseDriver
|
from .drivers.red_base import BaseDriver
|
||||||
|
|
||||||
@@ -31,12 +29,15 @@ class _ValueCtxManager:
|
|||||||
def __init__(self, value_obj, coro):
|
def __init__(self, value_obj, coro):
|
||||||
self.value_obj = value_obj
|
self.value_obj = value_obj
|
||||||
self.coro = coro
|
self.coro = coro
|
||||||
|
self.raw_value = None
|
||||||
|
self.__original_value = None
|
||||||
|
|
||||||
def __await__(self):
|
def __await__(self):
|
||||||
return self.coro.__await__()
|
return self.coro.__await__()
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self.raw_value = await self
|
self.raw_value = await self
|
||||||
|
self.__original_value = deepcopy(self.raw_value)
|
||||||
if not isinstance(self.raw_value, (list, dict)):
|
if not isinstance(self.raw_value, (list, dict)):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Type of retrieved value must be mutable (i.e. "
|
"Type of retrieved value must be mutable (i.e. "
|
||||||
@@ -46,7 +47,8 @@ class _ValueCtxManager:
|
|||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
async def __aexit__(self, *exc_info):
|
async def __aexit__(self, *exc_info):
|
||||||
await self.value_obj.set(self.raw_value)
|
if self.raw_value != self.__original_value:
|
||||||
|
await self.value_obj.set(self.raw_value)
|
||||||
|
|
||||||
|
|
||||||
class Value:
|
class Value:
|
||||||
@@ -225,7 +227,7 @@ class Group(Value):
|
|||||||
identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
|
identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
|
||||||
)
|
)
|
||||||
elif self.force_registration:
|
elif self.force_registration:
|
||||||
raise AttributeError("'{}' is not a valid registered Group " "or value.".format(item))
|
raise AttributeError("'{}' is not a valid registered Group or value.".format(item))
|
||||||
else:
|
else:
|
||||||
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
||||||
|
|
||||||
@@ -337,7 +339,7 @@ class Group(Value):
|
|||||||
default = poss_default
|
default = poss_default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return deepcopy(await self.driver.get(*self.identifiers, *path))
|
return await self.driver.get(*self.identifiers, *path)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default is not ...:
|
if default is not ...:
|
||||||
return default
|
return default
|
||||||
@@ -367,7 +369,7 @@ class Group(Value):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not defaults:
|
if not defaults:
|
||||||
defaults = deepcopy(self.defaults)
|
defaults = self.defaults
|
||||||
|
|
||||||
for key, value in current.items():
|
for key, value in current.items():
|
||||||
if isinstance(value, collections.Mapping):
|
if isinstance(value, collections.Mapping):
|
||||||
@@ -394,7 +396,7 @@ class Group(Value):
|
|||||||
# is equivalent to
|
# is equivalent to
|
||||||
|
|
||||||
data = {"foo": {"bar": None}}
|
data = {"foo": {"bar": None}}
|
||||||
d["foo"]["bar"] = "baz"
|
data["foo"]["bar"] = "baz"
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -441,6 +443,7 @@ class Config:
|
|||||||
attempting to access data.
|
attempting to access data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GLOBAL = "GLOBAL"
|
GLOBAL = "GLOBAL"
|
||||||
GUILD = "GUILD"
|
GUILD = "GUILD"
|
||||||
CHANNEL = "TEXTCHANNEL"
|
CHANNEL = "TEXTCHANNEL"
|
||||||
@@ -624,9 +627,7 @@ class Config:
|
|||||||
existing_is_dict = isinstance(_partial[k], dict)
|
existing_is_dict = isinstance(_partial[k], dict)
|
||||||
if val_is_dict != existing_is_dict:
|
if val_is_dict != existing_is_dict:
|
||||||
# != is XOR
|
# != is XOR
|
||||||
raise KeyError(
|
raise KeyError("You cannot register a Group and a Value under the same name.")
|
||||||
"You cannot register a Group and a Value under" " the same name."
|
|
||||||
)
|
|
||||||
if val_is_dict:
|
if val_is_dict:
|
||||||
Config._update_defaults(v, _partial=_partial[k])
|
Config._update_defaults(v, _partial=_partial[k])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
|||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
@@ -21,9 +22,7 @@ import pkg_resources
|
|||||||
from redbot.core import __version__
|
from redbot.core import __version__
|
||||||
from redbot.core import checks
|
from redbot.core import checks
|
||||||
from redbot.core import i18n
|
from redbot.core import i18n
|
||||||
from redbot.core import rpc
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from .utils import TYPE_CHECKING
|
|
||||||
from .utils.chat_formatting import pagify, box, inline
|
from .utils.chat_formatting import pagify, box, inline
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -44,12 +43,201 @@ OWNER_DISCLAIMER = (
|
|||||||
_ = i18n.Translator("Core", __file__)
|
_ = i18n.Translator("Core", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class CoreLogic:
|
||||||
|
def __init__(self, bot: "Red"):
|
||||||
|
self.bot = bot
|
||||||
|
self.bot.register_rpc_handler(self._load)
|
||||||
|
self.bot.register_rpc_handler(self._unload)
|
||||||
|
self.bot.register_rpc_handler(self._reload)
|
||||||
|
self.bot.register_rpc_handler(self._name)
|
||||||
|
self.bot.register_rpc_handler(self._prefixes)
|
||||||
|
self.bot.register_rpc_handler(self._version_info)
|
||||||
|
self.bot.register_rpc_handler(self._invite_url)
|
||||||
|
|
||||||
|
async def _load(self, cog_names: list):
|
||||||
|
"""
|
||||||
|
Loads cogs by name.
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cog_names : list of str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
3 element tuple of loaded, failed, and not found cogs.
|
||||||
|
"""
|
||||||
|
failed_packages = []
|
||||||
|
loaded_packages = []
|
||||||
|
notfound_packages = []
|
||||||
|
|
||||||
|
bot = self.bot
|
||||||
|
|
||||||
|
cogspecs = []
|
||||||
|
|
||||||
|
for name in cog_names:
|
||||||
|
try:
|
||||||
|
spec = await bot.cog_mgr.find_cog(name)
|
||||||
|
cogspecs.append((spec, name))
|
||||||
|
except RuntimeError:
|
||||||
|
notfound_packages.append(name)
|
||||||
|
|
||||||
|
for spec, name in cogspecs:
|
||||||
|
try:
|
||||||
|
self._cleanup_and_refresh_modules(spec.name)
|
||||||
|
await bot.load_extension(spec)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Package loading failed", exc_info=e)
|
||||||
|
|
||||||
|
exception_log = "Exception during loading of cog\n"
|
||||||
|
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||||
|
bot._last_exception = exception_log
|
||||||
|
failed_packages.append(name)
|
||||||
|
else:
|
||||||
|
await bot.add_loaded_package(name)
|
||||||
|
loaded_packages.append(name)
|
||||||
|
return loaded_packages, failed_packages, notfound_packages
|
||||||
|
|
||||||
|
def _cleanup_and_refresh_modules(self, module_name: str):
|
||||||
|
"""Interally reloads modules so that changes are detected"""
|
||||||
|
splitted = module_name.split(".")
|
||||||
|
|
||||||
|
def maybe_reload(new_name):
|
||||||
|
try:
|
||||||
|
lib = sys.modules[new_name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||||
|
|
||||||
|
modules = itertools.accumulate(splitted, "{}.{}".format)
|
||||||
|
for m in modules:
|
||||||
|
maybe_reload(m)
|
||||||
|
|
||||||
|
children = {name: lib for name, lib in sys.modules.items() if name.startswith(module_name)}
|
||||||
|
for child_name, lib in children.items():
|
||||||
|
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||||
|
|
||||||
|
def _get_package_strings(self, packages: list, fmt: str, other: tuple = None):
|
||||||
|
"""
|
||||||
|
Gets the strings needed for the load, unload and reload commands
|
||||||
|
"""
|
||||||
|
packages = [inline(name) for name in packages]
|
||||||
|
|
||||||
|
if other is None:
|
||||||
|
other = ("", "")
|
||||||
|
plural = "s" if len(packages) > 1 else ""
|
||||||
|
use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
|
||||||
|
packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
|
||||||
|
|
||||||
|
form = {"plural": plural, "packs": packages_string, "other": other}
|
||||||
|
final_string = fmt.format(**form)
|
||||||
|
return final_string
|
||||||
|
|
||||||
|
async def _unload(self, cog_names: list):
|
||||||
|
"""
|
||||||
|
Unloads cogs with the given names.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cog_names : list of str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2 element tuple of successful unloads and failed unloads.
|
||||||
|
"""
|
||||||
|
failed_packages = []
|
||||||
|
unloaded_packages = []
|
||||||
|
|
||||||
|
bot = self.bot
|
||||||
|
|
||||||
|
for name in cog_names:
|
||||||
|
if name in bot.extensions:
|
||||||
|
bot.unload_extension(name)
|
||||||
|
await bot.remove_loaded_package(name)
|
||||||
|
unloaded_packages.append(name)
|
||||||
|
else:
|
||||||
|
failed_packages.append(name)
|
||||||
|
|
||||||
|
return unloaded_packages, failed_packages
|
||||||
|
|
||||||
|
async def _reload(self, cog_names):
|
||||||
|
await self._unload(cog_names)
|
||||||
|
|
||||||
|
loaded, load_failed, not_found = await self._load(cog_names)
|
||||||
|
|
||||||
|
return loaded, load_failed, not_found
|
||||||
|
|
||||||
|
async def _name(self, name: str = None):
|
||||||
|
"""
|
||||||
|
Gets or sets the bot's username.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
If passed, the bot will change it's username.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The current (or new) username of the bot.
|
||||||
|
"""
|
||||||
|
if name is not None:
|
||||||
|
await self.bot.user.edit(username=name)
|
||||||
|
|
||||||
|
return self.bot.user.name
|
||||||
|
|
||||||
|
async def _prefixes(self, prefixes: list = None):
|
||||||
|
"""
|
||||||
|
Gets or sets the bot's global prefixes.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
prefixes : list of str
|
||||||
|
If passed, the bot will set it's global prefixes.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list of str
|
||||||
|
The current (or new) list of prefixes.
|
||||||
|
"""
|
||||||
|
if prefixes:
|
||||||
|
prefixes = sorted(prefixes, reverse=True)
|
||||||
|
await self.bot.db.prefix.set(prefixes)
|
||||||
|
return await self.bot.db.prefix()
|
||||||
|
|
||||||
|
async def _version_info(self):
|
||||||
|
"""
|
||||||
|
Version information for Red and discord.py
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
`redbot` and `discordpy` keys containing version information for both.
|
||||||
|
"""
|
||||||
|
return {"redbot": __version__, "discordpy": discord.__version__}
|
||||||
|
|
||||||
|
async def _invite_url(self):
|
||||||
|
"""
|
||||||
|
Generates the invite URL for the bot.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Invite URL.
|
||||||
|
"""
|
||||||
|
if self.bot.user.bot:
|
||||||
|
app_info = await self.bot.application_info()
|
||||||
|
return discord.utils.oauth_url(app_info.id)
|
||||||
|
return "Not a bot account!"
|
||||||
|
|
||||||
|
|
||||||
@i18n.cog_i18n(_)
|
@i18n.cog_i18n(_)
|
||||||
class Core:
|
class Core(CoreLogic):
|
||||||
"""Commands related to core functions"""
|
"""Commands related to core functions"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot # type: Red
|
super().__init__(bot)
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
@commands.command(hidden=True)
|
||||||
async def ping(self, ctx):
|
async def ping(self, ctx):
|
||||||
@@ -87,7 +275,7 @@ class Core:
|
|||||||
"".format(red_repo, author_repo, org_repo, support_server_url)
|
"".format(red_repo, author_repo, org_repo, support_server_url)
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = discord.Embed(color=discord.Color.red())
|
embed = discord.Embed(color=(await ctx.embed_colour()))
|
||||||
embed.add_field(name="Instance owned by", value=str(owner))
|
embed.add_field(name="Instance owned by", value=str(owner))
|
||||||
embed.add_field(name="Python", value=python_version)
|
embed.add_field(name="Python", value=python_version)
|
||||||
embed.add_field(name="discord.py", value=dpy_version)
|
embed.add_field(name="discord.py", value=dpy_version)
|
||||||
@@ -99,7 +287,7 @@ class Core:
|
|||||||
embed.add_field(name="About Red", value=about, inline=False)
|
embed.add_field(name="About Red", value=about, inline=False)
|
||||||
|
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text="Bringing joy since 02 Jan 2016 (over " "{} days ago!)".format(days_since)
|
text="Bringing joy since 02 Jan 2016 (over {} days ago!)".format(days_since)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
@@ -153,7 +341,6 @@ class Core:
|
|||||||
user_setting = await self.bot.db.user(ctx.author).embeds()
|
user_setting = await self.bot.db.user(ctx.author).embeds()
|
||||||
text += "User setting: {}".format(user_setting)
|
text += "User setting: {}".format(user_setting)
|
||||||
await ctx.send(box(text))
|
await ctx.send(box(text))
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@embedset.command(name="global")
|
@embedset.command(name="global")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -173,6 +360,7 @@ class Core:
|
|||||||
|
|
||||||
@embedset.command(name="guild")
|
@embedset.command(name="guild")
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
||||||
"""
|
"""
|
||||||
Toggle the guild's embed setting.
|
Toggle the guild's embed setting.
|
||||||
@@ -236,8 +424,7 @@ class Core:
|
|||||||
async def invite(self, ctx):
|
async def invite(self, ctx):
|
||||||
"""Show's Red's invite url"""
|
"""Show's Red's invite url"""
|
||||||
if self.bot.user.bot:
|
if self.bot.user.bot:
|
||||||
app_info = await self.bot.application_info()
|
await ctx.author.send(await self._invite_url())
|
||||||
await ctx.author.send(discord.utils.oauth_url(app_info.id))
|
|
||||||
else:
|
else:
|
||||||
await ctx.send("I'm not a bot account. I have no invite URL.")
|
await ctx.send("I'm not a bot account. I have no invite URL.")
|
||||||
|
|
||||||
@@ -249,7 +436,7 @@ class Core:
|
|||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
await ctx.send("Are you sure you want me to leave this server?" " Type yes to confirm.")
|
await ctx.send("Are you sure you want me to leave this server? Type yes to confirm.")
|
||||||
|
|
||||||
def conf_check(m):
|
def conf_check(m):
|
||||||
return m.author == author
|
return m.author == author
|
||||||
@@ -319,149 +506,70 @@ class Core:
|
|||||||
async def load(self, ctx, *, cog_name: str):
|
async def load(self, ctx, *, cog_name: str):
|
||||||
"""Loads packages"""
|
"""Loads packages"""
|
||||||
|
|
||||||
failed_packages = []
|
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||||
loaded_packages = []
|
async with ctx.typing():
|
||||||
notfound_packages = []
|
loaded, failed, not_found = await self._load(cog_names)
|
||||||
|
|
||||||
cognames = [c.strip() for c in cog_name.split(" ")]
|
if loaded:
|
||||||
cogspecs = []
|
|
||||||
|
|
||||||
for c in cognames:
|
|
||||||
try:
|
|
||||||
spec = await ctx.bot.cog_mgr.find_cog(c)
|
|
||||||
cogspecs.append((spec, c))
|
|
||||||
except RuntimeError:
|
|
||||||
notfound_packages.append(inline(c))
|
|
||||||
# await ctx.send(_("No module named '{}' was found in any"
|
|
||||||
# " cog path.").format(c))
|
|
||||||
|
|
||||||
if len(cogspecs) > 0:
|
|
||||||
for spec, name in cogspecs:
|
|
||||||
try:
|
|
||||||
await ctx.bot.load_extension(spec)
|
|
||||||
except Exception as e:
|
|
||||||
log.exception("Package loading failed", exc_info=e)
|
|
||||||
|
|
||||||
exception_log = "Exception in command '{}'\n" "".format(
|
|
||||||
ctx.command.qualified_name
|
|
||||||
)
|
|
||||||
exception_log += "".join(
|
|
||||||
traceback.format_exception(type(e), e, e.__traceback__)
|
|
||||||
)
|
|
||||||
self.bot._last_exception = exception_log
|
|
||||||
failed_packages.append(inline(name))
|
|
||||||
else:
|
|
||||||
await ctx.bot.add_loaded_package(name)
|
|
||||||
loaded_packages.append(inline(name))
|
|
||||||
|
|
||||||
if loaded_packages:
|
|
||||||
fmt = "Loaded {packs}"
|
fmt = "Loaded {packs}"
|
||||||
formed = self.get_package_strings(loaded_packages, fmt)
|
formed = self._get_package_strings(loaded, fmt)
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed_packages:
|
if failed:
|
||||||
fmt = (
|
fmt = (
|
||||||
"Failed to load package{plural} {packs}. Check your console or "
|
"Failed to load package{plural} {packs}. Check your console or "
|
||||||
"logs for details."
|
"logs for details."
|
||||||
)
|
)
|
||||||
formed = self.get_package_strings(failed_packages, fmt)
|
formed = self._get_package_strings(failed, fmt)
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
if notfound_packages:
|
if not_found:
|
||||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||||
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
@commands.group()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def unload(self, ctx, *, cog_name: str):
|
async def unload(self, ctx, *, cog_name: str):
|
||||||
"""Unloads packages"""
|
"""Unloads packages"""
|
||||||
cognames = [c.strip() for c in cog_name.split(" ")]
|
|
||||||
failed_packages = []
|
|
||||||
unloaded_packages = []
|
|
||||||
|
|
||||||
for c in cognames:
|
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||||
if c in ctx.bot.extensions:
|
|
||||||
ctx.bot.unload_extension(c)
|
|
||||||
await ctx.bot.remove_loaded_package(c)
|
|
||||||
unloaded_packages.append(inline(c))
|
|
||||||
else:
|
|
||||||
failed_packages.append(inline(c))
|
|
||||||
|
|
||||||
if unloaded_packages:
|
unloaded, failed = await self._unload(cog_names)
|
||||||
|
|
||||||
|
if unloaded:
|
||||||
fmt = "Package{plural} {packs} {other} unloaded."
|
fmt = "Package{plural} {packs} {other} unloaded."
|
||||||
formed = self.get_package_strings(unloaded_packages, fmt, ("was", "were"))
|
formed = self._get_package_strings(unloaded, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(_(formed))
|
||||||
|
|
||||||
if failed_packages:
|
if failed:
|
||||||
fmt = "The package{plural} {packs} {other} not loaded."
|
fmt = "The package{plural} {packs} {other} not loaded."
|
||||||
formed = self.get_package_strings(failed_packages, fmt, ("is", "are"))
|
formed = self._get_package_strings(failed, fmt, ("is", "are"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
@commands.command(name="reload")
|
@commands.command(name="reload")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def _reload(self, ctx, *, cog_name: str):
|
async def reload_(self, ctx, *, cog_name: str):
|
||||||
"""Reloads packages"""
|
"""Reloads packages"""
|
||||||
|
|
||||||
cognames = [c.strip() for c in cog_name.split(" ")]
|
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||||
|
async with ctx.typing():
|
||||||
|
loaded, failed, not_found = await self._reload(cog_names)
|
||||||
|
|
||||||
for c in cognames:
|
if loaded:
|
||||||
ctx.bot.unload_extension(c)
|
|
||||||
|
|
||||||
cogspecs = []
|
|
||||||
failed_packages = []
|
|
||||||
loaded_packages = []
|
|
||||||
notfound_packages = []
|
|
||||||
|
|
||||||
for c in cognames:
|
|
||||||
try:
|
|
||||||
spec = await ctx.bot.cog_mgr.find_cog(c)
|
|
||||||
cogspecs.append((spec, c))
|
|
||||||
except RuntimeError:
|
|
||||||
notfound_packages.append(inline(c))
|
|
||||||
|
|
||||||
for spec, name in cogspecs:
|
|
||||||
try:
|
|
||||||
self.cleanup_and_refresh_modules(spec.name)
|
|
||||||
await ctx.bot.load_extension(spec)
|
|
||||||
loaded_packages.append(inline(name))
|
|
||||||
except Exception as e:
|
|
||||||
log.exception("Package reloading failed", exc_info=e)
|
|
||||||
|
|
||||||
exception_log = "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
|
|
||||||
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
||||||
self.bot._last_exception = exception_log
|
|
||||||
|
|
||||||
failed_packages.append(inline(name))
|
|
||||||
|
|
||||||
if loaded_packages:
|
|
||||||
fmt = "Package{plural} {packs} {other} reloaded."
|
fmt = "Package{plural} {packs} {other} reloaded."
|
||||||
formed = self.get_package_strings(loaded_packages, fmt, ("was", "were"))
|
formed = self._get_package_strings(loaded, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
if failed_packages:
|
if failed:
|
||||||
fmt = "Failed to reload package{plural} {packs}. Check your " "logs for details"
|
fmt = "Failed to reload package{plural} {packs}. Check your logs for details"
|
||||||
formed = self.get_package_strings(failed_packages, fmt)
|
formed = self._get_package_strings(failed, fmt)
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
if notfound_packages:
|
if not_found:
|
||||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||||
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
||||||
await ctx.send(_(formed))
|
await ctx.send(formed)
|
||||||
|
|
||||||
def get_package_strings(self, packages: list, fmt: str, other: tuple = None):
|
|
||||||
"""
|
|
||||||
Gets the strings needed for the load, unload and reload commands
|
|
||||||
"""
|
|
||||||
if other is None:
|
|
||||||
other = ("", "")
|
|
||||||
plural = "s" if len(packages) > 1 else ""
|
|
||||||
use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
|
|
||||||
packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
|
|
||||||
|
|
||||||
form = {"plural": plural, "packs": packages_string, "other": other}
|
|
||||||
final_string = fmt.format(**form)
|
|
||||||
return final_string
|
|
||||||
|
|
||||||
@commands.command(name="shutdown")
|
@commands.command(name="shutdown")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -491,55 +599,32 @@ class Core:
|
|||||||
pass
|
pass
|
||||||
await ctx.bot.shutdown(restart=True)
|
await ctx.bot.shutdown(restart=True)
|
||||||
|
|
||||||
def cleanup_and_refresh_modules(self, module_name: str):
|
|
||||||
"""Interally reloads modules so that changes are detected"""
|
|
||||||
splitted = module_name.split(".")
|
|
||||||
|
|
||||||
def maybe_reload(new_name):
|
|
||||||
try:
|
|
||||||
lib = sys.modules[new_name]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
|
||||||
|
|
||||||
modules = itertools.accumulate(splitted, "{}.{}".format)
|
|
||||||
for m in modules:
|
|
||||||
maybe_reload(m)
|
|
||||||
|
|
||||||
children = {name: lib for name, lib in sys.modules.items() if name.startswith(module_name)}
|
|
||||||
for child_name, lib in children.items():
|
|
||||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
|
||||||
|
|
||||||
@commands.group(name="set")
|
@commands.group(name="set")
|
||||||
async def _set(self, ctx):
|
async def _set(self, ctx):
|
||||||
"""Changes Red's settings"""
|
"""Changes Red's settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
if ctx.guild:
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||||
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
|
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id) or "Not set"
|
||||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
|
||||||
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id) or "Not set"
|
||||||
|
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
||||||
|
guild_settings = f"Admin role: {admin_role}\nMod role: {mod_role}\n"
|
||||||
|
else:
|
||||||
|
guild_settings = ""
|
||||||
|
prefixes = None # This is correct. The below can happen in a guild.
|
||||||
if not prefixes:
|
if not prefixes:
|
||||||
prefixes = await ctx.bot.db.prefix()
|
prefixes = await ctx.bot.db.prefix()
|
||||||
locale = await ctx.bot.db.locale()
|
locale = await ctx.bot.db.locale()
|
||||||
|
|
||||||
|
prefix_string = " ".join(prefixes)
|
||||||
settings = (
|
settings = (
|
||||||
"{} Settings:\n\n"
|
f"{ctx.bot.user.name} Settings:\n\n"
|
||||||
"Prefixes: {}\n"
|
f"Prefixes: {prefix_string}\n"
|
||||||
"Admin role: {}\n"
|
f"{guild_settings}"
|
||||||
"Mod role: {}\n"
|
f"Locale: {locale}"
|
||||||
"Locale: {}"
|
|
||||||
"".format(
|
|
||||||
ctx.bot.user.name,
|
|
||||||
" ".join(prefixes),
|
|
||||||
admin_role.name if admin_role else "Not set",
|
|
||||||
mod_role.name if mod_role else "Not set",
|
|
||||||
locale,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@_set.command()
|
@_set.command()
|
||||||
@checks.guildowner()
|
@checks.guildowner()
|
||||||
@@ -575,13 +660,46 @@ class Core:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_set.command()
|
||||||
|
@checks.guildowner()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def serverfuzzy(self, ctx):
|
||||||
|
"""
|
||||||
|
Toggle whether to enable fuzzy command search for the server.
|
||||||
|
|
||||||
|
Default is for fuzzy command search to be disabled.
|
||||||
|
"""
|
||||||
|
current_setting = await ctx.bot.db.guild(ctx.guild).fuzzy()
|
||||||
|
await ctx.bot.db.guild(ctx.guild).fuzzy.set(not current_setting)
|
||||||
|
await ctx.send(
|
||||||
|
_("Fuzzy command search has been {} for this server.").format(
|
||||||
|
_("disabled") if current_setting else _("enabled")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@_set.command()
|
||||||
|
@checks.is_owner()
|
||||||
|
async def fuzzy(self, ctx):
|
||||||
|
"""
|
||||||
|
Toggle whether to enable fuzzy command search in DMs.
|
||||||
|
|
||||||
|
Default is for fuzzy command search to be disabled.
|
||||||
|
"""
|
||||||
|
current_setting = await ctx.bot.db.fuzzy()
|
||||||
|
await ctx.bot.db.fuzzy.set(not current_setting)
|
||||||
|
await ctx.send(
|
||||||
|
_("Fuzzy command search has been {} in DMs.").format(
|
||||||
|
_("disabled") if current_setting else _("enabled")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@_set.command(aliases=["color"])
|
@_set.command(aliases=["color"])
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def colour(self, ctx, *, colour: discord.Colour = None):
|
async def colour(self, ctx, *, colour: discord.Colour = None):
|
||||||
"""
|
"""
|
||||||
Sets a default colour to be used for the bot's embeds.
|
Sets a default colour to be used for the bot's embeds.
|
||||||
|
|
||||||
Acceptable values cor the colour parameter can be found at:
|
Acceptable values for the colour parameter can be found at:
|
||||||
|
|
||||||
http://discordpy.readthedocs.io/en/rewrite/ext/commands/api.html#discord.ext.commands.ColourConverter
|
http://discordpy.readthedocs.io/en/rewrite/ext/commands/api.html#discord.ext.commands.ColourConverter
|
||||||
"""
|
"""
|
||||||
@@ -714,7 +832,7 @@ class Core:
|
|||||||
async def _username(self, ctx, *, username: str):
|
async def _username(self, ctx, *, username: str):
|
||||||
"""Sets Red's username"""
|
"""Sets Red's username"""
|
||||||
try:
|
try:
|
||||||
await ctx.bot.user.edit(username=username)
|
await self._name(name=username)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
@@ -735,7 +853,7 @@ class Core:
|
|||||||
try:
|
try:
|
||||||
await ctx.guild.me.edit(nick=nickname)
|
await ctx.guild.me.edit(nick=nickname)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I lack the permissions to change my own " "nickname."))
|
await ctx.send(_("I lack the permissions to change my own nickname."))
|
||||||
else:
|
else:
|
||||||
await ctx.send("Done.")
|
await ctx.send("Done.")
|
||||||
|
|
||||||
@@ -746,8 +864,7 @@ class Core:
|
|||||||
if not prefixes:
|
if not prefixes:
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
return
|
return
|
||||||
prefixes = sorted(prefixes, reverse=True)
|
await self._prefixes(prefixes)
|
||||||
await ctx.bot.db.prefix.set(prefixes)
|
|
||||||
await ctx.send(_("Prefix set."))
|
await ctx.send(_("Prefix set."))
|
||||||
|
|
||||||
@_set.command(aliases=["serverprefixes"])
|
@_set.command(aliases=["serverprefixes"])
|
||||||
@@ -779,7 +896,7 @@ class Core:
|
|||||||
|
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
token += random.choice(chars)
|
token += random.choice(chars)
|
||||||
log.info("{0} ({0.id}) requested to be set as owner." "".format(ctx.author))
|
log.info("{0} ({0.id}) requested to be set as owner.".format(ctx.author))
|
||||||
print(_("\nVerification token:"))
|
print(_("\nVerification token:"))
|
||||||
print(token)
|
print(token)
|
||||||
|
|
||||||
@@ -869,8 +986,7 @@ class Core:
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def helpset(self, ctx: commands.Context):
|
async def helpset(self, ctx: commands.Context):
|
||||||
"""Manage settings for the help command."""
|
"""Manage settings for the help command."""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@helpset.command(name="pagecharlimit")
|
@helpset.command(name="pagecharlimit")
|
||||||
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
|
||||||
@@ -998,10 +1114,8 @@ class Core:
|
|||||||
if downloader_cog and hasattr(downloader_cog, "_repo_manager"):
|
if downloader_cog and hasattr(downloader_cog, "_repo_manager"):
|
||||||
repo_output = []
|
repo_output = []
|
||||||
repo_mgr = downloader_cog._repo_manager
|
repo_mgr = downloader_cog._repo_manager
|
||||||
for n, repo in repo_mgr._repos:
|
for repo in repo_mgr._repos.values():
|
||||||
repo_output.append(
|
repo_output.append({"url": repo.url, "name": repo.name, "branch": repo.branch})
|
||||||
{{"url": repo.url, "name": repo.name, "branch": repo.branch}}
|
|
||||||
)
|
|
||||||
repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json"
|
repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json"
|
||||||
with open(str(repo_filename), "w") as f:
|
with open(str(repo_filename), "w") as f:
|
||||||
f.write(json.dumps(repo_output, indent=4))
|
f.write(json.dumps(repo_output, indent=4))
|
||||||
@@ -1058,7 +1172,7 @@ class Core:
|
|||||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||||
prefix = prefixes[0]
|
prefix = prefixes[0]
|
||||||
|
|
||||||
content = _("Use `{}dm {} <text>` to reply to this user" "").format(prefix, author.id)
|
content = _("Use `{}dm {} <text>` to reply to this user").format(prefix, author.id)
|
||||||
|
|
||||||
description = _("Sent by {} {}").format(author, source)
|
description = _("Sent by {} {}").format(author, source)
|
||||||
|
|
||||||
@@ -1079,7 +1193,7 @@ class Core:
|
|||||||
await owner.send(content, embed=e)
|
await owner.send(content, embed=e)
|
||||||
except discord.InvalidArgument:
|
except discord.InvalidArgument:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot send your message, I'm unable to find " "my owner... *sigh*")
|
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||||
@@ -1091,7 +1205,7 @@ class Core:
|
|||||||
await owner.send("{}\n{}".format(content, box(msg_text)))
|
await owner.send("{}\n{}".format(content, box(msg_text)))
|
||||||
except discord.InvalidArgument:
|
except discord.InvalidArgument:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I cannot send your message, I'm unable to find " "my owner... *sigh*")
|
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||||
@@ -1136,7 +1250,7 @@ class Core:
|
|||||||
await destination.send(embed=e)
|
await destination.send(embed=e)
|
||||||
except:
|
except:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
@@ -1146,7 +1260,7 @@ class Core:
|
|||||||
await destination.send("{}\n{}".format(box(response), content))
|
await destination.send("{}\n{}".format(box(response), content))
|
||||||
except:
|
except:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to {}").format(destination))
|
await ctx.send(_("Message delivered to {}").format(destination))
|
||||||
@@ -1157,8 +1271,7 @@ class Core:
|
|||||||
"""
|
"""
|
||||||
Whitelist management commands.
|
Whitelist management commands.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@whitelist.command(name="add")
|
@whitelist.command(name="add")
|
||||||
async def whitelist_add(self, ctx, user: discord.User):
|
async def whitelist_add(self, ctx, user: discord.User):
|
||||||
@@ -1180,7 +1293,7 @@ class Core:
|
|||||||
|
|
||||||
msg = _("Whitelisted Users:")
|
msg = _("Whitelisted Users:")
|
||||||
for user in curr_list:
|
for user in curr_list:
|
||||||
msg.append("\n\t- {}".format(user))
|
msg += "\n\t- {}".format(user)
|
||||||
|
|
||||||
for page in pagify(msg):
|
for page in pagify(msg):
|
||||||
await ctx.send(box(page))
|
await ctx.send(box(page))
|
||||||
@@ -1216,8 +1329,7 @@ class Core:
|
|||||||
"""
|
"""
|
||||||
blacklist management commands.
|
blacklist management commands.
|
||||||
"""
|
"""
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
await ctx.send_help()
|
|
||||||
|
|
||||||
@blacklist.command(name="add")
|
@blacklist.command(name="add")
|
||||||
async def blacklist_add(self, ctx, user: discord.User):
|
async def blacklist_add(self, ctx, user: discord.User):
|
||||||
@@ -1243,7 +1355,7 @@ class Core:
|
|||||||
|
|
||||||
msg = _("blacklisted Users:")
|
msg = _("blacklisted Users:")
|
||||||
for user in curr_list:
|
for user in curr_list:
|
||||||
msg.append("\n\t- {}".format(user))
|
msg += "\n\t- {}".format(user)
|
||||||
|
|
||||||
for page in pagify(msg):
|
for page in pagify(msg):
|
||||||
await ctx.send(box(page))
|
await ctx.send(box(page))
|
||||||
@@ -1273,6 +1385,177 @@ class Core:
|
|||||||
await ctx.bot.db.blacklist.set([])
|
await ctx.bot.db.blacklist.set([])
|
||||||
await ctx.send(_("blacklist has been cleared."))
|
await ctx.send(_("blacklist has been cleared."))
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.admin_or_permissions(administrator=True)
|
||||||
|
async def localwhitelist(self, ctx):
|
||||||
|
"""
|
||||||
|
Whitelist management commands.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send_help()
|
||||||
|
|
||||||
|
@localwhitelist.command(name="add")
|
||||||
|
async def localwhitelist_add(self, ctx, *, user_or_role: str):
|
||||||
|
"""
|
||||||
|
Adds a user or role to the whitelist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
obj = await commands.MemberConverter().convert(ctx, user_or_role)
|
||||||
|
except commands.BadArgument:
|
||||||
|
obj = await commands.RoleConverter().convert(ctx, user_or_role)
|
||||||
|
user = False
|
||||||
|
else:
|
||||||
|
user = True
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
|
||||||
|
if obj.id not in curr_list:
|
||||||
|
curr_list.append(obj.id)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User added to whitelist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role added to whitelist."))
|
||||||
|
|
||||||
|
@localwhitelist.command(name="list")
|
||||||
|
async def localwhitelist_list(self, ctx):
|
||||||
|
"""
|
||||||
|
Lists whitelisted users and roles.
|
||||||
|
"""
|
||||||
|
curr_list = await ctx.bot.db.guild(ctx.guild).whitelist()
|
||||||
|
|
||||||
|
msg = _("Whitelisted Users and roles:")
|
||||||
|
for obj in curr_list:
|
||||||
|
msg += "\n\t- {}".format(obj)
|
||||||
|
|
||||||
|
for page in pagify(msg):
|
||||||
|
await ctx.send(box(page))
|
||||||
|
|
||||||
|
@localwhitelist.command(name="remove")
|
||||||
|
async def localwhitelist_remove(self, ctx, *, user_or_role: str):
|
||||||
|
"""
|
||||||
|
Removes user or role from whitelist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
obj = await commands.MemberConverter().convert(ctx, user_or_role)
|
||||||
|
except commands.BadArgument:
|
||||||
|
obj = await commands.RoleConverter().convert(ctx, user_or_role)
|
||||||
|
user = False
|
||||||
|
else:
|
||||||
|
user = True
|
||||||
|
|
||||||
|
removed = False
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
|
||||||
|
if obj.id in curr_list:
|
||||||
|
removed = True
|
||||||
|
curr_list.remove(obj.id)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User has been removed from whitelist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role has been removed from whitelist."))
|
||||||
|
else:
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User was not in the whitelist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role was not in the whitelist."))
|
||||||
|
|
||||||
|
@localwhitelist.command(name="clear")
|
||||||
|
async def localwhitelist_clear(self, ctx):
|
||||||
|
"""
|
||||||
|
Clears the whitelist.
|
||||||
|
"""
|
||||||
|
await ctx.bot.db.guild(ctx.guild).whitelist.set([])
|
||||||
|
await ctx.send(_("Whitelist has been cleared."))
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.admin_or_permissions(administrator=True)
|
||||||
|
async def localblacklist(self, ctx):
|
||||||
|
"""
|
||||||
|
blacklist management commands.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send_help()
|
||||||
|
|
||||||
|
@localblacklist.command(name="add")
|
||||||
|
async def localblacklist_add(self, ctx, *, user_or_role: str):
|
||||||
|
"""
|
||||||
|
Adds a user or role to the blacklist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
obj = await commands.MemberConverter().convert(ctx, user_or_role)
|
||||||
|
except commands.BadArgument:
|
||||||
|
obj = await commands.RoleConverter().convert(ctx, user_or_role)
|
||||||
|
user = False
|
||||||
|
else:
|
||||||
|
user = True
|
||||||
|
|
||||||
|
if user and await ctx.bot.is_owner(obj):
|
||||||
|
ctx.send(_("You cannot blacklist an owner!"))
|
||||||
|
return
|
||||||
|
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
|
||||||
|
if obj.id not in curr_list:
|
||||||
|
curr_list.append(obj.id)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User added to blacklist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role added to blacklist."))
|
||||||
|
|
||||||
|
@localblacklist.command(name="list")
|
||||||
|
async def localblacklist_list(self, ctx):
|
||||||
|
"""
|
||||||
|
Lists blacklisted users and roles.
|
||||||
|
"""
|
||||||
|
curr_list = await ctx.bot.db.guild(ctx.guild).blacklist()
|
||||||
|
|
||||||
|
msg = _("blacklisted Users and Roles:")
|
||||||
|
for obj in curr_list:
|
||||||
|
msg += "\n\t- {}".format(obj)
|
||||||
|
|
||||||
|
for page in pagify(msg):
|
||||||
|
await ctx.send(box(page))
|
||||||
|
|
||||||
|
@localblacklist.command(name="remove")
|
||||||
|
async def localblacklist_remove(self, ctx, *, user_or_role: str):
|
||||||
|
"""
|
||||||
|
Removes user or role from blacklist.
|
||||||
|
"""
|
||||||
|
removed = False
|
||||||
|
try:
|
||||||
|
obj = await commands.MemberConverter().convert(ctx, user_or_role)
|
||||||
|
except commands.BadArgument:
|
||||||
|
obj = await commands.RoleConverter().convert(ctx, user_or_role)
|
||||||
|
user = False
|
||||||
|
else:
|
||||||
|
user = True
|
||||||
|
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
|
||||||
|
if obj.id in curr_list:
|
||||||
|
removed = True
|
||||||
|
curr_list.remove(obj.id)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User has been removed from blacklist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role has been removed from blacklist."))
|
||||||
|
else:
|
||||||
|
if user:
|
||||||
|
await ctx.send(_("User was not in the blacklist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Role was not in the blacklist."))
|
||||||
|
|
||||||
|
@localblacklist.command(name="clear")
|
||||||
|
async def localblacklist_clear(self, ctx):
|
||||||
|
"""
|
||||||
|
Clears the blacklist.
|
||||||
|
"""
|
||||||
|
await ctx.bot.db.guild(ctx.guild).blacklist.set([])
|
||||||
|
await ctx.send(_("blacklist has been cleared."))
|
||||||
|
|
||||||
# RPC handlers
|
# RPC handlers
|
||||||
async def rpc_load(self, request):
|
async def rpc_load(self, request):
|
||||||
cog_name = request.params[0]
|
cog_name = request.params[0]
|
||||||
@@ -1281,7 +1564,7 @@ class Core:
|
|||||||
if spec is None:
|
if spec is None:
|
||||||
raise LookupError("No such cog found.")
|
raise LookupError("No such cog found.")
|
||||||
|
|
||||||
self.cleanup_and_refresh_modules(spec.name)
|
self._cleanup_and_refresh_modules(spec.name)
|
||||||
|
|
||||||
self.bot.load_extension(spec)
|
self.bot.load_extension(spec)
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,18 @@ import sys
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from copy import deepcopy
|
||||||
import hashlib
|
import hashlib
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from .json_io import JsonIO
|
from .json_io import JsonIO
|
||||||
from .utils import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import Config
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"create_temp_config",
|
||||||
"load_basic_configuration",
|
"load_basic_configuration",
|
||||||
"cog_data_path",
|
"cog_data_path",
|
||||||
"core_data_path",
|
"core_data_path",
|
||||||
@@ -43,6 +42,26 @@ if not config_dir:
|
|||||||
config_file = config_dir / "config.json"
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_config():
|
||||||
|
"""
|
||||||
|
Creates a default instance for Red, so it can be ran
|
||||||
|
without creating an instance.
|
||||||
|
|
||||||
|
.. warning:: The data of this instance will be removed
|
||||||
|
on next system restart.
|
||||||
|
"""
|
||||||
|
name = "temporary_red"
|
||||||
|
|
||||||
|
default_dirs = deepcopy(basic_config_default)
|
||||||
|
default_dirs["DATA_PATH"] = tempfile.mkdtemp()
|
||||||
|
default_dirs["STORAGE_TYPE"] = "JSON"
|
||||||
|
default_dirs["STORAGE_DETAILS"] = {}
|
||||||
|
|
||||||
|
config = JsonIO(config_file)._load_json()
|
||||||
|
config[name] = default_dirs
|
||||||
|
JsonIO(config_file)._save_json(config)
|
||||||
|
|
||||||
|
|
||||||
def load_basic_configuration(instance_name_: str):
|
def load_basic_configuration(instance_name_: str):
|
||||||
"""Loads the basic bootstrap configuration necessary for `Config`
|
"""Loads the basic bootstrap configuration necessary for `Config`
|
||||||
to know where to store or look for data.
|
to know where to store or look for data.
|
||||||
@@ -78,9 +97,7 @@ def load_basic_configuration(instance_name_: str):
|
|||||||
|
|
||||||
def _base_data_path() -> Path:
|
def _base_data_path() -> Path:
|
||||||
if basic_config is None:
|
if basic_config is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError("You must load the basic config before you can get the base data path.")
|
||||||
"You must load the basic config before you" " can get the base data path."
|
|
||||||
)
|
|
||||||
path = basic_config["DATA_PATH"]
|
path = basic_config["DATA_PATH"]
|
||||||
return Path(path).resolve()
|
return Path(path).resolve()
|
||||||
|
|
||||||
@@ -110,7 +127,7 @@ def cog_data_path(cog_instance=None, raw_name: str = None) -> Path:
|
|||||||
base_data_path = Path(_base_data_path())
|
base_data_path = Path(_base_data_path())
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"You must load the basic config before you" " can get the cog data path."
|
"You must load the basic config before you can get the cog data path."
|
||||||
) from e
|
) from e
|
||||||
cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
|
cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
|
||||||
|
|
||||||
@@ -128,7 +145,7 @@ def core_data_path() -> Path:
|
|||||||
base_data_path = Path(_base_data_path())
|
base_data_path = Path(_base_data_path())
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"You must load the basic config before you" " can get the core data path."
|
"You must load the basic config before you can get the core data path."
|
||||||
) from e
|
) from e
|
||||||
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
|
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
|
||||||
core_path.mkdir(exist_ok=True, parents=True)
|
core_path.mkdir(exist_ok=True, parents=True)
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ class Dev:
|
|||||||
"""
|
"""
|
||||||
if e.text is None:
|
if e.text is None:
|
||||||
return box("{0.__class__.__name__}: {0}".format(e), lang="py")
|
return box("{0.__class__.__name__}: {0}".format(e), lang="py")
|
||||||
return box(
|
return box("{0.text}{1:>{0.offset}}\n{2}: {0}".format(e, "^", type(e).__name__), lang="py")
|
||||||
"{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pages(msg: str):
|
def get_pages(msg: str):
|
||||||
@@ -209,12 +207,12 @@ class Dev:
|
|||||||
|
|
||||||
if ctx.channel.id in self.sessions:
|
if ctx.channel.id in self.sessions:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Already running a REPL session in this channel. " "Exit it with `quit`.")
|
_("Already running a REPL session in this channel. Exit it with `quit`.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.sessions.add(ctx.channel.id)
|
self.sessions.add(ctx.channel.id)
|
||||||
await ctx.send(_("Enter code to execute or evaluate." " `exit()` or `quit` to exit."))
|
await ctx.send(_("Enter code to execute or evaluate. `exit()` or `quit` to exit."))
|
||||||
|
|
||||||
msg_check = lambda m: (
|
msg_check = lambda m: (
|
||||||
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
|
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ __all__ = ["BaseDriver"]
|
|||||||
|
|
||||||
|
|
||||||
class BaseDriver:
|
class BaseDriver:
|
||||||
|
|
||||||
def __init__(self, cog_name, identifier):
|
def __init__(self, cog_name, identifier):
|
||||||
self.cog_name = cog_name
|
self.cog_name = cog_name
|
||||||
self.unique_cog_identifier = identifier
|
self.unique_cog_identifier = identifier
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
import copy
|
||||||
import weakref
|
import weakref
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ class JSON(BaseDriver):
|
|||||||
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
||||||
for i in full_identifiers:
|
for i in full_identifiers:
|
||||||
partial = partial[i]
|
partial = partial[i]
|
||||||
return partial
|
return copy.deepcopy(partial)
|
||||||
|
|
||||||
async def set(self, *identifiers: str, value=None):
|
async def set(self, *identifiers: str, value=None):
|
||||||
partial = self.data
|
partial = self.data
|
||||||
@@ -107,7 +108,7 @@ class JSON(BaseDriver):
|
|||||||
partial[i] = {}
|
partial[i] = {}
|
||||||
partial = partial[i]
|
partial = partial[i]
|
||||||
|
|
||||||
partial[full_identifiers[-1]] = value
|
partial[full_identifiers[-1]] = copy.deepcopy(value)
|
||||||
await self.jsonIO._threadsafe_save_json(self.data)
|
await self.jsonIO._threadsafe_save_json(self.data)
|
||||||
|
|
||||||
async def clear(self, *identifiers: str):
|
async def clear(self, *identifiers: str):
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class Mongo(BaseDriver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if partial is None:
|
if partial is None:
|
||||||
raise KeyError("No matching document was found and Config expects" " a KeyError.")
|
raise KeyError("No matching document was found and Config expects a KeyError.")
|
||||||
|
|
||||||
for i in identifiers:
|
for i in identifiers:
|
||||||
partial = partial[i]
|
partial = partial[i]
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ from pkg_resources import DistributionNotFound
|
|||||||
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__, commands
|
||||||
from .data_manager import storage_type
|
from .data_manager import storage_type
|
||||||
from .utils.chat_formatting import inline, bordered, pagify, box
|
from .utils.chat_formatting import inline, bordered, pagify, box
|
||||||
from .utils import fuzzy_command_search
|
from .utils import fuzzy_command_search
|
||||||
from colorama import Fore, Style, init
|
from colorama import Fore, Style, init
|
||||||
|
from . import rpc
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
sentry_log = logging.getLogger("red.sentry")
|
sentry_log = logging.getLogger("red.sentry")
|
||||||
@@ -49,7 +49,6 @@ def should_log_sentry(exception) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def init_events(bot, cli_flags):
|
def init_events(bot, cli_flags):
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_connect():
|
async def on_connect():
|
||||||
if bot.uptime is None:
|
if bot.uptime is None:
|
||||||
@@ -85,6 +84,9 @@ def init_events(bot, cli_flags):
|
|||||||
if packages:
|
if packages:
|
||||||
print("Loaded packages: " + ", ".join(packages))
|
print("Loaded packages: " + ", ".join(packages))
|
||||||
|
|
||||||
|
if bot.rpc_enabled:
|
||||||
|
await bot.rpc.initialize()
|
||||||
|
|
||||||
guilds = len(bot.guilds)
|
guilds = len(bot.guilds)
|
||||||
users = len(set([m for m in bot.get_all_members()]))
|
users = len(set([m for m in bot.get_all_members()]))
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ def init_events(bot, cli_flags):
|
|||||||
else:
|
else:
|
||||||
invite_url = None
|
invite_url = None
|
||||||
|
|
||||||
prefixes = await bot.db.prefix()
|
prefixes = cli_flags.prefix or (await bot.db.prefix())
|
||||||
lang = await bot.db.locale()
|
lang = await bot.db.locale()
|
||||||
red_version = __version__
|
red_version = __version__
|
||||||
red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
|
red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
|
||||||
@@ -119,24 +121,24 @@ def init_events(bot, cli_flags):
|
|||||||
|
|
||||||
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
|
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
try:
|
||||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
async with aiohttp.ClientSession() as session:
|
||||||
data = await r.json()
|
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
data = await r.json()
|
||||||
INFO.append(
|
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||||
"Outdated version! {} is available "
|
INFO.append(
|
||||||
"but you're using {}".format(data["info"]["version"], red_version)
|
"Outdated version! {} is available "
|
||||||
)
|
"but you're using {}".format(data["info"]["version"], red_version)
|
||||||
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id)
|
)
|
||||||
try:
|
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id)
|
||||||
await owner.send(
|
await owner.send(
|
||||||
"Your Red instance is out of date! {} is the current "
|
"Your Red instance is out of date! {} is the current "
|
||||||
"version, however you are using {}!".format(
|
"version, however you are using {}!".format(
|
||||||
data["info"]["version"], red_version
|
data["info"]["version"], red_version
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
INFO2 = []
|
INFO2 = []
|
||||||
|
|
||||||
sentry = await bot.db.enable_sentry()
|
sentry = await bot.db.enable_sentry()
|
||||||
@@ -173,8 +175,6 @@ def init_events(bot, cli_flags):
|
|||||||
print("\nInvite URL: {}\n".format(invite_url))
|
print("\nInvite URL: {}\n".format(invite_url))
|
||||||
|
|
||||||
bot.color = discord.Colour(await bot.db.color())
|
bot.color = discord.Colour(await bot.db.color())
|
||||||
if bot.rpc_enabled:
|
|
||||||
await bot.rpc.initialize()
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_error(event_method, *args, **kwargs):
|
async def on_error(event_method, *args, **kwargs):
|
||||||
@@ -184,6 +184,11 @@ def init_events(bot, cli_flags):
|
|||||||
async def on_command_error(ctx, error):
|
async def on_command_error(ctx, error):
|
||||||
if isinstance(error, commands.MissingRequiredArgument):
|
if isinstance(error, commands.MissingRequiredArgument):
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
|
elif isinstance(error, commands.ConversionFailure):
|
||||||
|
if error.args:
|
||||||
|
await ctx.send(error.args[0])
|
||||||
|
else:
|
||||||
|
await ctx.send_help()
|
||||||
elif isinstance(error, commands.BadArgument):
|
elif isinstance(error, commands.BadArgument):
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
elif isinstance(error, commands.DisabledCommand):
|
elif isinstance(error, commands.DisabledCommand):
|
||||||
@@ -226,7 +231,9 @@ def init_events(bot, cli_flags):
|
|||||||
term = ctx.invoked_with + " "
|
term = ctx.invoked_with + " "
|
||||||
if len(ctx.args) > 1:
|
if len(ctx.args) > 1:
|
||||||
term += " ".join(ctx.args[1:])
|
term += " ".join(ctx.args[1:])
|
||||||
await ctx.maybe_send_embed(fuzzy_command_search(ctx, ctx.invoked_with))
|
fuzzy_result = await fuzzy_command_search(ctx, ctx.invoked_with)
|
||||||
|
if fuzzy_result is not None:
|
||||||
|
await ctx.maybe_send_embed(fuzzy_result)
|
||||||
elif isinstance(error, commands.CheckFailure):
|
elif isinstance(error, commands.CheckFailure):
|
||||||
pass
|
pass
|
||||||
elif isinstance(error, commands.NoPrivateMessage):
|
elif isinstance(error, commands.NoPrivateMessage):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from . import commands
|
|||||||
|
|
||||||
|
|
||||||
def init_global_checks(bot):
|
def init_global_checks(bot):
|
||||||
|
|
||||||
@bot.check
|
@bot.check
|
||||||
async def global_perms(ctx):
|
async def global_perms(ctx):
|
||||||
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
||||||
@@ -27,10 +26,12 @@ def init_global_checks(bot):
|
|||||||
local_blacklist = await guild_settings.blacklist()
|
local_blacklist = await guild_settings.blacklist()
|
||||||
local_whitelist = await guild_settings.whitelist()
|
local_whitelist = await guild_settings.whitelist()
|
||||||
|
|
||||||
|
_ids = [r.id for r in ctx.author.roles if not r.is_default]
|
||||||
|
_ids.append(ctx.author.id)
|
||||||
if local_whitelist:
|
if local_whitelist:
|
||||||
return ctx.author.id in local_whitelist
|
return any(i in local_whitelist for i in _ids)
|
||||||
|
|
||||||
return ctx.author.id not in local_blacklist
|
return not any(i in local_blacklist for i in _ids)
|
||||||
|
|
||||||
@bot.check
|
@bot.check
|
||||||
async def bots(ctx):
|
async def bots(ctx):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from redbot.core.utils.chat_formatting import pagify, box
|
|||||||
from redbot.core.utils import fuzzy_command_search
|
from redbot.core.utils import fuzzy_command_search
|
||||||
|
|
||||||
|
|
||||||
EMPTY_STRING = u"\u200b"
|
EMPTY_STRING = "\u200b"
|
||||||
|
|
||||||
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
|
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
|
||||||
|
|
||||||
@@ -60,6 +60,12 @@ class Help(formatter.HelpFormatter):
|
|||||||
def pm_check(self, ctx):
|
def pm_check(self, ctx):
|
||||||
return isinstance(ctx.channel, discord.DMChannel)
|
return isinstance(ctx.channel, discord.DMChannel)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clean_prefix(self):
|
||||||
|
maybe_member = self.context.guild.me if self.context.guild else self.context.bot.user
|
||||||
|
pretty = f"@{maybe_member.display_name}"
|
||||||
|
return self.context.prefix.replace(maybe_member.mention, pretty)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def me(self):
|
def me(self):
|
||||||
return self.context.me
|
return self.context.me
|
||||||
@@ -76,10 +82,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
if self.pm_check(self.context):
|
if self.pm_check(self.context):
|
||||||
return self.context.bot.color
|
return self.context.bot.color
|
||||||
else:
|
else:
|
||||||
if await self.context.bot.db.guild(self.context.guild).use_bot_color():
|
return await self.context.embed_colour()
|
||||||
return self.context.bot.color
|
|
||||||
else:
|
|
||||||
return self.me.color
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def destination(self):
|
def destination(self):
|
||||||
@@ -184,15 +187,19 @@ class Help(formatter.HelpFormatter):
|
|||||||
for category, commands_ in itertools.groupby(data, key=category):
|
for category, commands_ in itertools.groupby(data, key=category):
|
||||||
commands_ = sorted(commands_)
|
commands_ = sorted(commands_)
|
||||||
if len(commands_) > 0:
|
if len(commands_) > 0:
|
||||||
field = EmbedField(category, self._add_subcommands(commands_), False)
|
for i, page in enumerate(
|
||||||
emb["fields"].append(field)
|
pagify(self._add_subcommands(commands_), page_length=1000)
|
||||||
|
):
|
||||||
|
title = category if i < 1 else f"{category} (continued)"
|
||||||
|
field = EmbedField(title, page, False)
|
||||||
|
emb["fields"].append(field)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Get list of commands for category
|
# Get list of commands for category
|
||||||
filtered = sorted(filtered)
|
filtered = sorted(filtered)
|
||||||
if filtered:
|
if filtered:
|
||||||
for i, page in enumerate(
|
for i, page in enumerate(
|
||||||
pagify(self._add_subcommands(filtered), page_length=1020)
|
pagify(self._add_subcommands(filtered), page_length=1000)
|
||||||
):
|
):
|
||||||
title = (
|
title = (
|
||||||
"**__Commands:__**"
|
"**__Commands:__**"
|
||||||
@@ -202,7 +209,6 @@ class Help(formatter.HelpFormatter):
|
|||||||
if i > 0:
|
if i > 0:
|
||||||
title += " (continued)"
|
title += " (continued)"
|
||||||
field = EmbedField(title, page, False)
|
field = EmbedField(title, page, False)
|
||||||
# This will still break at 6k total chars, hope that isnt an issue later
|
|
||||||
emb["fields"].append(field)
|
emb["fields"].append(field)
|
||||||
|
|
||||||
return emb
|
return emb
|
||||||
@@ -281,11 +287,10 @@ class Help(formatter.HelpFormatter):
|
|||||||
embed.set_author(**self.author)
|
embed.set_author(**self.author)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
async def cmd_not_found(self, ctx, cmd, color=None):
|
async def cmd_not_found(self, ctx, cmd, description=None, color=None):
|
||||||
# Shortcut for a shortcut. Sue me
|
# Shortcut for a shortcut. Sue me
|
||||||
out = fuzzy_command_search(ctx, " ".join(ctx.args[1:]))
|
|
||||||
embed = await self.simple_embed(
|
embed = await self.simple_embed(
|
||||||
ctx, title="Command {} not found.".format(cmd), description=out, color=color
|
ctx, title="Command {} not found.".format(cmd), description=description, color=color
|
||||||
)
|
)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@@ -296,7 +301,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command(hidden=True)
|
||||||
async def help(ctx, *cmds: str):
|
async def help(ctx, *cmds: str):
|
||||||
"""Shows help documentation.
|
"""Shows help documentation.
|
||||||
|
|
||||||
@@ -326,11 +331,19 @@ async def help(ctx, *cmds: str):
|
|||||||
command = ctx.bot.all_commands.get(name)
|
command = ctx.bot.all_commands.get(name)
|
||||||
if command is None:
|
if command is None:
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name))
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
|
if fuzzy_result is not None:
|
||||||
|
await destination.send(
|
||||||
|
embed=await ctx.bot.formatter.cmd_not_found(
|
||||||
|
ctx, name, description=fuzzy_result
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await destination.send(
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
|
if fuzzy_result is not None:
|
||||||
)
|
await destination.send(
|
||||||
|
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
||||||
@@ -341,11 +354,17 @@ async def help(ctx, *cmds: str):
|
|||||||
command = ctx.bot.all_commands.get(name)
|
command = ctx.bot.all_commands.get(name)
|
||||||
if command is None:
|
if command is None:
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name))
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
|
if fuzzy_result is not None:
|
||||||
|
await destination.send(
|
||||||
|
embed=await ctx.bot.formatter.cmd_not_found(
|
||||||
|
ctx, name, description=fuzzy_result
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await destination.send(
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
|
if fuzzy_result is not None:
|
||||||
)
|
await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
|
||||||
return
|
return
|
||||||
|
|
||||||
for key in cmds[1:]:
|
for key in cmds[1:]:
|
||||||
@@ -354,13 +373,19 @@ async def help(ctx, *cmds: str):
|
|||||||
command = command.all_commands.get(key)
|
command = command.all_commands.get(key)
|
||||||
if command is None:
|
if command is None:
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
await destination.send(
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
embed=await ctx.bot.formatter.cmd_not_found(ctx, key)
|
if fuzzy_result is not None:
|
||||||
)
|
await destination.send(
|
||||||
|
embed=await ctx.bot.formatter.cmd_not_found(
|
||||||
|
ctx, name, description=fuzzy_result
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await destination.send(
|
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||||
ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name))
|
if fuzzy_result is not None:
|
||||||
)
|
await destination.send(
|
||||||
|
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if use_embeds:
|
if use_embeds:
|
||||||
|
|||||||
@@ -461,6 +461,9 @@ async def create_case(
|
|||||||
if not await case_type.is_enabled():
|
if not await case_type.is_enabled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if user == bot.user:
|
||||||
|
return None
|
||||||
|
|
||||||
next_case_number = int(await get_next_case_number(guild))
|
next_case_number = int(await get_next_case_number(guild))
|
||||||
|
|
||||||
case = Case(
|
case = Case(
|
||||||
|
|||||||
@@ -1,117 +1,74 @@
|
|||||||
import weakref
|
import asyncio
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import jsonrpcserver.aio
|
from aiohttp_json_rpc import JsonRpc
|
||||||
|
from aiohttp_json_rpc.rpc import unpack_request_args
|
||||||
|
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
__all__ = ["methods", "RPC", "Methods"]
|
|
||||||
|
|
||||||
log = logging.getLogger("red.rpc")
|
log = logging.getLogger("red.rpc")
|
||||||
|
|
||||||
|
__all__ = ["RPC", "RPCMixin", "get_name"]
|
||||||
class Methods(jsonrpcserver.aio.AsyncMethods):
|
|
||||||
"""
|
|
||||||
Container class for all registered RPC methods, please use the existing `methods`
|
|
||||||
attribute rather than creating a new instance of this class.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
**NEVER** create a new instance of this class!
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._items = weakref.WeakValueDictionary()
|
|
||||||
|
|
||||||
def add(self, method, name: str = None):
|
|
||||||
"""
|
|
||||||
Registers a method to the internal RPC server making it available for
|
|
||||||
RPC users to call.
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Any method added here must take ONLY JSON serializable parameters and
|
|
||||||
MUST return a JSON serializable object.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
method : function
|
|
||||||
A reference to the function to register.
|
|
||||||
|
|
||||||
name : str
|
|
||||||
Name of the function as seen by the RPC clients.
|
|
||||||
"""
|
|
||||||
if not inspect.iscoroutinefunction(method):
|
|
||||||
raise TypeError("Method must be a coroutine.")
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
name = method.__qualname__
|
|
||||||
|
|
||||||
self._items[str(name)] = method
|
|
||||||
|
|
||||||
def remove(self, *, name: str = None, method=None):
|
|
||||||
"""
|
|
||||||
Unregisters an RPC method. Either a name or reference to the method must
|
|
||||||
be provided and name will take priority.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str
|
|
||||||
method : function
|
|
||||||
"""
|
|
||||||
if name and name in self._items:
|
|
||||||
del self._items[name]
|
|
||||||
|
|
||||||
elif method and method in self._items.values():
|
|
||||||
to_remove = []
|
|
||||||
for name, val in self._items.items():
|
|
||||||
if method == val:
|
|
||||||
to_remove.append(name)
|
|
||||||
|
|
||||||
for name in to_remove:
|
|
||||||
del self._items[name]
|
|
||||||
|
|
||||||
def all_methods(self):
|
|
||||||
"""
|
|
||||||
Lists all available method names.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list of str
|
|
||||||
"""
|
|
||||||
return self._items.keys()
|
|
||||||
|
|
||||||
|
|
||||||
methods = Methods()
|
def get_name(func, prefix=None):
|
||||||
|
class_name = prefix or func.__self__.__class__.__name__.lower()
|
||||||
|
func_name = func.__name__.strip("_")
|
||||||
|
if class_name == "redrpc":
|
||||||
|
return func_name.upper()
|
||||||
|
return f"{class_name}__{func_name}".upper()
|
||||||
|
|
||||||
|
|
||||||
class BaseRPCMethodMixin:
|
class RedRpc(JsonRpc):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.add_methods(("", self.get_method_info))
|
||||||
|
|
||||||
def __init__(self):
|
def _add_method(self, method, prefix=""):
|
||||||
methods.add(self.all_methods, name="all_methods")
|
if not asyncio.iscoroutinefunction(method):
|
||||||
|
return
|
||||||
|
|
||||||
async def all_methods(self):
|
name = get_name(method, prefix)
|
||||||
return list(methods.all_methods())
|
|
||||||
|
self.methods[name] = method
|
||||||
|
|
||||||
|
def remove_method(self, method):
|
||||||
|
meth_name = get_name(method)
|
||||||
|
new_methods = {}
|
||||||
|
for name, meth in self.methods.items():
|
||||||
|
if name != meth_name:
|
||||||
|
new_methods[name] = meth
|
||||||
|
self.methods = new_methods
|
||||||
|
|
||||||
|
def remove_methods(self, prefix: str):
|
||||||
|
new_methods = {}
|
||||||
|
for name, meth in self.methods.items():
|
||||||
|
splitted = name.split("__")
|
||||||
|
if len(splitted) < 2 or splitted[0] != prefix:
|
||||||
|
new_methods[name] = meth
|
||||||
|
self.methods = new_methods
|
||||||
|
|
||||||
|
async def get_method_info(self, request):
|
||||||
|
method_name = request.params[0]
|
||||||
|
if method_name in self.methods:
|
||||||
|
return self.methods[method_name].__doc__
|
||||||
|
return "No docstring available."
|
||||||
|
|
||||||
|
|
||||||
class RPC(BaseRPCMethodMixin):
|
class RPC:
|
||||||
"""
|
"""
|
||||||
RPC server manager.
|
RPC server manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self):
|
||||||
self.app = web.Application(loop=bot.loop)
|
self.app = web.Application()
|
||||||
self.app.router.add_post("/rpc", self.handle)
|
self._rpc = RedRpc()
|
||||||
|
self.app.router.add_route("*", "/", self._rpc)
|
||||||
|
|
||||||
self.app_handler = self.app.make_handler()
|
self.app_handler = self.app.make_handler()
|
||||||
|
|
||||||
self.server = None
|
self.server = None
|
||||||
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""
|
"""
|
||||||
Finalizes the initialization of the RPC server and allows it to begin
|
Finalizes the initialization of the RPC server and allows it to begin
|
||||||
@@ -126,10 +83,79 @@ class RPC(BaseRPCMethodMixin):
|
|||||||
"""
|
"""
|
||||||
self.server.close()
|
self.server.close()
|
||||||
|
|
||||||
async def handle(self, request):
|
def add_method(self, method, prefix: str = None):
|
||||||
request = await request.text()
|
if prefix is None:
|
||||||
response = await methods.dispatch(request)
|
prefix = method.__self__.__class__.__name__.lower()
|
||||||
if response.is_notification:
|
|
||||||
return web.Response()
|
if not asyncio.iscoroutinefunction(method):
|
||||||
else:
|
raise TypeError("RPC methods must be coroutines.")
|
||||||
return web.json_response(response, status=response.http_status)
|
|
||||||
|
self._rpc.add_methods((prefix, unpack_request_args(method)))
|
||||||
|
|
||||||
|
def add_multi_method(self, *methods, prefix: str = None):
|
||||||
|
if not all(asyncio.iscoroutinefunction(m) for m in methods):
|
||||||
|
raise TypeError("RPC methods must be coroutines.")
|
||||||
|
|
||||||
|
for method in methods:
|
||||||
|
self.add_method(method, prefix=prefix)
|
||||||
|
|
||||||
|
def remove_method(self, method):
|
||||||
|
self._rpc.remove_method(method)
|
||||||
|
|
||||||
|
def remove_methods(self, prefix: str):
|
||||||
|
self._rpc.remove_methods(prefix)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCMixin:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.rpc = RPC()
|
||||||
|
|
||||||
|
self.rpc_handlers = {} # Lowered cog name to method
|
||||||
|
|
||||||
|
def register_rpc_handler(self, method):
|
||||||
|
"""
|
||||||
|
Registers a method to act as an RPC handler if the internal RPC server is active.
|
||||||
|
|
||||||
|
When calling this method through the RPC server, use the naming scheme "cogname__methodname".
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
All parameters to RPC handler methods must be JSON serializable objects.
|
||||||
|
The return value of handler methods must also be JSON serializable.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
method : coroutine
|
||||||
|
The method to register with the internal RPC server.
|
||||||
|
"""
|
||||||
|
self.rpc.add_method(method)
|
||||||
|
|
||||||
|
cog_name = method.__self__.__class__.__name__.upper()
|
||||||
|
if cog_name not in self.rpc_handlers:
|
||||||
|
self.rpc_handlers[cog_name] = []
|
||||||
|
|
||||||
|
self.rpc_handlers[cog_name].append(method)
|
||||||
|
|
||||||
|
def unregister_rpc_handler(self, method):
|
||||||
|
"""
|
||||||
|
Unregisters an RPC method handler.
|
||||||
|
|
||||||
|
This will be called automatically for you on cog unload and will pass silently if the
|
||||||
|
method is not previously registered.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
method : coroutine
|
||||||
|
The method to unregister from the internal RPC server.
|
||||||
|
"""
|
||||||
|
self.rpc.remove_method(method)
|
||||||
|
|
||||||
|
name = get_name(method)
|
||||||
|
cog_name = name.split("__")[0]
|
||||||
|
|
||||||
|
if cog_name in self.rpc_handlers:
|
||||||
|
try:
|
||||||
|
self.rpc_handlers[cog_name].remove(method)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
__all__ = ["TYPE_CHECKING", "NewType", "safe_delete", "fuzzy_command_search"]
|
__all__ = ["safe_delete", "fuzzy_command_search"]
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from fuzzywuzzy import process
|
from fuzzywuzzy import process
|
||||||
from .chat_formatting import box
|
from .chat_formatting import box
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
except ImportError:
|
|
||||||
TYPE_CHECKING = False
|
|
||||||
|
|
||||||
try:
|
def fuzzy_filter(record):
|
||||||
from typing import NewType
|
return record.funcName != "extractWithoutOrder"
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def NewType(name, tp):
|
|
||||||
return type(name, (tp,), {})
|
logging.getLogger().addFilter(fuzzy_filter)
|
||||||
|
|
||||||
|
|
||||||
def safe_delete(pth: Path):
|
def safe_delete(pth: Path):
|
||||||
@@ -31,9 +27,49 @@ def safe_delete(pth: Path):
|
|||||||
shutil.rmtree(str(pth), ignore_errors=True)
|
shutil.rmtree(str(pth), ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def fuzzy_command_search(ctx: commands.Context, term: str):
|
async def filter_commands(ctx: commands.Context, extracted: list):
|
||||||
|
return [
|
||||||
|
i
|
||||||
|
for i in extracted
|
||||||
|
if i[1] >= 90
|
||||||
|
and not i[0].hidden
|
||||||
|
and await i[0].can_run(ctx)
|
||||||
|
and all([await p.can_run(ctx) for p in i[0].parents])
|
||||||
|
and not any([p.hidden for p in i[0].parents])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def fuzzy_command_search(ctx: commands.Context, term: str):
|
||||||
out = ""
|
out = ""
|
||||||
for pos, extracted in enumerate(process.extract(term, ctx.bot.walk_commands(), limit=5), 1):
|
if ctx.guild is not None:
|
||||||
|
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy()
|
||||||
|
else:
|
||||||
|
enabled = await ctx.bot.db.fuzzy()
|
||||||
|
if not enabled:
|
||||||
|
return None
|
||||||
|
alias_cog = ctx.bot.get_cog("Alias")
|
||||||
|
if alias_cog is not None:
|
||||||
|
is_alias, alias = await alias_cog.is_alias(ctx.guild, term)
|
||||||
|
if is_alias:
|
||||||
|
return None
|
||||||
|
|
||||||
|
customcom_cog = ctx.bot.get_cog("CustomCommands")
|
||||||
|
if customcom_cog is not None:
|
||||||
|
cmd_obj = customcom_cog.commandobj
|
||||||
|
try:
|
||||||
|
ccinfo = await cmd_obj.get(ctx.message, term)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
extracted_cmds = await filter_commands(
|
||||||
|
ctx, process.extract(term, ctx.bot.walk_commands(), limit=5)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not extracted_cmds:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for pos, extracted in enumerate(extracted_cmds, 1):
|
||||||
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format(
|
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format(
|
||||||
pos,
|
pos,
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AntiSpam:
|
|||||||
Where quantity represents the maximum amount of times
|
Where quantity represents the maximum amount of times
|
||||||
something should be allowed in an interval.
|
something should be allowed in an interval.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO : Decorator interface for command check using `spammy`
|
# TODO : Decorator interface for command check using `spammy`
|
||||||
# with insertion of the antispam element into context
|
# with insertion of the antispam element into context
|
||||||
# for manual stamping on succesful command completion
|
# for manual stamping on succesful command completion
|
||||||
|
|||||||
53
redbot/core/utils/caching.py
Normal file
53
redbot/core/utils/caching.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
class LRUDict:
|
||||||
|
"""
|
||||||
|
dict with LRU-eviction and max-size
|
||||||
|
|
||||||
|
This is intended for caching, it may not behave how you want otherwise
|
||||||
|
|
||||||
|
This uses collections.OrderedDict under the hood, but does not directly expose
|
||||||
|
all of it's methods (intentional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *keyval_pairs, size):
|
||||||
|
self.size = size
|
||||||
|
self._dict = collections.OrderedDict(*keyval_pairs)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
if key in self._dict:
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
ret = self._dict.__getitem__(key)
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if key in self._dict:
|
||||||
|
self._dict.move_to_end(key, last=True)
|
||||||
|
self._dict[key] = value
|
||||||
|
if len(self._dict) > self.size:
|
||||||
|
self._dict.popitem(last=False)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
return self._dict.__delitem__(key)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
return self._dict.clear()
|
||||||
|
|
||||||
|
def pop(self, key):
|
||||||
|
return self._dict.pop(key)
|
||||||
|
|
||||||
|
# all of the below access all of the items, and therefore shouldnt modify the ordering for eviction
|
||||||
|
def keys(self):
|
||||||
|
return self._dict.keys()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._dict.items()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._dict.values()
|
||||||
@@ -5,10 +5,14 @@ https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
|
|||||||
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
from typing import Union, Iterable
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
|
_ReactableEmoji = Union[str, discord.Emoji]
|
||||||
|
|
||||||
|
|
||||||
async def menu(
|
async def menu(
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
@@ -66,8 +70,8 @@ async def menu(
|
|||||||
message = await ctx.send(embed=current_page)
|
message = await ctx.send(embed=current_page)
|
||||||
else:
|
else:
|
||||||
message = await ctx.send(current_page)
|
message = await ctx.send(current_page)
|
||||||
for key in controls.keys():
|
# Don't wait for reactions to be added (GH-1797)
|
||||||
await message.add_reaction(key)
|
ctx.bot.loop.create_task(_add_menu_reactions(message, controls.keys()))
|
||||||
else:
|
else:
|
||||||
if isinstance(current_page, discord.Embed):
|
if isinstance(current_page, discord.Embed):
|
||||||
await message.edit(embed=current_page)
|
await message.edit(embed=current_page)
|
||||||
@@ -148,4 +152,12 @@ async def close_menu(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _add_menu_reactions(message: discord.Message, emojis: Iterable[_ReactableEmoji]):
|
||||||
|
"""Add the reactions"""
|
||||||
|
# The task should exit silently if the message is deleted
|
||||||
|
with contextlib.suppress(discord.NotFound):
|
||||||
|
for emoji in emojis:
|
||||||
|
await message.add_reaction(emoji)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONTROLS = {"⬅": prev_page, "❌": close_menu, "➡": next_page}
|
DEFAULT_CONTROLS = {"⬅": prev_page, "❌": close_menu, "➡": next_page}
|
||||||
|
|||||||
@@ -135,16 +135,10 @@ async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Membe
|
|||||||
|
|
||||||
if isinstance(obj, discord.Role):
|
if isinstance(obj, discord.Role):
|
||||||
return obj.id in [admin_role_id, mod_role_id]
|
return obj.id in [admin_role_id, mod_role_id]
|
||||||
mod_roles = [r for r in server.roles if r.id == mod_role_id]
|
|
||||||
mod_role = mod_roles[0] if len(mod_roles) > 0 else None
|
|
||||||
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
|
||||||
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
|
||||||
|
|
||||||
if user and user == await bot.is_owner(user):
|
if await bot.is_owner(user):
|
||||||
return True
|
return True
|
||||||
elif admin_role and discord.utils.get(user.roles, name=admin_role):
|
elif discord.utils.find(lambda r: r.id in (admin_role_id, mod_role_id), user.roles):
|
||||||
return True
|
|
||||||
elif mod_role and discord.utils.get(user.roles, name=mod_role):
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -220,17 +214,14 @@ async def is_admin_or_superior(
|
|||||||
else:
|
else:
|
||||||
raise TypeError("Only messages, members or roles may be passed")
|
raise TypeError("Only messages, members or roles may be passed")
|
||||||
|
|
||||||
server = obj.guild
|
admin_role_id = await bot.db.guild(obj.guild).admin_role()
|
||||||
admin_role_id = await bot.db.guild(server).admin_role()
|
|
||||||
|
|
||||||
if isinstance(obj, discord.Role):
|
if isinstance(obj, discord.Role):
|
||||||
return obj.id == admin_role_id
|
return obj.id == admin_role_id
|
||||||
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
|
||||||
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
|
||||||
|
|
||||||
if user and await bot.is_owner(user):
|
if user and await bot.is_owner(user):
|
||||||
return True
|
return True
|
||||||
elif admin_roles and discord.utils.get(user.roles, name=admin_role):
|
elif discord.utils.get(user.roles, id=admin_role_id):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from distutils.version import StrictVersion
|
||||||
from redbot.setup import (
|
from redbot.setup import (
|
||||||
basic_setup,
|
basic_setup,
|
||||||
load_existing_config,
|
load_existing_config,
|
||||||
@@ -16,18 +18,17 @@ from redbot.setup import (
|
|||||||
create_backup,
|
create_backup,
|
||||||
save_config,
|
save_config,
|
||||||
)
|
)
|
||||||
|
from redbot.core import __version__
|
||||||
from redbot.core.utils import safe_delete
|
from redbot.core.utils import safe_delete
|
||||||
from redbot.core.cli import confirm
|
from redbot.core.cli import confirm
|
||||||
|
|
||||||
if sys.platform == "linux":
|
if sys.platform == "linux":
|
||||||
import distro
|
import distro
|
||||||
|
|
||||||
PYTHON_OK = sys.version_info >= (3, 5)
|
PYTHON_OK = sys.version_info >= (3, 6)
|
||||||
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
||||||
|
|
||||||
INTRO = (
|
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
||||||
"==========================\n" "Red Discord Bot - Launcher\n" "==========================\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
IS_WINDOWS = os.name == "nt"
|
IS_WINDOWS = os.name == "nt"
|
||||||
IS_MAC = sys.platform == "darwin"
|
IS_MAC = sys.platform == "darwin"
|
||||||
@@ -383,15 +384,30 @@ def debug_info():
|
|||||||
+ "User: {}\n".format(user_who_ran)
|
+ "User: {}\n".format(user_who_ran)
|
||||||
)
|
)
|
||||||
print(info)
|
print(info)
|
||||||
exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def is_outdated():
|
||||||
|
red_pypi = "https://pypi.python.org/pypi/Red-DiscordBot"
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get("{}/json".format(red_pypi)) as r:
|
||||||
|
data = await r.json()
|
||||||
|
new_version = data["info"]["version"]
|
||||||
|
return StrictVersion(new_version) > StrictVersion(__version__), new_version
|
||||||
|
|
||||||
|
|
||||||
def main_menu():
|
def main_menu():
|
||||||
if IS_WINDOWS:
|
if IS_WINDOWS:
|
||||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||||
clear_screen()
|
clear_screen()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
outdated, new_version = loop.run_until_complete(is_outdated())
|
||||||
while True:
|
while True:
|
||||||
print(INTRO)
|
print(INTRO)
|
||||||
|
print("\033[4mCurrent version:\033[0m {}".format(__version__))
|
||||||
|
if outdated:
|
||||||
|
print("Red is outdated. {} is available.".format(new_version))
|
||||||
|
print("")
|
||||||
print("1. Run Red w/ autorestart in case of issues")
|
print("1. Run Red w/ autorestart in case of issues")
|
||||||
print("2. Run Red")
|
print("2. Run Red")
|
||||||
print("3. Update Red")
|
print("3. Update Red")
|
||||||
@@ -420,13 +436,12 @@ def main_menu():
|
|||||||
basic_setup()
|
basic_setup()
|
||||||
wait()
|
wait()
|
||||||
elif choice == "5":
|
elif choice == "5":
|
||||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
loop.run_until_complete(remove_instance_interaction())
|
||||||
wait()
|
wait()
|
||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
debug_info()
|
debug_info()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
while True:
|
while True:
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print("==== Reinstall Red ====")
|
print("==== Reinstall Red ====")
|
||||||
print(
|
print(
|
||||||
@@ -457,7 +472,7 @@ def main_menu():
|
|||||||
def main():
|
def main():
|
||||||
if not PYTHON_OK:
|
if not PYTHON_OK:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Red requires Python 3.5 or greater. " "Please install the correct version!"
|
"Red requires Python 3.6 or greater. Please install the correct version!"
|
||||||
)
|
)
|
||||||
if args.debuginfo: # Check first since the function triggers an exit
|
if args.debuginfo: # Check first since the function triggers an exit
|
||||||
debug_info()
|
debug_info()
|
||||||
|
|||||||
5
redbot/meta.py
Normal file
5
redbot/meta.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
This module will contain various attributes useful for testing and cog development.
|
||||||
|
"""
|
||||||
|
|
||||||
|
testing = False
|
||||||
1
redbot/pytest/__init__.py
Normal file
1
redbot/pytest/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import *
|
||||||
20
redbot/pytest/admin.py
Normal file
20
redbot/pytest/admin.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.admin import Admin
|
||||||
|
from redbot.cogs.admin.announcer import Announcer
|
||||||
|
|
||||||
|
__all__ = ["admin", "announcer"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def admin(config):
|
||||||
|
return Admin(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def announcer(admin):
|
||||||
|
a = Announcer(MagicMock(), "Some message", admin.conf)
|
||||||
|
yield a
|
||||||
|
a.cancel()
|
||||||
13
redbot/pytest/alias.py
Normal file
13
redbot/pytest/alias.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.alias import Alias
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
__all__ = ["alias"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def alias(config, monkeypatch):
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
return Alias(None)
|
||||||
13
redbot/pytest/cog_manager.py
Normal file
13
redbot/pytest/cog_manager.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["cog_mgr", "default_dir"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog_mgr(red):
|
||||||
|
return red.cog_mgr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def default_dir(red):
|
||||||
|
return red.main_dir
|
||||||
@@ -9,6 +9,26 @@ from redbot.core.bot import Red
|
|||||||
|
|
||||||
from redbot.core.drivers import red_json
|
from redbot.core.drivers import red_json
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"monkeysession",
|
||||||
|
"override_data_path",
|
||||||
|
"coroutine",
|
||||||
|
"json_driver",
|
||||||
|
"config",
|
||||||
|
"config_fr",
|
||||||
|
"red",
|
||||||
|
"guild_factory",
|
||||||
|
"empty_guild",
|
||||||
|
"empty_channel",
|
||||||
|
"empty_member",
|
||||||
|
"empty_message",
|
||||||
|
"empty_role",
|
||||||
|
"empty_user",
|
||||||
|
"member_factory",
|
||||||
|
"user_factory",
|
||||||
|
"ctx",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def monkeysession(request):
|
def monkeysession(request):
|
||||||
@@ -27,7 +47,6 @@ def override_data_path(tmpdir):
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def coroutine():
|
def coroutine():
|
||||||
|
|
||||||
async def some_coro(*args, **kwargs):
|
async def some_coro(*args, **kwargs):
|
||||||
return args, kwargs
|
return args, kwargs
|
||||||
|
|
||||||
@@ -74,7 +93,6 @@ def guild_factory():
|
|||||||
mock_guild = namedtuple("Guild", "id members")
|
mock_guild = namedtuple("Guild", "id members")
|
||||||
|
|
||||||
class GuildFactory:
|
class GuildFactory:
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return mock_guild(random.randint(1, 999999999), [])
|
return mock_guild(random.randint(1, 999999999), [])
|
||||||
|
|
||||||
@@ -103,7 +121,6 @@ def member_factory(guild_factory):
|
|||||||
mock_member = namedtuple("Member", "id guild display_name")
|
mock_member = namedtuple("Member", "id guild display_name")
|
||||||
|
|
||||||
class MemberFactory:
|
class MemberFactory:
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return mock_member(random.randint(1, 999999999), guild_factory.get(), "Testing_Name")
|
return mock_member(random.randint(1, 999999999), guild_factory.get(), "Testing_Name")
|
||||||
|
|
||||||
@@ -120,7 +137,6 @@ def user_factory():
|
|||||||
mock_user = namedtuple("User", "id")
|
mock_user = namedtuple("User", "id")
|
||||||
|
|
||||||
class UserFactory:
|
class UserFactory:
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return mock_user(random.randint(1, 999999999))
|
return mock_user(random.randint(1, 999999999))
|
||||||
|
|
||||||
@@ -158,7 +174,7 @@ def red(config_fr):
|
|||||||
|
|
||||||
Config.get_core_conf = lambda *args, **kwargs: config_fr
|
Config.get_core_conf = lambda *args, **kwargs: config_fr
|
||||||
|
|
||||||
red = Red(cli_flags, description=description, pm_help=None)
|
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||||
|
|
||||||
yield red
|
yield red
|
||||||
|
|
||||||
24
redbot/pytest/data_manager.py
Normal file
24
redbot/pytest/data_manager.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.core import data_manager
|
||||||
|
|
||||||
|
__all__ = ["cleanup_datamanager", "data_mgr_config", "cog_instance"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_datamanager():
|
||||||
|
data_manager.basic_config = None
|
||||||
|
data_manager.jsonio = None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def data_mgr_config(tmpdir):
|
||||||
|
default = data_manager.basic_config_default.copy()
|
||||||
|
default["BASE_DIR"] = str(tmpdir)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog_instance():
|
||||||
|
thing = type("CogTest", (object,), {})
|
||||||
|
return thing()
|
||||||
12
redbot/pytest/dataconverter.py
Normal file
12
redbot/pytest/dataconverter.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from redbot.cogs.dataconverter import core_specs
|
||||||
|
|
||||||
|
__all__ = ["get_specresolver"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_specresolver(path):
|
||||||
|
here = Path(path)
|
||||||
|
|
||||||
|
resolver = core_specs.SpecResolver(here.parent)
|
||||||
|
return resolver
|
||||||
103
redbot/pytest/downloader.py
Normal file
103
redbot/pytest/downloader.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.downloader.repo_manager import RepoManager, Repo
|
||||||
|
from redbot.cogs.downloader.installable import Installable
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"patch_relative_to",
|
||||||
|
"repo_manager",
|
||||||
|
"repo",
|
||||||
|
"repo_norun",
|
||||||
|
"bot_repo",
|
||||||
|
"INFO_JSON",
|
||||||
|
"installable",
|
||||||
|
"fake_run_noprint",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_run(*args, **kwargs):
|
||||||
|
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
||||||
|
res = fake_result_tuple(0, (args, kwargs))
|
||||||
|
print(args[0])
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_run_noprint(*args, **kwargs):
|
||||||
|
fake_result_tuple = namedtuple("fake_result", "returncode result")
|
||||||
|
res = fake_result_tuple(0, (args, kwargs))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def patch_relative_to(monkeysession):
|
||||||
|
def fake_relative_to(self, some_path: Path):
|
||||||
|
return self
|
||||||
|
|
||||||
|
monkeysession.setattr("pathlib.Path.relative_to", fake_relative_to)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo_manager(tmpdir_factory):
|
||||||
|
rm = RepoManager()
|
||||||
|
# rm.repos_folder = Path(str(tmpdir_factory.getbasetemp())) / 'repos'
|
||||||
|
return rm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(tmpdir):
|
||||||
|
repo_folder = Path(str(tmpdir)) / "repos" / "squid"
|
||||||
|
repo_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return Repo(
|
||||||
|
url="https://github.com/tekulvw/Squid-Plugins",
|
||||||
|
name="squid",
|
||||||
|
branch="rewrite_cogs",
|
||||||
|
folder_path=repo_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo_norun(repo):
|
||||||
|
repo._run = fake_run
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bot_repo(event_loop):
|
||||||
|
cwd = Path.cwd()
|
||||||
|
return Repo(
|
||||||
|
name="Red-DiscordBot",
|
||||||
|
branch="WRONG",
|
||||||
|
url="https://empty.com/something.git",
|
||||||
|
folder_path=cwd,
|
||||||
|
loop=event_loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Installable
|
||||||
|
INFO_JSON = {
|
||||||
|
"author": ("tekulvw",),
|
||||||
|
"bot_version": (3, 0, 0),
|
||||||
|
"description": "A long description",
|
||||||
|
"hidden": False,
|
||||||
|
"install_msg": "A post-installation message",
|
||||||
|
"required_cogs": {},
|
||||||
|
"requirements": ("tabulate"),
|
||||||
|
"short": "A short description",
|
||||||
|
"tags": ("tag1", "tag2"),
|
||||||
|
"type": "COG",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def installable(tmpdir):
|
||||||
|
cog_path = tmpdir.mkdir("test_repo").mkdir("test_cog")
|
||||||
|
info_path = cog_path.join("info.json")
|
||||||
|
info_path.write_text(json.dumps(INFO_JSON), "utf-8")
|
||||||
|
|
||||||
|
cog_info = Installable(Path(str(cog_path)))
|
||||||
|
return cog_info
|
||||||
15
redbot/pytest/economy.py
Normal file
15
redbot/pytest/economy.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["bank"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def bank(config, monkeypatch):
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
from redbot.core import bank
|
||||||
|
|
||||||
|
bank._register_defaults()
|
||||||
|
return bank
|
||||||
15
redbot/pytest/mod.py
Normal file
15
redbot/pytest/mod.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
__all__ = ["mod"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mod(config, monkeypatch):
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
from redbot.core import modlog
|
||||||
|
|
||||||
|
modlog._register_defaults()
|
||||||
|
return modlog
|
||||||
51
redbot/pytest/rpc.py
Normal file
51
redbot/pytest/rpc.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
from redbot.core.rpc import RPC, RPCMixin
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
__all__ = ["rpc", "rpcmixin", "cog", "existing_func", "existing_multi_func"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def rpc():
|
||||||
|
return RPC()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def rpcmixin():
|
||||||
|
r = RPCMixin()
|
||||||
|
r.rpc = MagicMock(spec=RPC)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cog():
|
||||||
|
class Cog:
|
||||||
|
async def cofunc(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cofunc2(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cofunc3(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Cog()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def existing_func(rpc, cog):
|
||||||
|
rpc.add_method(cog.cofunc)
|
||||||
|
|
||||||
|
return cog.cofunc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def existing_multi_func(rpc, cog):
|
||||||
|
funcs = [cog.cofunc, cog.cofunc2, cog.cofunc3]
|
||||||
|
rpc.add_multi_method(*funcs)
|
||||||
|
|
||||||
|
return funcs
|
||||||
@@ -32,7 +32,7 @@ if not config_dir:
|
|||||||
try:
|
try:
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
print("You don't have permission to write to " "'{}'\nExiting...".format(config_dir))
|
print("You don't have permission to write to '{}'\nExiting...".format(config_dir))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
config_file = config_dir / "config.json"
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ def get_data_dir():
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("You have chosen {} to be your data directory." "".format(default_data_dir))
|
print("You have chosen {} to be your data directory.".format(default_data_dir))
|
||||||
if not confirm("Please confirm (y/n):"):
|
if not confirm("Please confirm (y/n):"):
|
||||||
print("Please start the process over.")
|
print("Please start the process over.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
|
|||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
raven==6.5.0
|
raven==6.5.0
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
jsonrpcserver
|
aiohttp-json-rpc==0.8.7
|
||||||
pyyaml==3.12
|
pyyaml==3.12
|
||||||
fuzzywuzzy[speedup]<=0.16.0
|
fuzzywuzzy[speedup]<=0.16.0
|
||||||
Red-Trivia>=1.1.1
|
Red-Trivia>=1.1.1
|
||||||
|
|||||||
33
setup.py
33
setup.py
@@ -1,6 +1,9 @@
|
|||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
from distutils import ccompiler
|
||||||
|
from distutils.errors import CCompilerError, DistutilsPlatformError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -11,7 +14,9 @@ IS_TRAVIS = "TRAVIS" in os.environ
|
|||||||
IS_DEPLOYING = "DEPLOYING" in os.environ
|
IS_DEPLOYING = "DEPLOYING" in os.environ
|
||||||
IS_RTD = "READTHEDOCS" in os.environ
|
IS_RTD = "READTHEDOCS" in os.environ
|
||||||
|
|
||||||
dep_links = ["https://github.com/Rapptz/discord.py/tarball/rewrite#egg=discord.py-1.0"]
|
dep_links = [
|
||||||
|
"https://github.com/Rapptz/discord.py/tarball/7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0"
|
||||||
|
]
|
||||||
if IS_TRAVIS:
|
if IS_TRAVIS:
|
||||||
dep_links = []
|
dep_links = []
|
||||||
|
|
||||||
@@ -21,6 +26,20 @@ def get_package_list():
|
|||||||
return core
|
return core
|
||||||
|
|
||||||
|
|
||||||
|
def check_compiler_available():
|
||||||
|
m = ccompiler.new_compiler()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tdir:
|
||||||
|
with tempfile.NamedTemporaryFile(prefix="dummy", suffix=".c", dir=tdir) as tfile:
|
||||||
|
tfile.write(b"int main(int argc, char** argv) {return 0;}")
|
||||||
|
tfile.seek(0)
|
||||||
|
try:
|
||||||
|
m.compile([tfile.name], output_dir=tdir)
|
||||||
|
except (CCompilerError, DistutilsPlatformError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_requirements():
|
def get_requirements():
|
||||||
with open("requirements.txt") as f:
|
with open("requirements.txt") as f:
|
||||||
requirements = f.read().splitlines()
|
requirements = f.read().splitlines()
|
||||||
@@ -31,6 +50,9 @@ def get_requirements():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if not check_compiler_available(): # Can't compile python-Levensthein, so drop extra
|
||||||
|
requirements.remove("fuzzywuzzy[speedup]<=0.16.0")
|
||||||
|
requirements.append("fuzzywuzzy<=0.16.0")
|
||||||
if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD):
|
if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD):
|
||||||
requirements.append("discord.py>=1.0.0a0")
|
requirements.append("discord.py>=1.0.0a0")
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
@@ -89,10 +111,10 @@ setup(
|
|||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Framework :: AsyncIO",
|
"Framework :: AsyncIO",
|
||||||
|
"Framework :: Pytest",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Documentation :: Sphinx",
|
"Topic :: Documentation :: Sphinx",
|
||||||
@@ -102,9 +124,10 @@ setup(
|
|||||||
"redbot=redbot.__main__:main",
|
"redbot=redbot.__main__:main",
|
||||||
"redbot-setup=redbot.setup:main",
|
"redbot-setup=redbot.setup:main",
|
||||||
"redbot-launcher=redbot.launcher:main",
|
"redbot-launcher=redbot.launcher:main",
|
||||||
]
|
],
|
||||||
|
"pytest11": ["red-discordbot = redbot.pytest"],
|
||||||
},
|
},
|
||||||
python_requires=">=3.5,<3.7",
|
python_requires=">=3.6,<3.7",
|
||||||
setup_requires=get_requirements(),
|
setup_requires=get_requirements(),
|
||||||
install_requires=get_requirements(),
|
install_requires=get_requirements(),
|
||||||
dependency_links=dep_links,
|
dependency_links=dep_links,
|
||||||
@@ -113,6 +136,6 @@ setup(
|
|||||||
"mongo": ["motor"],
|
"mongo": ["motor"],
|
||||||
"docs": ["sphinx>=1.7", "sphinxcontrib-asyncio", "sphinx_rtd_theme"],
|
"docs": ["sphinx>=1.7", "sphinxcontrib-asyncio", "sphinx_rtd_theme"],
|
||||||
"voice": ["red-lavalink>=0.0.4"],
|
"voice": ["red-lavalink>=0.0.4"],
|
||||||
"style": ["black"],
|
"style": ["black==18.5b1"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,20 +2,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.admin import Admin
|
from redbot.pytest.admin import *
|
||||||
from redbot.cogs.admin.announcer import Announcer
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def admin(config):
|
|
||||||
return Admin(config)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def announcer(admin):
|
|
||||||
a = Announcer(MagicMock(), "Some message", admin.conf)
|
|
||||||
yield a
|
|
||||||
a.cancel()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
0
tests/cogs/dataconverter/__init__.py
Normal file
0
tests/cogs/dataconverter/__init__.py
Normal file
26
tests/cogs/dataconverter/data/mod/past_nicknames.json
Normal file
26
tests/cogs/dataconverter/data/mod/past_nicknames.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"1" : {
|
||||||
|
"1" : [
|
||||||
|
"Test",
|
||||||
|
"Test2",
|
||||||
|
"TEST3"
|
||||||
|
],
|
||||||
|
"2" : [
|
||||||
|
"Test4",
|
||||||
|
"Test5",
|
||||||
|
"TEST6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2" : {
|
||||||
|
"1" : [
|
||||||
|
"Test",
|
||||||
|
"Test2",
|
||||||
|
"TEST3"
|
||||||
|
],
|
||||||
|
"2" : [
|
||||||
|
"Test4",
|
||||||
|
"Test5",
|
||||||
|
"TEST6"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user