mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 01:12:33 -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/antispam.py @mikeshardmind
|
||||
redbot/core/utils/tunnel.py @mikeshardmind
|
||||
redbot/core/utils/caching.py @mikeshardmind
|
||||
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
@@ -44,6 +45,7 @@ redbot/cogs/trivia/* @Tobotimus
|
||||
redbot/cogs/dataconverter/* @mikeshardmind
|
||||
redbot/cogs/reports/* @mikeshardmind
|
||||
redbot/cogs/permissions/* @mikeshardmind
|
||||
redbot/cogs/warnings/* @palmtree5
|
||||
|
||||
# Docs
|
||||
docs/* @tekulvw @palmtree5
|
||||
|
||||
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
|
||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||
@@ -79,7 +79,7 @@ Note: If you haven't used `pipenv` before but are comfortable with virtualenvs,
|
||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||
|
||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on both python 3.5 and 3.6 (test environments `py35` and `py36` respectively)
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 (test environment `py36`)
|
||||
- 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`)
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
|
||||
|
||||
### 4.4 Make
|
||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||
1. `make reformat`: Reformat all python files in the project with Black
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,40 +1,15 @@
|
||||
# Trivia list repo injection
|
||||
redbot/trivia/
|
||||
|
||||
*.json
|
||||
*.exe
|
||||
*.dll
|
||||
.data
|
||||
!/tests/cogs/dataconverter/data/**/*.json
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.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:
|
||||
.idea/
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
4
Pipfile
4
Pipfile
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[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}
|
||||
|
||||
[dev-packages]
|
||||
@@ -14,7 +14,7 @@ pytest-asyncio = "*"
|
||||
sphinx = ">1.7"
|
||||
sphinxcontrib-asyncio = "*"
|
||||
sphinx-rtd-theme = "*"
|
||||
black = {version = "*", python_version = ">= '3.6'"}
|
||||
black = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
81
Pipfile.lock
generated
81
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d340e4a19777736703970e45766d05d67b973db38b87382b6ef8696cb53abb60"
|
||||
"sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -32,6 +32,13 @@
|
||||
],
|
||||
"version": "==2.2.5"
|
||||
},
|
||||
"aiohttp-json-rpc": {
|
||||
"hashes": [
|
||||
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
|
||||
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
|
||||
],
|
||||
"version": "==0.8.7"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
@@ -63,7 +70,7 @@
|
||||
"discord.py": {
|
||||
"editable": true,
|
||||
"git": "git://github.com/Rapptz/discord.py",
|
||||
"ref": "rewrite"
|
||||
"ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
@@ -76,13 +83,6 @@
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"funcsigs": {
|
||||
"hashes": [
|
||||
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
||||
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
||||
],
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
||||
@@ -97,19 +97,6 @@
|
||||
],
|
||||
"version": "==2.6"
|
||||
},
|
||||
"jsonrpcserver": {
|
||||
"hashes": [
|
||||
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
|
||||
],
|
||||
"version": "==3.5.4"
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
|
||||
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||
@@ -166,13 +153,6 @@
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||
@@ -248,19 +228,18 @@
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14",
|
||||
"sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80"
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.5.3"
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:4fec2566f9fbbd4a58de50a168cbe3ab952713530410d227e82e4c65d1fad946",
|
||||
"sha256:5fec0f25486046b9edb97961c946412ced96021247dd1a60ecd9f0567b68b030"
|
||||
"sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
|
||||
"sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==18.5b0"
|
||||
"version": "==18.6b2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@@ -369,11 +348,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d",
|
||||
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a"
|
||||
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
|
||||
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.6.0"
|
||||
"version": "==3.6.1"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
@@ -413,19 +392,19 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b",
|
||||
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10"
|
||||
"sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
|
||||
"sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.4"
|
||||
"version": "==1.7.5"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33",
|
||||
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d"
|
||||
"sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
|
||||
"sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.1"
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"sphinxcontrib-asyncio": {
|
||||
"hashes": [
|
||||
@@ -436,10 +415,16 @@
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9",
|
||||
"sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2"
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
|
||||
],
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
|
||||
73
README.rst
73
README.rst
@@ -1,46 +1,47 @@
|
||||
.. raw:: html
|
||||
.. class:: center
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red Discord Bot"></a>
|
||||
<br>
|
||||
Red Discord Bot
|
||||
<br>
|
||||
</h1>
|
||||
.. image:: https://imgur.com/pY1WUFX.png
|
||||
:target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
|
||||
:alt: Red Discord Bot
|
||||
|
||||
.. 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">
|
||||
<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>
|
||||
.. class:: center
|
||||
|
||||
.. raw:: html
|
||||
.. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
|
||||
:target: https://discord.gg/red
|
||||
:alt: Discord server
|
||||
|
||||
<p align="center">
|
||||
<a href="#overview">Overview</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
|
||||
<a href="#plugins"></a> •
|
||||
<a href="#join-the-community">Community</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
.. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
|
||||
:target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
|
||||
:alt: Travis CI status
|
||||
|
||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
||||
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/badge/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
|
||||
|
||||
@@ -37,6 +37,7 @@ For each of those, settings have varying priorities (listed below, highest to lo
|
||||
7. Role settings (see below)
|
||||
8. Server whitelist
|
||||
9. Server blacklist
|
||||
10. Default settings
|
||||
|
||||
For the role whitelist and blacklist settings,
|
||||
roles will be checked individually in order from highest to lowest role the user has
|
||||
@@ -73,3 +74,34 @@ An example of the expected format is shown below.
|
||||
- 96733288462286848
|
||||
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:
|
||||
:exclude-members: get_context
|
||||
|
||||
.. automethod:: register_rpc_handler
|
||||
.. automethod:: unregister_rpc_handler
|
||||
|
||||
Red
|
||||
^^^
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
Commands Package
|
||||
================
|
||||
|
||||
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same
|
||||
attributes. Some of these attributes, however, have been slightly modified, as outlined below.
|
||||
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||
they both have the same attributes. Some of these attributes, however, have been slightly modified,
|
||||
as outlined below.
|
||||
|
||||
.. 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
|
||||
is installed or a repo is added
|
||||
|
||||
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||
used for installing.
|
||||
|
||||
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||
is displayed when a user executes ``!cog list``
|
||||
|
||||
@@ -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)``
|
||||
|
||||
- ``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.
|
||||
Downloader will not deal with this functionality but it may be useful for other cogs.
|
||||
|
||||
@@ -4,36 +4,60 @@
|
||||
RPC
|
||||
===
|
||||
|
||||
.. currentmodule:: redbot.core.rpc
|
||||
|
||||
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
||||
Cogs must register functions to be exposed to RPC clients.
|
||||
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
||||
|
||||
To begin, register all methods using individual calls to the :func:`Methods.add` method.
|
||||
To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
|
||||
|
||||
********
|
||||
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
|
||||
*************
|
||||
|
||||
.. py:attribute:: redbot.core.rpc.methods
|
||||
|
||||
An instance of the :class:`Methods` class.
|
||||
All attempts to register new RPC methods **MUST** use this object.
|
||||
You should never create a new instance of the :class:`Methods` class!
|
||||
|
||||
RPC
|
||||
^^^
|
||||
.. autoclass:: redbot.core.rpc.RPC
|
||||
:members:
|
||||
|
||||
Methods
|
||||
^^^^^^^
|
||||
.. autoclass:: redbot.core.rpc.Methods
|
||||
:members:
|
||||
Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
|
||||
|
||||
@@ -17,8 +17,7 @@ you in the process.
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
||||
are on Windows, stick with 3.5). Open a terminal or command prompt and type
|
||||
To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
|
||||
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||
This will install the latest version of V3.
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
-i https://pypi.org/simple
|
||||
alabaster==0.7.10
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.1.5
|
||||
attrs==18.1.0
|
||||
babel==2.5.3
|
||||
babel==2.6.0
|
||||
black==18.6b2
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
click==6.7
|
||||
docutils==0.14
|
||||
idna==2.6
|
||||
imagesize==1.0.0
|
||||
jinja2==2.10
|
||||
markupsafe==1.0
|
||||
more-itertools==4.1.0
|
||||
more-itertools==4.2.0
|
||||
packaging==17.1
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pygments==2.2.0
|
||||
pyparsing==2.2.0
|
||||
pytest-asyncio==0.8.0
|
||||
pytest==3.5.1
|
||||
pytest==3.6.1
|
||||
pytz==2018.4
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
sphinx-rtd-theme==0.3.1
|
||||
sphinx==1.7.4
|
||||
sphinx-rtd-theme==0.4.0
|
||||
sphinx==1.7.5
|
||||
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
|
||||
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 os
|
||||
import sys
|
||||
@@ -13,25 +14,24 @@ def main():
|
||||
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
||||
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
||||
print(
|
||||
"Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(
|
||||
d
|
||||
)
|
||||
f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
|
||||
)
|
||||
exit(1)
|
||||
return 1
|
||||
else:
|
||||
print("Running 'regen_messages.py' for {}".format(d))
|
||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||
if retval.returncode != 0:
|
||||
exit(1)
|
||||
return 1
|
||||
os.chdir(root_dir)
|
||||
os.chdir("redbot/core/locales")
|
||||
print("Running 'regen_messages.py' for core")
|
||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||
if retval.returncode != 0:
|
||||
exit(1)
|
||||
return 1
|
||||
os.chdir(root_dir)
|
||||
subprocess.run(["crowdin", "upload"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
@@ -12,12 +12,3 @@ if discord.version_info.major < 1:
|
||||
" >= 1.0.0."
|
||||
)
|
||||
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
|
||||
from redbot.core.bot import Red, ExitCodes
|
||||
from redbot.core.cog_manager import CogManagerUI
|
||||
from redbot.core.data_manager import load_basic_configuration, config_file
|
||||
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
||||
from redbot.core.json_io import JsonIO
|
||||
from redbot.core.global_checks import init_global_checks
|
||||
from redbot.core.events import init_events
|
||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||
from redbot.core.core_commands import Core
|
||||
from redbot.core.dev_commands import Dev
|
||||
from redbot.core import rpc, __version__
|
||||
from redbot.core import __version__
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import logging
|
||||
@@ -40,7 +40,7 @@ def init_loggers(cli_flags):
|
||||
logger = logging.getLogger("red")
|
||||
|
||||
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]",
|
||||
)
|
||||
|
||||
@@ -106,12 +106,20 @@ def main():
|
||||
elif cli_flags.version:
|
||||
print(description)
|
||||
sys.exit(0)
|
||||
elif not cli_flags.instance_name:
|
||||
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||
print("Error: No instance name was provided!")
|
||||
sys.exit(1)
|
||||
if cli_flags.no_instance:
|
||||
print(
|
||||
"\033[1m"
|
||||
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
||||
"\033[0m"
|
||||
)
|
||||
cli_flags.instance_name = "temporary_red"
|
||||
create_temp_config()
|
||||
load_basic_configuration(cli_flags.instance_name)
|
||||
log, sentry_log = init_loggers(cli_flags)
|
||||
red = Red(cli_flags, description=description, pm_help=None)
|
||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||
init_global_checks(red)
|
||||
init_events(red, cli_flags)
|
||||
red.add_cog(Core(red))
|
||||
@@ -122,8 +130,10 @@ def main():
|
||||
tmp_data = {}
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||
if cli_flags.token:
|
||||
token = cli_flags.token
|
||||
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||
if token is None or not prefix:
|
||||
if not (token and prefix):
|
||||
if cli_flags.no_prompt is False:
|
||||
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
|
||||
if new_token:
|
||||
@@ -138,18 +148,16 @@ def main():
|
||||
sys.exit(0)
|
||||
if tmp_data["enable_sentry"]:
|
||||
red.enable_sentry()
|
||||
cleanup_tasks = True
|
||||
try:
|
||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
||||
except discord.LoginFailure:
|
||||
cleanup_tasks = False # No login happened, no need for this
|
||||
log.critical(
|
||||
"This token doesn't seem to be valid. If it belongs to "
|
||||
"a user account, remember that the --not-bot flag "
|
||||
"must be used. For self-bot functionalities instead, "
|
||||
"--self-bot"
|
||||
)
|
||||
db_token = red.db.token()
|
||||
db_token = loop.run_until_complete(red.db.token())
|
||||
if db_token and not cli_flags.no_prompt:
|
||||
print("\nDo you want to reset the token? (y/n)")
|
||||
if confirm("> "):
|
||||
@@ -164,10 +172,13 @@ def main():
|
||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||
loop.run_until_complete(red.logout())
|
||||
finally:
|
||||
if cleanup_tasks:
|
||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||
gathered.cancel()
|
||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||
gathered.cancel()
|
||||
try:
|
||||
red.rpc.server.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
sys.exit(red._shutdown_mode.value)
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Tuple
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core import Config, checks, commands
|
||||
|
||||
import logging
|
||||
|
||||
@@ -40,7 +39,6 @@ RUNNING_ANNOUNCEMENT = (
|
||||
|
||||
|
||||
class Admin:
|
||||
|
||||
def __init__(self, config=Config):
|
||||
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
|
||||
):
|
||||
"""
|
||||
Adds a role to a user. If user is left blank it defaults to the
|
||||
author of the command.
|
||||
Adds a role to a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -147,8 +145,8 @@ class Admin:
|
||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||
):
|
||||
"""
|
||||
Removes a role from a user. If user is left blank it defaults to the
|
||||
author of the command.
|
||||
Removes a role from a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
@@ -163,8 +161,7 @@ class Admin:
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def editrole(self, ctx: commands.Context):
|
||||
"""Edits roles settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@editrole.command(name="colour", aliases=["color"])
|
||||
async def editrole_colour(
|
||||
@@ -265,20 +262,16 @@ class Admin:
|
||||
@announce.command(name="ignore")
|
||||
@commands.guild_only()
|
||||
@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.
|
||||
Defaults to the current server if none is provided.
|
||||
Toggles whether the announcements will ignore the current server.
|
||||
"""
|
||||
if guild is None:
|
||||
guild = ctx.guild
|
||||
|
||||
ignored = await self.conf.guild(guild).announce_ignore()
|
||||
await self.conf.guild(guild).announce_ignore.set(not ignored)
|
||||
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||
|
||||
verb = "will" if ignored else "will not"
|
||||
|
||||
await ctx.send("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]:
|
||||
"""
|
||||
@@ -298,11 +291,13 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
return valid_roles
|
||||
|
||||
@commands.guild_only()
|
||||
@commands.group(invoke_without_command=True)
|
||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Add a role to yourself that server admins have configured as
|
||||
user settable.
|
||||
Add a role to yourself that server admins have configured as user settable.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._addrole(ctx, ctx.author, selfrole)
|
||||
@@ -311,15 +306,19 @@ class Admin:
|
||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Removes a selfrole from yourself.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._removerole(ctx, ctx.author, selfrole)
|
||||
|
||||
@selfrole.command(name="add")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""
|
||||
Add a role to the list of available selfroles.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
async with self.conf.guild(ctx.guild).selfroles() as 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.")
|
||||
|
||||
@selfrole.command(name="delete")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||
"""
|
||||
Removes a role from the list of available selfroles.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||
curr_selfroles.remove(role.id)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class Announcer:
|
||||
|
||||
def __init__(self, ctx: commands.Context, message: str, config=None):
|
||||
"""
|
||||
:param ctx:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class MemberDefaultAuthor(commands.Converter):
|
||||
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
|
||||
member_converter = commands.MemberConverter()
|
||||
try:
|
||||
@@ -17,7 +16,6 @@ class MemberDefaultAuthor(commands.Converter):
|
||||
|
||||
|
||||
class SelfRole(commands.Converter):
|
||||
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
||||
admin = ctx.command.instance
|
||||
if admin is None:
|
||||
@@ -30,5 +28,5 @@ class SelfRole(commands.Converter):
|
||||
role = await role_converter.convert(ctx, arg)
|
||||
|
||||
if role.id not in selfroles:
|
||||
raise commands.BadArgument("The provided role is not a valid" " selfrole.")
|
||||
raise commands.BadArgument("The provided role is not a valid selfrole.")
|
||||
return role
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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))
|
||||
|
||||
@@ -174,16 +174,14 @@ class Alias:
|
||||
@commands.guild_only()
|
||||
async def alias(self, ctx: commands.Context):
|
||||
"""Manage per-server aliases for commands"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@alias.group(name="global")
|
||||
async def global_(self, ctx: commands.Context):
|
||||
"""
|
||||
Manage global aliases.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@checks.mod_or_permissions(manage_guild=True)
|
||||
@alias.command(name="add")
|
||||
@@ -233,9 +231,7 @@ class Alias:
|
||||
|
||||
await self.add_alias(ctx, alias_name, command)
|
||||
|
||||
await ctx.send(
|
||||
_("A new alias with the trigger `{}`" " has been created.").format(alias_name)
|
||||
)
|
||||
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
|
||||
|
||||
@checks.is_owner()
|
||||
@global_.command(name="add")
|
||||
@@ -282,14 +278,14 @@ class Alias:
|
||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||
|
||||
await ctx.send(
|
||||
_("A new global alias with the trigger `{}`" " has been created.").format(alias_name)
|
||||
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
|
||||
)
|
||||
|
||||
@alias.command(name="help")
|
||||
@commands.guild_only()
|
||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""Tries to execute help for the base command of the alias"""
|
||||
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:
|
||||
base_cmd = alias.command[0]
|
||||
|
||||
@@ -307,9 +303,7 @@ class Alias:
|
||||
|
||||
if is_alias:
|
||||
await ctx.send(
|
||||
_("The `{}` alias will execute the" " command `{}`").format(
|
||||
alias_name, alias.command
|
||||
)
|
||||
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
|
||||
)
|
||||
else:
|
||||
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):
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
)
|
||||
else:
|
||||
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):
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||
|
||||
@@ -5,7 +5,6 @@ from redbot.core import commands
|
||||
|
||||
|
||||
class AliasEntry:
|
||||
|
||||
def __init__(
|
||||
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
|
||||
):
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from pathlib import Path
|
||||
from aiohttp import ClientSession
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from .audio import Audio
|
||||
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
|
||||
import redbot.core
|
||||
|
||||
log = logging.getLogger("red.audio")
|
||||
|
||||
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__)
|
||||
|
||||
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()
|
||||
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:
|
||||
log.info("Downloading Lavalink.jar")
|
||||
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())
|
||||
|
||||
session.close()
|
||||
|
||||
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)
|
||||
|
||||
bot.add_cog(cog)
|
||||
bot.loop.create_task(cog.disconnect_timer())
|
||||
bot.loop.create_task(cog.init_config())
|
||||
|
||||
@@ -6,6 +6,7 @@ import heapq
|
||||
import lavalink
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import redbot.core
|
||||
from redbot.core import Config, commands, checks, bank
|
||||
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__)
|
||||
|
||||
__version__ = "0.0.6a"
|
||||
__version__ = "0.0.6c"
|
||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Audio:
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
||||
@@ -38,6 +38,8 @@ class Audio:
|
||||
default_guild = {
|
||||
"dj_enabled": False,
|
||||
"dj_role": None,
|
||||
"emptydc_enabled": False,
|
||||
"emptydc_timer": 0,
|
||||
"jukebox": False,
|
||||
"jukebox_price": 0,
|
||||
"playlists": {},
|
||||
@@ -168,8 +170,7 @@ class Audio:
|
||||
@commands.guild_only()
|
||||
async def audioset(self, ctx):
|
||||
"""Music configuration options."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@audioset.command()
|
||||
@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._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()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def role(self, ctx, role_name: discord.Role):
|
||||
@@ -242,12 +263,16 @@ class Audio:
|
||||
global_data = await self.config.all()
|
||||
dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"])
|
||||
dj_enabled = data["dj_enabled"]
|
||||
emptydc_enabled = data["emptydc_enabled"]
|
||||
emptydc_timer = data["emptydc_timer"]
|
||||
jukebox = data["jukebox"]
|
||||
jukebox_price = data["jukebox_price"]
|
||||
jarbuild = redbot.core.__version__
|
||||
|
||||
vote_percent = data["vote_percent"]
|
||||
msg = "```ini\n" "----Server Settings----\n"
|
||||
if emptydc_enabled:
|
||||
msg += "Disconnect timer: [{0}]\n".format(self._dynamic_time(emptydc_timer))
|
||||
if dj_enabled:
|
||||
msg += "DJ Role: [{}]\n".format(dj_role_obj.name)
|
||||
if jukebox:
|
||||
@@ -426,7 +451,11 @@ class Audio:
|
||||
await message.add_reaction(expected[i])
|
||||
|
||||
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:
|
||||
(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()
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -590,7 +625,7 @@ class Audio:
|
||||
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
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:
|
||||
for track in tracks:
|
||||
@@ -603,7 +638,7 @@ class Audio:
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(
|
||||
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:
|
||||
@@ -619,11 +654,11 @@ class Audio:
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(
|
||||
text="{} until track playback: #{} in queue".format(
|
||||
queue_total_duration, before_queue_length
|
||||
queue_total_duration, before_queue_length + 1
|
||||
)
|
||||
)
|
||||
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:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
@@ -632,8 +667,7 @@ class Audio:
|
||||
@commands.guild_only()
|
||||
async def playlist(self, ctx):
|
||||
"""Playlist configuration options."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@playlist.command(name="append")
|
||||
async def _playlist_append(self, ctx, playlist_name, *url):
|
||||
@@ -680,8 +714,10 @@ class Audio:
|
||||
return await self._embed_msg(
|
||||
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)
|
||||
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))
|
||||
|
||||
@playlist.command(name="delete")
|
||||
@@ -740,6 +776,7 @@ class Audio:
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.cooldown(1, 15, discord.ext.commands.BucketType.guild)
|
||||
@playlist.command(name="queue")
|
||||
async def _playlist_queue(self, ctx, playlist_name=None):
|
||||
"""Save the queue to a playlist."""
|
||||
@@ -766,11 +803,11 @@ class Audio:
|
||||
await self._embed_msg(ctx, "Please enter a name for this playlist.")
|
||||
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
return m.author == ctx.author and not m.content.startswith(ctx.prefix)
|
||||
|
||||
try:
|
||||
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:
|
||||
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
|
||||
if playlist_name in playlists:
|
||||
@@ -781,11 +818,12 @@ class Audio:
|
||||
return await self._embed_msg(ctx, "No playlist name entered, try again later.")
|
||||
playlist_list = self._to_json(ctx, None, tracklist)
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(
|
||||
ctx,
|
||||
"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)
|
||||
if tracklist is not None:
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlist_name = playlist_name.split(" ")[0].strip('"')
|
||||
playlists[playlist_name] = playlist_list
|
||||
return await self._embed_msg(
|
||||
ctx,
|
||||
@@ -891,8 +930,11 @@ class Audio:
|
||||
file_suffix = file_url.rsplit(".", 1)[1]
|
||||
if file_suffix != "txt":
|
||||
return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
|
||||
async with self.session.request("GET", file_url) as r:
|
||||
v2_playlist = await r.json(content_type="text/plain")
|
||||
try:
|
||||
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:
|
||||
v2_playlist_url = v2_playlist["link"]
|
||||
except KeyError:
|
||||
@@ -961,6 +1003,12 @@ class Audio:
|
||||
return False
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -1178,6 +1226,12 @@ class Audio:
|
||||
"""
|
||||
if not self._player_check(ctx):
|
||||
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)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
@@ -1195,10 +1249,10 @@ class Audio:
|
||||
|
||||
query = query.strip("<>")
|
||||
if query.startswith("list "):
|
||||
query = "ytsearch:{}".format(query.lstrip("list "))
|
||||
query = "ytsearch:{}".format(query.replace("list ", ""))
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, "Nothing found 👀")
|
||||
return await self._embed_msg(ctx, "Nothing found.")
|
||||
songembed = discord.Embed(
|
||||
colour=ctx.guild.me.top_role.colour,
|
||||
title="Queued {} track(s).".format(len(tracks)),
|
||||
@@ -1217,12 +1271,12 @@ class Audio:
|
||||
await player.play()
|
||||
return await ctx.send(embed=songembed)
|
||||
if query.startswith("sc "):
|
||||
query = "scsearch:{}".format(query.lstrip("sc "))
|
||||
query = "scsearch:{}".format(query.replace("sc ", ""))
|
||||
elif not query.startswith("http"):
|
||||
query = "ytsearch:{}".format(query)
|
||||
tracks = await player.get_tracks(query)
|
||||
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)
|
||||
search_page_list = []
|
||||
@@ -1485,11 +1539,13 @@ class Audio:
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _skip_action(ctx):
|
||||
async def _skip_action(self, ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
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)
|
||||
if player.current.is_stream:
|
||||
embed = discord.Embed(
|
||||
@@ -1593,8 +1649,7 @@ class Audio:
|
||||
@checks.is_owner()
|
||||
async def llsetup(self, ctx):
|
||||
"""Lavalink server configuration options."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@llsetup.command()
|
||||
async def external(self, ctx):
|
||||
@@ -1711,6 +1766,34 @@ class Audio:
|
||||
if player.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
|
||||
async def _draw_time(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
@@ -1826,6 +1909,15 @@ class Audio:
|
||||
track_obj[key] = value
|
||||
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):
|
||||
if after.channel != before.channel:
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,9 @@ import asyncio
|
||||
from subprocess import Popen, DEVNULL, PIPE
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
_JavaVersion = Tuple[int, int]
|
||||
|
||||
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
|
||||
if not java_available:
|
||||
return False
|
||||
return False, None
|
||||
|
||||
version = await get_java_version(loop)
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -36,15 +38,17 @@ def check_global_setting_admin():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -58,8 +62,9 @@ class Bank:
|
||||
|
||||
# SECTION commands
|
||||
|
||||
@commands.group()
|
||||
@check_global_setting_guildowner()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.group(autohelp=True)
|
||||
async def bankset(self, ctx: commands.Context):
|
||||
"""Base command for bank settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
@@ -69,17 +74,15 @@ class Bank:
|
||||
default_balance = await bank._conf.default_balance()
|
||||
else:
|
||||
if not ctx.guild:
|
||||
await ctx.send_help()
|
||||
return
|
||||
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
||||
currency_name = await bank._conf.guild(ctx.guild).currency()
|
||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||
|
||||
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)
|
||||
await ctx.send(box(settings))
|
||||
await ctx.send_help()
|
||||
|
||||
@bankset.command(name="toggleglobal")
|
||||
@checks.is_owner()
|
||||
|
||||
@@ -71,11 +71,11 @@ class Cleanup:
|
||||
to_delete = []
|
||||
too_old = False
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
while not too_old and len(to_delete) < number:
|
||||
message = None
|
||||
async for message in channel.history(limit=limit, before=before, after=after):
|
||||
if (
|
||||
(not number or len(to_delete) - 1 < number)
|
||||
(not number or len(to_delete) < number)
|
||||
and check(message)
|
||||
and (ctx.message.created_at - message.created_at).days < 14
|
||||
and (delete_pinned or not message.pinned)
|
||||
@@ -96,12 +96,10 @@ class Cleanup:
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: commands.Context):
|
||||
"""Deletes messages."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def text(
|
||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
@@ -113,6 +111,10 @@ class Cleanup:
|
||||
Remember to use double quotes."""
|
||||
|
||||
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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -139,7 +141,7 @@ class Cleanup:
|
||||
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
|
||||
)
|
||||
log.info(reason)
|
||||
@@ -151,7 +153,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(
|
||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
@@ -160,6 +161,10 @@ class Cleanup:
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
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
|
||||
try:
|
||||
@@ -172,7 +177,6 @@ class Cleanup:
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -213,7 +217,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||
"""Deletes all messages after specified message.
|
||||
|
||||
@@ -225,11 +228,14 @@ class Cleanup:
|
||||
"""
|
||||
|
||||
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
|
||||
is_bot = self.bot.user.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
|
||||
|
||||
after = await channel.get_message(message_id)
|
||||
@@ -242,7 +248,7 @@ class Cleanup:
|
||||
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
|
||||
)
|
||||
log.info(reason)
|
||||
@@ -251,7 +257,6 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Deletes last X messages.
|
||||
|
||||
@@ -259,6 +264,9 @@ class Cleanup:
|
||||
cleanup messages 26"""
|
||||
|
||||
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
|
||||
|
||||
is_bot = self.bot.user.bot
|
||||
@@ -273,7 +281,7 @@ class Cleanup:
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}." "".format(
|
||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||
author.name, author.id, number, channel.name
|
||||
)
|
||||
log.info(reason)
|
||||
@@ -285,11 +293,13 @@ class Cleanup:
|
||||
|
||||
@cleanup.command(name="bot")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Cleans up command messages and messages from the bot."""
|
||||
|
||||
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
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -413,7 +423,7 @@ class Cleanup:
|
||||
if author == self.bot.user:
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
if channel.name:
|
||||
if ctx.guild:
|
||||
channel_name = "channel " + channel.name
|
||||
else:
|
||||
channel_name = str(channel)
|
||||
|
||||
@@ -25,7 +25,6 @@ class AlreadyExists(CCError):
|
||||
|
||||
|
||||
class CommandObj:
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
config = kwargs.get("config")
|
||||
self.bot = kwargs.get("bot")
|
||||
@@ -43,7 +42,7 @@ class CommandObj:
|
||||
intro = _(
|
||||
"Welcome to the interactive random {} maker!\n"
|
||||
"Every message you send will be added as one of the random "
|
||||
"response to choose from once this {} is "
|
||||
"responses to choose from once this {} is "
|
||||
"triggered. To exit this interactive menu, type `{}`"
|
||||
).format("customcommand", "customcommand", "exit()")
|
||||
await ctx.send(intro)
|
||||
@@ -75,7 +74,7 @@ class CommandObj:
|
||||
return ccinfo["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
|
||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
raise AlreadyExists()
|
||||
@@ -132,6 +131,7 @@ class CommandObj:
|
||||
@cog_i18n(_)
|
||||
class CustomCommands:
|
||||
"""Custom commands
|
||||
|
||||
Creates commands used to display text"""
|
||||
|
||||
def __init__(self, bot):
|
||||
@@ -141,12 +141,11 @@ class CustomCommands:
|
||||
self.config.register_guild(commands={})
|
||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||
|
||||
@commands.group(aliases=["cc"], no_pm=True)
|
||||
@commands.group(aliases=["cc"])
|
||||
@commands.guild_only()
|
||||
async def customcom(self, ctx: commands.Context):
|
||||
"""Custom commands management"""
|
||||
if not ctx.invoked_subcommand:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@customcom.group(name="add")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@@ -166,14 +165,14 @@ class CustomCommands:
|
||||
|
||||
{server} message.guild
|
||||
"""
|
||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@cc_add.command(name="random")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_random(self, ctx: commands.Context, command: str):
|
||||
"""
|
||||
Create a CC where it will randomly choose a response!
|
||||
|
||||
Note: This is interactive
|
||||
"""
|
||||
channel = ctx.channel
|
||||
@@ -185,7 +184,7 @@ class CustomCommands:
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
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)
|
||||
)
|
||||
)
|
||||
@@ -196,6 +195,7 @@ class CustomCommands:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_simple(self, ctx, command: str, *, text):
|
||||
"""Adds a simple custom command
|
||||
|
||||
Example:
|
||||
[p]customcom add simple yourcommand Text you want
|
||||
"""
|
||||
@@ -209,7 +209,7 @@ class CustomCommands:
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
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)
|
||||
)
|
||||
)
|
||||
@@ -218,6 +218,7 @@ class CustomCommands:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_edit(self, ctx, command: str, *, text=None):
|
||||
"""Edits a custom command
|
||||
|
||||
Example:
|
||||
[p]customcom edit yourcommand Text you want
|
||||
"""
|
||||
@@ -229,7 +230,7 @@ class CustomCommands:
|
||||
await ctx.send(_("Custom command successfully edited."))
|
||||
except NotFound:
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -119,11 +119,10 @@ class SpecResolver(object):
|
||||
def past_nicknames_conv_spec(self, data: dict):
|
||||
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
||||
ret = {}
|
||||
for k, v in flatscoped.items():
|
||||
outerkey, innerkey = (*k[:-1],), (k[-1],)
|
||||
if outerkey not in ret:
|
||||
ret[outerkey] = {}
|
||||
ret[outerkey].update({innerkey: v})
|
||||
for config_identifiers, v2data in flatscoped.items():
|
||||
if config_identifiers not in ret:
|
||||
ret[config_identifiers] = {}
|
||||
ret[config_identifiers].update({("past_nicks",): v2data})
|
||||
return ret
|
||||
|
||||
def customcom_conv_spec(self, data: dict):
|
||||
@@ -144,18 +143,28 @@ class SpecResolver(object):
|
||||
ret[outerkey].update({innerkey: ccinfo})
|
||||
return ret
|
||||
|
||||
async def convert(self, bot: Red, prettyname: str):
|
||||
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"]
|
||||
def get_config_object(self, bot, cogname, attr, _id):
|
||||
try:
|
||||
config = getattr(bot.get_cog(cogname), attr)
|
||||
except (TypeError, AttributeError):
|
||||
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:
|
||||
items = converter(dc.json_load(filepath))
|
||||
await dc(config).dict_import(items)
|
||||
|
||||
@@ -41,7 +41,7 @@ class DataConverter:
|
||||
)
|
||||
)
|
||||
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):
|
||||
menu += "\n{}. {}".format(index, entry)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
__all__ = ["install_agreement"]
|
||||
__all__ = ["do_install_agreement"]
|
||||
|
||||
REPO_INSTALL_MSG = (
|
||||
"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 pred(ctx: commands.Context):
|
||||
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
|
||||
async def do_install_agreement(ctx: commands.Context):
|
||||
downloader = ctx.cog
|
||||
if downloader is None or downloader.already_agreed:
|
||||
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
|
||||
from discord.ext import commands
|
||||
from .repo_manager import RepoManager
|
||||
from redbot.core import commands
|
||||
from .installable import Installable
|
||||
|
||||
|
||||
class InstalledCog(commands.Converter):
|
||||
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
||||
downloader = ctx.bot.get_cog("Downloader")
|
||||
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.bot import Red
|
||||
from .checks import install_agreement
|
||||
from .checks import do_install_agreement
|
||||
from .converters import InstalledCog
|
||||
from .errors import CloningError, ExistingGitRepo
|
||||
from .installable import Installable
|
||||
@@ -27,7 +27,6 @@ _ = Translator("Downloader", __file__)
|
||||
|
||||
@cog_i18n(_)
|
||||
class Downloader:
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
@@ -211,11 +210,9 @@ class Downloader:
|
||||
"""
|
||||
Command group for managing Downloader repos.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@repo.command(name="add")
|
||||
@install_agreement()
|
||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||
"""
|
||||
Add a new repo to Downloader.
|
||||
@@ -223,6 +220,9 @@ class Downloader:
|
||||
Name can only contain characters A-z, numbers and underscore
|
||||
Branch will default to master if not specified
|
||||
"""
|
||||
agreed = await do_install_agreement(ctx)
|
||||
if not agreed:
|
||||
return
|
||||
try:
|
||||
# noinspection PyTypeChecker
|
||||
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||
@@ -234,7 +234,7 @@ class Downloader:
|
||||
else:
|
||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||
if repo.install_msg is not None:
|
||||
await ctx.send(repo.install_msg)
|
||||
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@repo.command(name="delete")
|
||||
async def _repo_del(self, ctx, repo_name: Repo):
|
||||
@@ -278,8 +278,7 @@ class Downloader:
|
||||
"""
|
||||
Command group for managing installable Cogs.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@cog.command(name="install")
|
||||
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
|
||||
if cog is None:
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -306,7 +305,7 @@ class Downloader:
|
||||
|
||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
||||
await ctx.send(
|
||||
_("Failed to install the required libraries for" " `{}`: `{}`").format(
|
||||
_("Failed to install the required libraries for `{}`: `{}`").format(
|
||||
cog.name, cog.requirements
|
||||
)
|
||||
)
|
||||
@@ -320,7 +319,7 @@ class Downloader:
|
||||
|
||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||
if cog.install_msg is not None:
|
||||
await ctx.send(cog.install_msg)
|
||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@cog.command(name="uninstall")
|
||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||
@@ -381,11 +380,25 @@ class Downloader:
|
||||
"""
|
||||
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 = _("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):
|
||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||
|
||||
@@ -401,7 +414,9 @@ class Downloader:
|
||||
)
|
||||
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))
|
||||
|
||||
async def is_installed(
|
||||
|
||||
@@ -17,6 +17,7 @@ class DownloaderException(Exception):
|
||||
"""
|
||||
Base class for Downloader exceptions.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -31,6 +32,7 @@ class InvalidRepoName(DownloaderException):
|
||||
Throw when a repo name is invalid. Check
|
||||
the message for a more detailed reason.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -39,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
|
||||
Thrown when trying to clone into a folder where a
|
||||
git repo already exists.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -47,6 +50,7 @@ class MissingGitRepo(DownloaderException):
|
||||
Thrown when a git repo is expected to exist but
|
||||
does not.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -54,6 +58,7 @@ class CloningError(GitException):
|
||||
"""
|
||||
Thrown when git clone returns a non zero exit code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -62,6 +67,7 @@ class CurrentHashError(GitException):
|
||||
Thrown when git returns a non zero exit code attempting
|
||||
to determine the current commit hash.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -70,6 +76,7 @@ class HardResetError(GitException):
|
||||
Thrown when there is an issue trying to execute a hard reset
|
||||
(usually prior to a repo update).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -77,6 +84,7 @@ class UpdateError(GitException):
|
||||
"""
|
||||
Thrown when git pull returns a non zero error code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -84,6 +92,7 @@ class GitDiffError(GitException):
|
||||
"""
|
||||
Thrown when a git diff fails.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -91,4 +100,5 @@ class PipError(DownloaderException):
|
||||
"""
|
||||
Thrown when pip returns a non-zero return code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -3,9 +3,8 @@ import distutils.dir_util
|
||||
import shutil
|
||||
from enum import Enum
|
||||
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 .json_mixins import RepoJSONMixin
|
||||
|
||||
@@ -76,6 +75,7 @@ class Installable(RepoJSONMixin):
|
||||
self.bot_version = (3, 0, 0)
|
||||
self.min_python_version = (3, 5, 1)
|
||||
self.hidden = False
|
||||
self.disabled = False
|
||||
self.required_cogs = {} # Cog name -> repo URL
|
||||
self.requirements = ()
|
||||
self.tags = ()
|
||||
@@ -117,7 +117,7 @@ class Installable(RepoJSONMixin):
|
||||
try:
|
||||
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||
except:
|
||||
log.exception("Error occurred when copying path:" " {}".format(self._location))
|
||||
log.exception("Error occurred when copying path: {}".format(self._location))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -146,9 +146,7 @@ class Installable(RepoJSONMixin):
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
info = {}
|
||||
log.exception(
|
||||
"Invalid JSON information file at path:" " {}".format(info_file_path)
|
||||
)
|
||||
log.exception("Invalid JSON information file at path: {}".format(info_file_path))
|
||||
else:
|
||||
self._info = info
|
||||
|
||||
@@ -176,6 +174,12 @@ class Installable(RepoJSONMixin):
|
||||
hidden = False
|
||||
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.requirements = info.get("requirements", ())
|
||||
|
||||
@@ -2,17 +2,13 @@ import asyncio
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import shutil
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from subprocess import run as sp_run, PIPE
|
||||
from sys import executable
|
||||
from typing import Tuple, MutableMapping, Union
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core import data_manager
|
||||
from redbot.core import data_manager, commands
|
||||
from redbot.core.utils import safe_delete
|
||||
from .errors import *
|
||||
from .installable import Installable, InstallableType
|
||||
@@ -27,10 +23,8 @@ class Repo(RepoJSONMixin):
|
||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
||||
GIT_DIFF_FILE_STATUS = (
|
||||
"git -C {path} diff --no-commit-id --name-status" " {old_hash} {new_hash}"
|
||||
)
|
||||
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.." " {relative_file_path}"
|
||||
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
||||
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
|
||||
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
||||
|
||||
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
||||
@@ -98,7 +92,7 @@ class Repo(RepoJSONMixin):
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@@ -222,7 +216,7 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
if p.returncode != 0:
|
||||
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()
|
||||
@@ -472,7 +466,7 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
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
|
||||
@@ -495,7 +489,6 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
|
||||
class RepoManager:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._repos = {}
|
||||
|
||||
@@ -9,7 +9,8 @@ import discord
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
@@ -74,7 +75,6 @@ SLOT_PAYOUTS_MSG = _(
|
||||
|
||||
|
||||
def guild_only_check():
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
if await bank.is_global():
|
||||
return True
|
||||
@@ -87,7 +87,6 @@ def guild_only_check():
|
||||
|
||||
|
||||
class SetParser:
|
||||
|
||||
def __init__(self, argument):
|
||||
allowed = ("+", "-")
|
||||
self.sum = int(argument)
|
||||
@@ -139,11 +138,11 @@ class Economy:
|
||||
self.config.register_role(**self.default_role_settings)
|
||||
self.slot_register = defaultdict(dict)
|
||||
|
||||
@guild_only_check()
|
||||
@commands.group(name="bank")
|
||||
async def _bank(self, ctx: commands.Context):
|
||||
"""Bank operations"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@_bank.command()
|
||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||
@@ -212,7 +211,6 @@ class Economy:
|
||||
)
|
||||
|
||||
@_bank.command()
|
||||
@guild_only_check()
|
||||
@check_global_setting_guildowner()
|
||||
async def reset(self, ctx, confirmation: bool = False):
|
||||
"""Deletes bank accounts"""
|
||||
@@ -228,13 +226,13 @@ class Economy:
|
||||
else:
|
||||
await bank.wipe_bank()
|
||||
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"
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
@commands.command()
|
||||
async def payday(self, ctx: commands.Context):
|
||||
"""Get some free currency"""
|
||||
author = ctx.author
|
||||
@@ -254,7 +252,7 @@ class Economy:
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
"You are currently #{4} on the global leaderboard!"
|
||||
).format(
|
||||
author,
|
||||
credits_name,
|
||||
@@ -267,7 +265,7 @@ class Economy:
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -301,7 +299,7 @@ class Economy:
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -312,8 +310,8 @@ class Economy:
|
||||
"""Prints out the leaderboard
|
||||
|
||||
Defaults to top 10"""
|
||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
if top < 1:
|
||||
top = 10
|
||||
if (
|
||||
@@ -323,25 +321,25 @@ class Economy:
|
||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
highscore = ""
|
||||
for pos, acc in enumerate(bank_sorted, 1):
|
||||
pos = pos
|
||||
poswidth = 2
|
||||
name = acc[1]["name"]
|
||||
namewidth = 35
|
||||
balance = acc[1]["balance"]
|
||||
balwidth = 2
|
||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
||||
pos=pos,
|
||||
poswidth=poswidth,
|
||||
name=name,
|
||||
namewidth=namewidth,
|
||||
balance=balance,
|
||||
balwidth=balwidth,
|
||||
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
||||
highscores = [
|
||||
(
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
if acc[0] != author.id
|
||||
else (
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
for pos, acc in enumerate(bank_sorted, 1)
|
||||
]
|
||||
if highscores:
|
||||
pages = [
|
||||
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
||||
for x in range(0, len(highscores), 10)
|
||||
]
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(_("There are no accounts in the bank."))
|
||||
|
||||
@@ -427,7 +425,7 @@ class Economy:
|
||||
now = then - bid + pay
|
||||
await bank.set_balance(author, now)
|
||||
await channel.send(
|
||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!" "").format(
|
||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, payout["phrase"], bid, then, now
|
||||
)
|
||||
)
|
||||
@@ -436,7 +434,7 @@ class Economy:
|
||||
await bank.withdraw_credits(author, bid)
|
||||
now = then - bid
|
||||
await channel.send(
|
||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!" "").format(
|
||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, bid, then, now
|
||||
)
|
||||
)
|
||||
@@ -448,7 +446,6 @@ class Economy:
|
||||
"""Changes economy module settings"""
|
||||
guild = ctx.guild
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
if await bank.is_global():
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
slot_max = await self.config.SLOT_MAX()
|
||||
@@ -497,7 +494,7 @@ class Economy:
|
||||
"""Maximum slot machine bid"""
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
if bid < 1 or bid < slot_min:
|
||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater" " than slotmin."))
|
||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
|
||||
return
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
@@ -526,9 +523,7 @@ class Economy:
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||
await ctx.send(
|
||||
_("Value modified. At least {} seconds must pass " "between each payday.").format(
|
||||
seconds
|
||||
)
|
||||
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
|
||||
)
|
||||
|
||||
@economyset.command()
|
||||
@@ -543,7 +538,7 @@ class Economy:
|
||||
await self.config.PAYDAY_CREDITS.set(creds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(_("Every payday will now give {} {}." "").format(creds, credits_name))
|
||||
await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
|
||||
|
||||
@economyset.command()
|
||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||
@@ -555,7 +550,7 @@ class Economy:
|
||||
else:
|
||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -569,7 +564,7 @@ class Economy:
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
await bank.set_default_balance(creds, guild)
|
||||
await ctx.send(
|
||||
_("Registering an account will now give {} {}." "").format(creds, credits_name)
|
||||
_("Registering an account will now give {} {}.").format(creds, credits_name)
|
||||
)
|
||||
|
||||
# What would I ever do without stackoverflow?
|
||||
|
||||
@@ -49,7 +49,6 @@ class Filter:
|
||||
Using this command with no subcommands will send
|
||||
the list of the server's filtered words."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
server = ctx.guild
|
||||
author = ctx.author
|
||||
word_list = await self.settings.guild(server).filter()
|
||||
@@ -124,36 +123,35 @@ class Filter:
|
||||
|
||||
@_filter.command(name="names")
|
||||
async def filter_names(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggles whether or not to check names and nicknames against the filter
|
||||
"""Toggles whether or not to check names and nicknames against the filter
|
||||
|
||||
This is disabled by default
|
||||
"""
|
||||
guild = ctx.guild
|
||||
current_setting = await self.settings.guild(guild).filter_names()
|
||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||
if current_setting:
|
||||
await ctx.send(
|
||||
_("Names and nicknames will no longer be " "checked against the filter")
|
||||
)
|
||||
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
|
||||
else:
|
||||
await ctx.send(_("Names and nicknames will now be checked against " "the filter"))
|
||||
await ctx.send(_("Names and nicknames will now be checked against the filter."))
|
||||
|
||||
@_filter.command(name="defaultname")
|
||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||
"""
|
||||
Sets the default name to use if filtering names is enabled
|
||||
"""Sets the default name to use if filtering names is enabled
|
||||
|
||||
Note that this has no effect if filtering names is disabled
|
||||
|
||||
The default name used is John Doe
|
||||
"""
|
||||
guild = ctx.guild
|
||||
await self.settings.guild(guild).filter_default_name.set(name)
|
||||
await ctx.send(_("The name to use on filtered names has been set"))
|
||||
await ctx.send(_("The name to use on filtered names has been set."))
|
||||
|
||||
@_filter.command(name="ban")
|
||||
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||
"""
|
||||
Sets up an autoban if the specified number of messages are
|
||||
filtered in the specified amount of time (in seconds)
|
||||
"""Autobans if the specified number of messages are filtered in the timeframe
|
||||
|
||||
The timeframe is represented by seconds.
|
||||
"""
|
||||
if (count <= 0) != (timeframe <= 0):
|
||||
await ctx.send(
|
||||
@@ -223,7 +221,7 @@ class Filter:
|
||||
user_count >= filter_count
|
||||
and message.created_at.timestamp() < next_reset_time
|
||||
):
|
||||
reason = "Autoban (too many filtered messages)"
|
||||
reason = "Autoban (too many filtered messages.)"
|
||||
try:
|
||||
await server.ban(author, reason=reason)
|
||||
except:
|
||||
|
||||
@@ -21,7 +21,6 @@ class RPS(Enum):
|
||||
|
||||
|
||||
class RPSParser:
|
||||
|
||||
def __init__(self, argument):
|
||||
argument = argument.lower()
|
||||
if argument == "rock":
|
||||
@@ -98,7 +97,7 @@ class General:
|
||||
msg = ""
|
||||
if user.id == ctx.bot.user.id:
|
||||
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"
|
||||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||||
table = str.maketrans(char, tran)
|
||||
@@ -192,18 +191,12 @@ class General:
|
||||
async def serverinfo(self, ctx):
|
||||
"""Shows server's informations"""
|
||||
guild = ctx.guild
|
||||
online = len(
|
||||
[
|
||||
m.status
|
||||
for m in guild.members
|
||||
if m.status == discord.Status.online or m.status == discord.Status.idle
|
||||
]
|
||||
)
|
||||
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||
total_users = len(guild.members)
|
||||
text_channels = len(guild.text_channels)
|
||||
voice_channels = len(guild.voice_channels)
|
||||
passed = (ctx.message.created_at - guild.created_at).days
|
||||
created_at = _("Since {}. That's over {} days ago!" "").format(
|
||||
created_at = _("Since {}. That's over {} days ago!").format(
|
||||
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
||||
)
|
||||
|
||||
@@ -228,7 +221,7 @@ class General:
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
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()
|
||||
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
||||
@@ -265,7 +258,7 @@ class General:
|
||||
definition = item_list[pos]["definition"]
|
||||
example = item_list[pos]["example"]
|
||||
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
|
||||
)
|
||||
msg = pagify(msg, ["\n"])
|
||||
|
||||
@@ -13,6 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
@cog_i18n(_)
|
||||
class Image:
|
||||
"""Image related commands."""
|
||||
|
||||
default_global = {"imgur_client_id": None}
|
||||
|
||||
def __init__(self, bot):
|
||||
@@ -31,8 +32,7 @@ class Image:
|
||||
|
||||
Make sure to set the client ID using
|
||||
[p]imgurcreds"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@_imgur.command(name="search")
|
||||
async def imgur_search(self, ctx, *, term: str):
|
||||
@@ -42,7 +42,7 @@ class Image:
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}").format(
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
@@ -54,7 +54,7 @@ class Image:
|
||||
if data["success"]:
|
||||
results = data["data"]
|
||||
if not results:
|
||||
await ctx.send(_("Your search returned no results"))
|
||||
await ctx.send(_("Your search returned no results."))
|
||||
return
|
||||
shuffle(results)
|
||||
msg = _("Search results...\n")
|
||||
@@ -63,7 +63,7 @@ class Image:
|
||||
msg += "\n"
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
|
||||
@_imgur.command(name="subreddit")
|
||||
async def imgur_subreddit(
|
||||
@@ -91,7 +91,7 @@ class Image:
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}").format(
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
@@ -116,12 +116,13 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command()
|
||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||
"""Sets the imgur client id
|
||||
|
||||
You will need an account on Imgur to get this
|
||||
|
||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
||||
@@ -130,7 +131,7 @@ class Image:
|
||||
set the authorization callback url to 'https://localhost'
|
||||
leave the app website blank, enter a valid email address, and
|
||||
enter a description. Check the box for the captcha, then click Next.
|
||||
Your client ID will be on the page that loads"""
|
||||
Your client ID will be on the page that loads."""
|
||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||
await ctx.send(_("Set the imgur client id!"))
|
||||
|
||||
@@ -143,7 +144,7 @@ class Image:
|
||||
await ctx.send_help()
|
||||
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
|
||||
)
|
||||
|
||||
@@ -155,7 +156,7 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API"))
|
||||
await ctx.send(_("Error contacting the API."))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
async def gifr(self, ctx, *keywords):
|
||||
@@ -166,7 +167,7 @@ class Image:
|
||||
await ctx.send_help()
|
||||
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
|
||||
)
|
||||
|
||||
@@ -178,4 +179,4 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
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
|
||||
|
||||
|
||||
def mod_or_voice_permissions(**perms):
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -31,7 +30,6 @@ def mod_or_voice_permissions(**perms):
|
||||
|
||||
|
||||
def admin_or_voice_permissions(**perms):
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -54,7 +52,6 @@ def admin_or_voice_permissions(**perms):
|
||||
|
||||
|
||||
def bot_has_voice_permissions(**perms):
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
guild = ctx.guild
|
||||
for vc in guild.voice_channels:
|
||||
|
||||
@@ -168,8 +168,6 @@ class Mod:
|
||||
"""Manages server administration settings."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
guild = ctx.guild
|
||||
await ctx.send_help()
|
||||
|
||||
# Display current settings
|
||||
delete_repeats = await self.settings.guild(guild).delete_repeats()
|
||||
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
|
||||
@@ -199,12 +197,12 @@ class Mod:
|
||||
if not toggled:
|
||||
await self.settings.guild(guild).respect_hierarchy.set(True)
|
||||
await ctx.send(
|
||||
_("Role hierarchy will be checked when " "moderation commands are issued.")
|
||||
_("Role hierarchy will be checked when moderation commands are issued.")
|
||||
)
|
||||
else:
|
||||
await self.settings.guild(guild).respect_hierarchy.set(False)
|
||||
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()
|
||||
@@ -241,7 +239,7 @@ class Mod:
|
||||
cur_setting = await self.settings.guild(guild).delete_repeats()
|
||||
if not cur_setting:
|
||||
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:
|
||||
await self.settings.guild(guild).delete_repeats.set(False)
|
||||
await ctx.send(_("Repeated messages will be ignored."))
|
||||
@@ -304,7 +302,7 @@ class Mod:
|
||||
|
||||
if author == user:
|
||||
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
|
||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||
@@ -316,6 +314,9 @@ class Mod:
|
||||
)
|
||||
)
|
||||
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)
|
||||
try:
|
||||
await guild.kick(user, reason=audit_reason)
|
||||
@@ -357,7 +358,7 @@ class Mod:
|
||||
|
||||
if author == user:
|
||||
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
|
||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||
@@ -369,6 +370,9 @@ class Mod:
|
||||
)
|
||||
)
|
||||
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.isdigit():
|
||||
@@ -451,15 +455,15 @@ class Mod:
|
||||
self.ban_queue.append(queue_entry)
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
self.ban_queue.remove(queue_entry)
|
||||
await ctx.send(_("I lack the permissions to do this."))
|
||||
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)
|
||||
try:
|
||||
@@ -547,7 +551,7 @@ class Mod:
|
||||
|
||||
if author == user:
|
||||
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
|
||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||
@@ -624,7 +628,6 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@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):
|
||||
"""Unbans the target user.
|
||||
|
||||
@@ -632,13 +635,17 @@ class Mod:
|
||||
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-
|
||||
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
|
||||
author = ctx.author
|
||||
user = await self.bot.get_user_info(user_id)
|
||||
if not user:
|
||||
await ctx.send(_("Couldn't find a user with that ID!"))
|
||||
return
|
||||
reason = get_audit_reason(ctx.author, reason)
|
||||
audit_reason = get_audit_reason(ctx.author, reason)
|
||||
bans = await guild.bans()
|
||||
bans = [be.user for be in bans]
|
||||
if user not in bans:
|
||||
@@ -647,7 +654,7 @@ class Mod:
|
||||
queue_entry = (guild.id, user.id)
|
||||
self.unban_queue.append(queue_entry)
|
||||
try:
|
||||
await guild.unban(user, reason=reason)
|
||||
await guild.unban(user, reason=audit_reason)
|
||||
except discord.HTTPException:
|
||||
self.unban_queue.remove(queue_entry)
|
||||
await ctx.send(_("Something went wrong while attempting to unban that user"))
|
||||
@@ -753,7 +760,7 @@ class Mod:
|
||||
else:
|
||||
await ctx.send(_("That user is already muted and deafened server-wide!"))
|
||||
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:
|
||||
await modlog.create_case(
|
||||
@@ -825,7 +832,7 @@ class Mod:
|
||||
await ctx.send("Done.")
|
||||
except discord.Forbidden:
|
||||
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()
|
||||
@@ -833,8 +840,7 @@ class Mod:
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
async def mute(self, ctx: commands.Context):
|
||||
"""Mutes user in the channel/server"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@mute.command(name="voice")
|
||||
@commands.guild_only()
|
||||
@@ -1002,8 +1008,7 @@ class Mod:
|
||||
"""Unmutes user in the channel/server
|
||||
|
||||
Defaults to channel"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@unmute.command(name="voice")
|
||||
@commands.guild_only()
|
||||
@@ -1168,7 +1173,6 @@ class Mod:
|
||||
async def ignore(self, ctx: commands.Context):
|
||||
"""Adds servers/channels to ignorelist"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
await ctx.send(await self.count_ignored())
|
||||
|
||||
@ignore.command(name="channel")
|
||||
@@ -1185,7 +1189,7 @@ class Mod:
|
||||
await ctx.send(_("Channel already in ignore list."))
|
||||
|
||||
@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):
|
||||
"""Ignores current server"""
|
||||
guild = ctx.guild
|
||||
@@ -1201,7 +1205,6 @@ class Mod:
|
||||
async def unignore(self, ctx: commands.Context):
|
||||
"""Removes servers/channels from ignorelist"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
await ctx.send(await self.count_ignored())
|
||||
|
||||
@unignore.command(name="channel")
|
||||
@@ -1219,7 +1222,7 @@ class Mod:
|
||||
await ctx.send(_("That channel is not in the ignore list."))
|
||||
|
||||
@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):
|
||||
"""Removes current guild from ignore list"""
|
||||
guild = ctx.message.guild
|
||||
@@ -1319,7 +1322,7 @@ class Mod:
|
||||
value="{0.name} (ID {0.id})".format(voice_state.channel),
|
||||
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 = " ~ ".join((name, user.nick)) if user.nick else name
|
||||
@@ -1335,7 +1338,7 @@ class Mod:
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
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()
|
||||
async def names(self, ctx: commands.Context, user: discord.Member):
|
||||
@@ -1355,15 +1358,15 @@ class Mod:
|
||||
if msg:
|
||||
await ctx.send(msg)
|
||||
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):
|
||||
names = await self.settings.user(user).past_names()
|
||||
nicks = await self.settings.member(user).past_nicks()
|
||||
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:
|
||||
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
|
||||
|
||||
async def check_tempban_expirations(self):
|
||||
@@ -1418,7 +1421,7 @@ class Mod:
|
||||
await guild.ban(author, reason="Mention spam (Autoban)")
|
||||
except discord.HTTPException:
|
||||
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:
|
||||
try:
|
||||
@@ -1439,7 +1442,13 @@ class Mod:
|
||||
return True
|
||||
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:
|
||||
* delete delay"""
|
||||
guild = ctx.guild
|
||||
@@ -1595,18 +1604,22 @@ class Mod:
|
||||
if entry.target == target:
|
||||
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:
|
||||
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
|
||||
name_list.remove(after.nick)
|
||||
name_list.append(after.nick)
|
||||
name_list.remove(after.name)
|
||||
name_list.append(after.name)
|
||||
while len(name_list) > 20:
|
||||
name_list.pop(0)
|
||||
|
||||
if before.nick != after.nick and after.nick is not None:
|
||||
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:
|
||||
nick_list.remove(after.nick)
|
||||
nick_list.append(after.nick)
|
||||
|
||||
@@ -19,8 +19,7 @@ class ModLog:
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modlogset(self, ctx: commands.Context):
|
||||
"""Settings for the mod log"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
@@ -35,9 +34,7 @@ class ModLog:
|
||||
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
||||
else:
|
||||
await ctx.send(
|
||||
_("I do not have permissions to " "send messages in {}!").format(
|
||||
channel.mention
|
||||
)
|
||||
_("I do not have permissions to send messages in {}!").format(channel.mention)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Tuple
|
||||
|
||||
|
||||
class CogOrCommand(commands.Converter):
|
||||
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||
ret = ctx.bot.get_cog(arg)
|
||||
if ret:
|
||||
@@ -12,15 +11,34 @@ class CogOrCommand(commands.Converter):
|
||||
if ret:
|
||||
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):
|
||||
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
|
||||
raise commands.BadArgument()
|
||||
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.config import Config
|
||||
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 .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__)
|
||||
|
||||
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(_)
|
||||
@@ -32,61 +35,34 @@ class Permissions:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||
self._before = []
|
||||
self._after = []
|
||||
self.config.register_global(owner_models={})
|
||||
self.config.register_guild(owner_models={})
|
||||
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:
|
||||
ctx: commands.Context
|
||||
level: str
|
||||
allowed: list of commands
|
||||
denied: list of commands
|
||||
default: list of commands
|
||||
|
||||
and returning:
|
||||
None: do not interfere
|
||||
True: command should be allowed even if they dont
|
||||
have role or perm requirements for the check
|
||||
False: command should be blocked
|
||||
representing how permissions interacts with the
|
||||
user, channel, guild, and (possibly) voice channel
|
||||
for all commands on the bot (not just the one in the context object)
|
||||
|
||||
before_or_after:
|
||||
Should literally be a str equaling 'before' or 'after'
|
||||
This should be based on if this should take priority
|
||||
over set rules or not
|
||||
This mainly exists for use by the help formatter,
|
||||
but others may find it useful
|
||||
|
||||
3rd party cogs adding checks using this should only allow
|
||||
the owner to add checks before, and ensure only the owner
|
||||
can add checks recieving the level 'owner'
|
||||
Unlike the rest of the permission system, if other models are added later,
|
||||
due to optimizations made for this, this needs to be adjusted accordingly
|
||||
|
||||
3rd party cogs should keep a copy of of any checks they registered
|
||||
and deregister then on unload
|
||||
This does not account for before and after permission hooks,
|
||||
these need to be checked seperately
|
||||
"""
|
||||
return await mass_resolve(ctx=ctx, config=self.config)
|
||||
|
||||
if before_or_after == "before":
|
||||
self._before.append(check_obj)
|
||||
elif before_or_after == "after":
|
||||
self._after.append(check_obj)
|
||||
else:
|
||||
raise TypeError("RTFM")
|
||||
|
||||
def remove_check(self, check_obj: object, before_or_after: str):
|
||||
"""
|
||||
removes a previously registered check object
|
||||
|
||||
3rd party cogs should keep a copy of of any checks they registered
|
||||
and deregister then on unload
|
||||
"""
|
||||
|
||||
if before_or_after == "before":
|
||||
self._before.remove(check_obj)
|
||||
elif before_or_after == "after":
|
||||
self._after.remove(check_obj)
|
||||
else:
|
||||
raise TypeError("RTFM")
|
||||
|
||||
async def __global_check(self, ctx):
|
||||
async def __global_check(self, ctx: commands.Context) -> bool:
|
||||
"""
|
||||
Yes, this is needed on top of hooking into checks.py
|
||||
to ensure that unchecked commands can still be managed by permissions
|
||||
@@ -94,7 +70,7 @@ class Permissions:
|
||||
defering to check logic
|
||||
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:
|
||||
return False
|
||||
@@ -109,7 +85,7 @@ class Permissions:
|
||||
ctx: `redbot.core.context.commands.Context`
|
||||
The context of the command
|
||||
level: `str`
|
||||
One of 'owner', 'guildowner', 'admin', 'mod'
|
||||
One of 'owner', 'guildowner', 'admin', 'mod', 'all'
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -119,25 +95,44 @@ class Permissions:
|
||||
"""
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return True
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
entries = [x for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
|
||||
for check in self._before:
|
||||
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)
|
||||
if override is not None:
|
||||
return override
|
||||
|
||||
for model in self.resolution_order[level]:
|
||||
override_model = getattr(self, model + "_model", None)
|
||||
override = await override_model(ctx) if override_model else None
|
||||
# checked ids + configureable to be checked against
|
||||
cache_tup = entries_from_ctx(ctx) + (
|
||||
ctx.cog.__class__.__name__,
|
||||
ctx.command.qualified_name,
|
||||
)
|
||||
if cache_tup in self.cache:
|
||||
override = self.cache[cache_tup]
|
||||
if override is not None:
|
||||
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)
|
||||
if override is not None:
|
||||
return override
|
||||
@@ -156,7 +151,8 @@ class Permissions:
|
||||
"""
|
||||
Handles guild level overrides
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
return None
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
return resolve_models(ctx=ctx, models=models)
|
||||
|
||||
@@ -171,8 +167,7 @@ class Permissions:
|
||||
"""
|
||||
Permission management tools
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@permissions.command()
|
||||
async def explain(self, ctx: commands.Context):
|
||||
@@ -206,10 +201,10 @@ class Permissions:
|
||||
"\n"
|
||||
"1. Rules about a user.\n"
|
||||
"2. Rules about the voice channel a user is in.\n"
|
||||
"3. Rules about the text channel a command was issued in\n"
|
||||
"3. Rules about the text channel a command was issued in.\n"
|
||||
"4. Rules about a role the user has "
|
||||
"(The highest role they have with a rule will be used)\n"
|
||||
"5. Rules about the guild a user is in (Owner level only)"
|
||||
"(The highest role they have with a rule will be used).\n"
|
||||
"5. Rules about the guild a user is in (Owner level only)."
|
||||
"\n\nFor more details, please read the official documentation."
|
||||
)
|
||||
|
||||
@@ -236,7 +231,9 @@ class Permissions:
|
||||
else:
|
||||
try:
|
||||
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:
|
||||
can = False
|
||||
|
||||
@@ -254,15 +251,16 @@ class Permissions:
|
||||
Take a YAML file upload to set permissions from
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file"))
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Inalid syntax"))
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="getglobalacl")
|
||||
@@ -280,15 +278,16 @@ class Permissions:
|
||||
Take a YAML file upload to set permissions from
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file"))
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Inalid syntax"))
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -309,15 +308,16 @@ class Permissions:
|
||||
Use this to not lose existing rules
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file"))
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Inalid syntax"))
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="updateglobalacl")
|
||||
@@ -328,15 +328,16 @@ class Permissions:
|
||||
Use this to not lose existing rules
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file"))
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Inalid syntax"))
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="addglobalrule")
|
||||
@@ -348,7 +349,7 @@ class Permissions:
|
||||
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
|
||||
|
||||
@@ -363,7 +364,7 @@ class Permissions:
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
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
|
||||
async with self.config.owner_models() as models:
|
||||
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)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -392,7 +394,7 @@ class Permissions:
|
||||
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
|
||||
|
||||
@@ -407,7 +409,7 @@ class Permissions:
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
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
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
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)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="removeglobalrule")
|
||||
@@ -450,7 +453,7 @@ class Permissions:
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
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
|
||||
async with self.config.owner_models() as models:
|
||||
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)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@@ -494,7 +498,7 @@ class Permissions:
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
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
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
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)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="setdefaultguildrule")
|
||||
async def set_default_guild_rule(
|
||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
||||
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
|
||||
|
||||
Use with a cog or command and no setting to clear the default and defer to
|
||||
normal check logic
|
||||
"""
|
||||
if allow_or_deny:
|
||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
||||
else:
|
||||
val_to_set = None
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
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
|
||||
|
||||
models.update(data)
|
||||
await ctx.send(_("Defualt set."))
|
||||
await ctx.send(_("Default set."))
|
||||
self.invalidate_cache(type_name)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="setdefaultglobalrule")
|
||||
async def set_default_global_rule(
|
||||
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None
|
||||
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
|
||||
|
||||
Use with a cog or command and no setting to clear the default and defer to
|
||||
normal check logic
|
||||
"""
|
||||
|
||||
if allow_or_deny:
|
||||
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
|
||||
else:
|
||||
val_to_set = None
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.owner_models() as models:
|
||||
@@ -570,33 +563,18 @@ class Permissions:
|
||||
data[model_type][type_name]["default"] = val_to_set
|
||||
|
||||
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()
|
||||
@permissions.command(name="clearglobalsettings")
|
||||
async def clear_globals(self, ctx: commands.Context):
|
||||
"""
|
||||
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()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="clearguildsettings")
|
||||
@@ -604,23 +582,61 @@ class Permissions:
|
||||
"""
|
||||
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?")
|
||||
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."))
|
||||
async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
|
||||
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
|
||||
m = await ctx.send(_("Are you sure?"))
|
||||
for r in REACTS.keys():
|
||||
await m.add_reaction(r)
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=lambda r, u: u == ctx.author and str(r) in REACTS,
|
||||
timeout=30,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||
|
||||
if REACTS.get(str(reaction)):
|
||||
await self.config.guild(ctx.guild).owner_models.clear()
|
||||
await ctx.send(_("Guild settings cleared"))
|
||||
agreed = REACTS.get(str(reaction))
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Returns the value from a check if it is valid
|
||||
"""
|
||||
|
||||
# Non staticmethods should not be run without their parent
|
||||
# class, even if the parent class did not deregister them
|
||||
if check.__module__ is None:
|
||||
pass
|
||||
elif isinstance(check, types.FunctionType):
|
||||
if (
|
||||
next(filter(lambda x: check.__module__ == x.__module__, ctx.bot.cogs.values()), None)
|
||||
is None
|
||||
):
|
||||
return None
|
||||
|
||||
val = None
|
||||
# let's not spam the console with improperly made 3rd party checks
|
||||
try:
|
||||
if asyncio.iscoroutine(check) or asyncio.iscoroutinefunction(check):
|
||||
if asyncio.iscoroutinefunction(check):
|
||||
val = await check(ctx, level=level)
|
||||
else:
|
||||
val = check(ctx, level=level)
|
||||
@@ -67,23 +73,7 @@ def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) ->
|
||||
"""
|
||||
resolves specific lists
|
||||
"""
|
||||
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
|
||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
# entries now contains the following (in order) (if applicable)
|
||||
# author.id
|
||||
# author.voice.voice_channel.id
|
||||
# channel.id
|
||||
# role.id for each role (highest to lowest)
|
||||
# (implicitly) guild.id because
|
||||
# the @everyone role shares an id with the guild
|
||||
|
||||
for entry in entries:
|
||||
for entry in entries_from_ctx(ctx):
|
||||
if entry in whitelist:
|
||||
return True
|
||||
if entry in blacklist:
|
||||
|
||||
@@ -59,29 +59,29 @@ class Reports:
|
||||
@commands.group(name="reportset")
|
||||
async def reportset(self, ctx: commands.Context):
|
||||
"""
|
||||
settings for reports
|
||||
Settings for the report system.
|
||||
"""
|
||||
pass
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="output")
|
||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""sets the output channel"""
|
||||
"""Set the channel where reports will show up"""
|
||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||
await ctx.send(_("Report Channel Set."))
|
||||
await ctx.send(_("The report channel has been set."))
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="toggleactive")
|
||||
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||
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 = not active
|
||||
await self.config.guild(ctx.guild).active.set(active)
|
||||
if active:
|
||||
await ctx.send(_("Reporting now enabled"))
|
||||
await ctx.send(_("Reporting is now enabled"))
|
||||
else:
|
||||
await ctx.send(_("Reporting disabled."))
|
||||
await ctx.send(_("Reporting is now disabled."))
|
||||
|
||||
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||
ret = False
|
||||
@@ -105,7 +105,7 @@ class Reports:
|
||||
*,
|
||||
mod: bool = False,
|
||||
permissions: Union[discord.Permissions, dict] = None,
|
||||
prompt: str = ""
|
||||
prompt: str = "",
|
||||
):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
@@ -175,7 +175,10 @@ class Reports:
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url
|
||||
name=_("Report from {author}{maybe_nick}").format(
|
||||
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
|
||||
),
|
||||
icon_url=author.avatar_url,
|
||||
)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
send_content = None
|
||||
@@ -201,10 +204,10 @@ class Reports:
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||
"""
|
||||
Follow the prompts to make a report
|
||||
Send a report.
|
||||
|
||||
optionally use with a report message
|
||||
to use it non interactively
|
||||
Use without arguments for interactive reporting, or do
|
||||
[p]report <text> to use it non-interactively.
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -212,11 +215,6 @@ class Reports:
|
||||
guild = await self.discover_guild(
|
||||
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:
|
||||
return
|
||||
g_active = await self.config.guild(guild).active()
|
||||
@@ -229,22 +227,18 @@ class Reports:
|
||||
if self.antispam[guild.id][author.id].spammy:
|
||||
return await author.send(
|
||||
_(
|
||||
"You've sent a few too many of these recently. "
|
||||
"Contact a server admin to resolve this, or try again "
|
||||
"later."
|
||||
"You've sent too many reports recently. "
|
||||
"Please contact a server admin if this is important matter, "
|
||||
"or please wait and try again later."
|
||||
)
|
||||
)
|
||||
|
||||
if author.id in self.user_cache:
|
||||
return await author.send(
|
||||
_("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)
|
||||
|
||||
if _report:
|
||||
@@ -261,9 +255,7 @@ class Reports:
|
||||
)
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("This requires DMs enabled."))
|
||||
self.user_cache.remove(author.id)
|
||||
return
|
||||
return await ctx.send(_("This requires DMs enabled."))
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
@@ -271,18 +263,32 @@ class Reports:
|
||||
try:
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
||||
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:
|
||||
val = await self.send_report(message, guild)
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
if val is None:
|
||||
await author.send(_("There was an error sending your report."))
|
||||
await author.send(
|
||||
_("There was an error sending your report, please contact a server admin.")
|
||||
)
|
||||
else:
|
||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
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):
|
||||
"""
|
||||
@@ -315,10 +321,12 @@ class Reports:
|
||||
@report.command(name="interact")
|
||||
async def response(self, ctx, ticket_number: int):
|
||||
"""
|
||||
opens a message tunnel between things you say in this channel
|
||||
and the ticket opener's direct messages
|
||||
Open a message tunnel.
|
||||
|
||||
tunnels do not persist across bot restarts
|
||||
This tunnel will forward things you say in this channel
|
||||
to the ticket opener's direct messages.
|
||||
|
||||
Tunnels do not persist across bot restarts.
|
||||
"""
|
||||
|
||||
# note, mod_or_permissions is an implicit guild_only
|
||||
@@ -344,14 +352,15 @@ class Reports:
|
||||
)
|
||||
|
||||
big_topic = _(
|
||||
"{who} opened a 2-way communication."
|
||||
"{who} opened a 2-way communication "
|
||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||
"(8MB file size limitation on uploads) "
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded will be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
"You can close a communication at any point by reacting with "
|
||||
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||
"Any message succesfully forwarded will be marked with "
|
||||
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||
"Tunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||
@@ -359,8 +368,7 @@ class Reports:
|
||||
try:
|
||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("User has disabled DMs."))
|
||||
tun.close()
|
||||
await ctx.send(_("That user has DMs disabled."))
|
||||
else:
|
||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from .streams import Streams
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Streams(bot))
|
||||
async def setup(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.i18n import Translator, cog_i18n
|
||||
from .streamtypes import (
|
||||
Stream,
|
||||
TwitchStream,
|
||||
HitboxStream,
|
||||
MixerStream,
|
||||
@@ -25,6 +26,7 @@ from . import streamtypes as StreamClasses
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
CHECK_DELAY = 60
|
||||
|
||||
@@ -50,9 +52,11 @@ class Streams:
|
||||
|
||||
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]$")
|
||||
|
||||
@@ -62,7 +66,8 @@ class Streams:
|
||||
return True
|
||||
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.communities = await self.load_communities()
|
||||
|
||||
@@ -70,16 +75,14 @@ class Streams:
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
stream = TwitchStream(name=channel_name, token=token)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
||||
"""
|
||||
Checks if a Youtube channel is streaming
|
||||
"""
|
||||
"""Checks if a Youtube channel is live"""
|
||||
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
||||
is_name = self.check_name_or_id(channel_id_or_name)
|
||||
if is_name:
|
||||
@@ -90,19 +93,19 @@ class Streams:
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
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)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@@ -110,24 +113,24 @@ class Streams:
|
||||
try:
|
||||
embed = await stream.is_online()
|
||||
except OfflineStream:
|
||||
await ctx.send(_("The stream is offline."))
|
||||
await ctx.send(_("That user is offline."))
|
||||
except StreamNotFound:
|
||||
await ctx.send(_("The channel doesn't seem to exist."))
|
||||
await ctx.send(_("That channel doesn't seem to exist."))
|
||||
except InvalidTwitchCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
except InvalidYoutubeCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
except APIError:
|
||||
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:
|
||||
await ctx.send(embed=embed)
|
||||
@@ -136,50 +139,46 @@ class Streams:
|
||||
@commands.guild_only()
|
||||
@checks.mod()
|
||||
async def streamalert(self, ctx: commands.Context):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@streamalert.group(name="twitch")
|
||||
async def _twitch(self, ctx: commands.Context):
|
||||
"""Twitch stream alerts"""
|
||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@_twitch.command(name="channel")
|
||||
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())
|
||||
|
||||
@_twitch.command(name="community")
|
||||
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
||||
"""Sets a Twitch stream alert notification in the channel
|
||||
for the specified community."""
|
||||
"""Sets an alert notification in the channel for the specified twitch community."""
|
||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||
|
||||
@streamalert.command(name="youtube")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="hitbox")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="mixer")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="picarto")
|
||||
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)
|
||||
|
||||
@streamalert.command(name="stop")
|
||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||
"""Stops all stream notifications in the channel
|
||||
|
||||
Adding 'yes' will disable all notifications in the server"""
|
||||
streams = self.streams.copy()
|
||||
local_channel_ids = [c.id for c in ctx.guild.channels]
|
||||
@@ -202,7 +201,7 @@ class Streams:
|
||||
self.streams = 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"
|
||||
)
|
||||
|
||||
@@ -212,7 +211,7 @@ class Streams:
|
||||
async def streamalert_list(self, ctx: commands.Context):
|
||||
streams_list = defaultdict(list)
|
||||
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 channel_id in stream.channels:
|
||||
@@ -220,7 +219,7 @@ class Streams:
|
||||
streams_list[channel_id].append(stream.name.lower())
|
||||
|
||||
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
|
||||
|
||||
for channel_id, streams in streams_list.items():
|
||||
@@ -243,7 +242,7 @@ class Streams:
|
||||
exists = await self.check_exists(stream)
|
||||
except InvalidTwitchCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
@@ -251,13 +250,13 @@ class Streams:
|
||||
except InvalidYoutubeCredentials:
|
||||
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))
|
||||
)
|
||||
return
|
||||
except APIError:
|
||||
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
|
||||
else:
|
||||
@@ -276,7 +275,7 @@ class Streams:
|
||||
await community.get_community_streams()
|
||||
except InvalidTwitchCredentials:
|
||||
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)
|
||||
)
|
||||
)
|
||||
@@ -286,7 +285,7 @@ class Streams:
|
||||
return
|
||||
except APIError:
|
||||
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
|
||||
except OfflineCommunity:
|
||||
@@ -297,14 +296,12 @@ class Streams:
|
||||
@commands.group()
|
||||
@checks.mod()
|
||||
async def streamset(self, ctx: commands.Context):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||
"""Set the Client ID for twitch.
|
||||
|
||||
To do this, follow these steps:
|
||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
||||
2. Click *Register Your Application*
|
||||
@@ -312,7 +309,6 @@ class Streams:
|
||||
select an Application Category of your choosing.
|
||||
4. Click *Register*, and on the following page, copy the Client ID.
|
||||
5. Paste the Client ID into this command. Done!
|
||||
|
||||
"""
|
||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||
@@ -322,14 +318,11 @@ class Streams:
|
||||
@checks.is_owner()
|
||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||
"""Sets the API key for Youtube.
|
||||
|
||||
To get one, do the following:
|
||||
|
||||
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)
|
||||
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!
|
||||
|
||||
"""
|
||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||
await ctx.send(_("Youtube key set."))
|
||||
@@ -337,9 +330,8 @@ class Streams:
|
||||
@streamset.group()
|
||||
@commands.guild_only()
|
||||
async def mention(self, ctx: commands.Context):
|
||||
"""Sets mentions for stream alerts."""
|
||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
|
||||
await ctx.send_help()
|
||||
"""Sets mentions for alerts."""
|
||||
pass
|
||||
|
||||
@mention.command(aliases=["everyone"])
|
||||
@commands.guild_only()
|
||||
@@ -350,17 +342,16 @@ class Streams:
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_everyone.set(False)
|
||||
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"
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.db.guild(guild).mention_everyone.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned"
|
||||
).format("@\u200beveryone")
|
||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||
"@\u200beveryone"
|
||||
)
|
||||
)
|
||||
|
||||
@mention.command(aliases=["here"])
|
||||
@@ -371,16 +362,13 @@ class Streams:
|
||||
current_setting = await self.db.guild(guild).mention_here()
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_here.set(False)
|
||||
await ctx.send(
|
||||
_("{} will no longer be mentioned " "for a stream alert.").format("@\u200bhere")
|
||||
)
|
||||
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
|
||||
else:
|
||||
await self.db.guild(guild).mention_here.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned"
|
||||
).format("@\u200bhere")
|
||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
||||
"@\u200bhere"
|
||||
)
|
||||
)
|
||||
|
||||
@mention.command()
|
||||
@@ -394,18 +382,16 @@ class Streams:
|
||||
if current_setting:
|
||||
await self.db.role(role).mention.set(False)
|
||||
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)
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.db.role(role).mention.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned"
|
||||
""
|
||||
).format("@\u200b{}".format(role.name))
|
||||
_("When a stream or community " "is live, {} will be mentioned." "").format(
|
||||
"@\u200b{}".format(role.name)
|
||||
)
|
||||
)
|
||||
|
||||
@streamset.command()
|
||||
@@ -414,7 +400,7 @@ class Streams:
|
||||
"""Toggles automatic deletion of notifications for streams that go offline"""
|
||||
await self.db.guild(ctx.guild).autodelete.set(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:
|
||||
await ctx.send("Notifications will never be deleted.")
|
||||
|
||||
@@ -424,7 +410,7 @@ class Streams:
|
||||
if stream not in self.streams:
|
||||
self.streams.append(stream)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -433,7 +419,7 @@ class Streams:
|
||||
if not stream.channels:
|
||||
self.streams.remove(stream)
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -448,7 +434,7 @@ class Streams:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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)
|
||||
)
|
||||
@@ -459,7 +445,7 @@ class Streams:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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)
|
||||
)
|
||||
@@ -534,9 +520,9 @@ class Streams:
|
||||
mention_str = await self._get_mention_str(channel.guild)
|
||||
|
||||
if mention_str:
|
||||
content = "{}, {} is online!".format(mention_str, stream.name)
|
||||
content = "{}, {} is live!".format(mention_str, stream.name)
|
||||
else:
|
||||
content = "{} is online!".format(stream.name)
|
||||
content = "{} is live!".format(stream.name)
|
||||
|
||||
try:
|
||||
m = await channel.send(content, embed=embed)
|
||||
@@ -562,7 +548,7 @@ class Streams:
|
||||
try:
|
||||
stream_list = await community.get_community_streams()
|
||||
except CommunityNotFound:
|
||||
print(_("Community {} not found!").format(community.name))
|
||||
print(_("The Community {} was not found!").format(community.name))
|
||||
continue
|
||||
except OfflineCommunity:
|
||||
for message in community._messages_cache:
|
||||
@@ -671,4 +657,5 @@ class Streams:
|
||||
await self.db.communities.set(raw_communities)
|
||||
|
||||
def __unload(self):
|
||||
self.task.cancel()
|
||||
if self.task:
|
||||
self.task.cancel()
|
||||
|
||||
@@ -30,7 +30,6 @@ def rnd(url):
|
||||
|
||||
|
||||
class TwitchCommunity:
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.name = kwargs.pop("name")
|
||||
self.id = kwargs.pop("id", None)
|
||||
@@ -119,7 +118,6 @@ class TwitchCommunity:
|
||||
|
||||
|
||||
class Stream:
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.name = kwargs.pop("name", None)
|
||||
self.channels = kwargs.pop("channels", [])
|
||||
@@ -148,7 +146,6 @@ class Stream:
|
||||
|
||||
|
||||
class YoutubeStream(Stream):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop("id", 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"])
|
||||
title = vid_data["snippet"]["title"]
|
||||
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.set_author(name=channel_title)
|
||||
embed.set_image(url=rnd(thumbnail))
|
||||
@@ -213,7 +210,6 @@ class YoutubeStream(Stream):
|
||||
|
||||
|
||||
class TwitchStream(Stream):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop("id", None)
|
||||
self._token = kwargs.pop("token", None)
|
||||
@@ -266,7 +262,7 @@ class TwitchStream(Stream):
|
||||
url = channel["url"]
|
||||
logo = channel["logo"]
|
||||
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"]
|
||||
if not status:
|
||||
status = "Untitled broadcast"
|
||||
@@ -288,7 +284,6 @@ class TwitchStream(Stream):
|
||||
|
||||
|
||||
class HitboxStream(Stream):
|
||||
|
||||
async def is_online(self):
|
||||
url = "https://api.hitbox.tv/media/live/" + self.name
|
||||
|
||||
@@ -326,7 +321,6 @@ class HitboxStream(Stream):
|
||||
|
||||
|
||||
class MixerStream(Stream):
|
||||
|
||||
async def is_online(self):
|
||||
url = "https://mixer.com/api/v1/channels/" + self.name
|
||||
|
||||
@@ -348,7 +342,7 @@ class MixerStream(Stream):
|
||||
raise APIError()
|
||||
|
||||
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"]
|
||||
url = "https://mixer.com/" + data["token"]
|
||||
embed = discord.Embed(title=data["name"], url=url)
|
||||
@@ -368,7 +362,6 @@ class MixerStream(Stream):
|
||||
|
||||
|
||||
class PicartoStream(Stream):
|
||||
|
||||
async def is_online(self):
|
||||
url = "https://api.picarto.tv/v1/channel/name/" + self.name
|
||||
|
||||
@@ -390,7 +383,7 @@ class PicartoStream(Stream):
|
||||
|
||||
def make_embed(self, data):
|
||||
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"]
|
||||
thumbnail = data["thumbnails"]["web"]
|
||||
@@ -412,5 +405,5 @@ class PicartoStream(Stream):
|
||||
data["adult"] = ""
|
||||
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from collections import Counter
|
||||
import yaml
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
from redbot.ext import trivia as ext_trivia
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
@@ -18,6 +18,7 @@ UNIQUE_ID = 0xb3c0e453
|
||||
|
||||
class InvalidListError(Exception):
|
||||
"""A Trivia list file is in invalid format."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -46,7 +47,6 @@ class Trivia:
|
||||
async def triviaset(self, ctx: commands.Context):
|
||||
"""Manage trivia settings."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
settings = self.conf.guild(ctx.guild)
|
||||
settings_dict = await settings.all()
|
||||
msg = box(
|
||||
@@ -81,7 +81,7 @@ class Trivia:
|
||||
return
|
||||
settings = self.conf.guild(ctx.guild)
|
||||
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")
|
||||
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
||||
@@ -160,7 +160,7 @@ class Trivia:
|
||||
return
|
||||
await settings.payout_multiplier.set(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
|
||||
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
||||
|
||||
@@ -206,7 +206,7 @@ class Trivia:
|
||||
return
|
||||
if not trivia_dict:
|
||||
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
|
||||
settings = await self.conf.guild(ctx.guild).all()
|
||||
@@ -245,13 +245,13 @@ class Trivia:
|
||||
"""List available trivia categories."""
|
||||
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:
|
||||
await ctx.author.send(msg)
|
||||
return
|
||||
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):
|
||||
"""Leaderboard for trivia.
|
||||
|
||||
@@ -382,7 +382,7 @@ class Trivia:
|
||||
try:
|
||||
priority.remove(key)
|
||||
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
|
||||
priority.append(key)
|
||||
items = data.items()
|
||||
@@ -480,13 +480,13 @@ class Trivia:
|
||||
try:
|
||||
path = next(p for p in self._all_lists() if p.stem == category)
|
||||
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:
|
||||
try:
|
||||
dict_ = yaml.load(file)
|
||||
except yaml.error.YAMLError as exc:
|
||||
raise InvalidListError("YAML parsing failed") from exc
|
||||
raise InvalidListError("YAML parsing failed.") from exc
|
||||
else:
|
||||
return dict_
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ async def warning_points_add_check(
|
||||
act = a
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ async def warning_points_remove_check(
|
||||
act = a
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@@ -69,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
|
||||
check_str = inspect.getsource(checks.is_owner)
|
||||
if any(inspect.getsource(x) in check_str for x in com.checks):
|
||||
# command the user specified has the is_owner check
|
||||
return None, _(
|
||||
"That command requires bot owner. I can't " "allow you to use that for an action"
|
||||
return (
|
||||
None,
|
||||
_("That command requires bot owner. I can't allow you to use that for an action"),
|
||||
)
|
||||
return "{prefix}" + orig, None
|
||||
|
||||
@@ -80,10 +81,11 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
||||
the points threshold for the action"""
|
||||
await ctx.send(
|
||||
_(
|
||||
"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 "
|
||||
"Enter the command to be run when the user **exceeds the points for "
|
||||
"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 "
|
||||
"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. "
|
||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||
"Please wait 15 seconds before entering your response."
|
||||
@@ -99,8 +101,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then."))
|
||||
return None
|
||||
else:
|
||||
if msg.content == "none":
|
||||
return None
|
||||
|
||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||
if command is None:
|
||||
@@ -120,12 +124,13 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
||||
"""
|
||||
await ctx.send(
|
||||
_(
|
||||
"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 "
|
||||
"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 "
|
||||
"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 "
|
||||
"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. "
|
||||
"Commands requiring bot owner are not allowed for security reasons.\n\n"
|
||||
"Please wait 15 seconds before entering your response."
|
||||
@@ -141,9 +146,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then."))
|
||||
return None
|
||||
|
||||
else:
|
||||
if msg.content == "none":
|
||||
return None
|
||||
command, m = get_command_from_input(ctx.bot, msg.content)
|
||||
if command is None:
|
||||
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.utils.mod import is_admin_or_superior
|
||||
from redbot.core.utils.chat_formatting import warning, pagify
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
_ = Translator("Warnings", __file__)
|
||||
|
||||
@@ -46,17 +47,16 @@ class Warnings:
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warningset(self, ctx: commands.Context):
|
||||
"""Warning settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@warningset.command()
|
||||
@commands.guild_only()
|
||||
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
|
||||
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
||||
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()
|
||||
@@ -64,8 +64,7 @@ class Warnings:
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnaction(self, ctx: commands.Context):
|
||||
"""Action management"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@warnaction.command(name="add")
|
||||
@commands.guild_only()
|
||||
@@ -76,27 +75,9 @@ class Warnings:
|
||||
"""
|
||||
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 = {
|
||||
"action_name": name,
|
||||
"points": points,
|
||||
@@ -116,7 +97,7 @@ class Warnings:
|
||||
# Sort in descending order by point count for ease in
|
||||
# finding the highest possible action to take
|
||||
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")
|
||||
@commands.guild_only()
|
||||
@@ -141,8 +122,7 @@ class Warnings:
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnreason(self, ctx: commands.Context):
|
||||
"""Add reasons for warnings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@warnreason.command(name="add")
|
||||
@commands.guild_only()
|
||||
@@ -161,7 +141,7 @@ class Warnings:
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
registered_reasons.update(completed)
|
||||
|
||||
await ctx.send(_("That reason has been registered"))
|
||||
await ctx.send(_("That reason has been registered."))
|
||||
|
||||
@warnreason.command(name="del")
|
||||
@commands.guild_only()
|
||||
@@ -173,7 +153,7 @@ class Warnings:
|
||||
if registered_reasons.pop(reason_name.lower(), None):
|
||||
await ctx.tick()
|
||||
else:
|
||||
await ctx.send(_("That is not a registered reason name"))
|
||||
await ctx.send(_("That is not a registered reason name."))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@@ -185,13 +165,20 @@ class Warnings:
|
||||
msg_list = []
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
for r, v in registered_reasons.items():
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, v["points"], v["description"]
|
||||
if ctx.embed_requested():
|
||||
em = discord.Embed(
|
||||
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:
|
||||
await ctx.send_interactive(msg_list)
|
||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(_("There are no reasons configured!"))
|
||||
|
||||
@@ -205,14 +192,21 @@ class Warnings:
|
||||
msg_list = []
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for r in registered_actions:
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||
"Drop command: {}".format(
|
||||
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
||||
if await ctx.embed_requested():
|
||||
em = discord.Embed(title=_("Action: {name}").format(name=r["action_name"]))
|
||||
em.add_field(name=_("Points"), value="{}".format(r["points"]), inline=False)
|
||||
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:
|
||||
await ctx.send_interactive(msg_list)
|
||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
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
|
||||
"""
|
||||
if user == ctx.author:
|
||||
await ctx.send(_("You cannot warn yourself."))
|
||||
return
|
||||
if reason.lower() == "custom":
|
||||
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
|
||||
if not custom_allowed:
|
||||
await ctx.send(
|
||||
_(
|
||||
"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))
|
||||
)
|
||||
return
|
||||
@@ -259,7 +256,27 @@ class Warnings:
|
||||
await member_settings.total_points.set(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.guild_only()
|
||||
@@ -275,7 +292,7 @@ class Warnings:
|
||||
else:
|
||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||
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
|
||||
else:
|
||||
@@ -306,6 +323,9 @@ class Warnings:
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||
"""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
|
||||
member = guild.get_member(user_id)
|
||||
if member is None: # no longer in guild, but need a "member" object
|
||||
@@ -336,7 +356,7 @@ class Warnings:
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then"))
|
||||
await ctx.send(_("Ok then."))
|
||||
return
|
||||
try:
|
||||
int(msg.content)
|
||||
@@ -349,11 +369,11 @@ class Warnings:
|
||||
return
|
||||
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:
|
||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(_("Ok then"))
|
||||
await ctx.send(_("Ok then."))
|
||||
return
|
||||
to_add["description"] = msg.content
|
||||
return to_add
|
||||
|
||||
@@ -4,7 +4,6 @@ __all__ = ["Config", "__version__"]
|
||||
|
||||
|
||||
class VersionInfo:
|
||||
|
||||
def __init__(self, major, minor, micro, releaselevel, serial):
|
||||
self._levels = ["alpha", "beta", "final"]
|
||||
self.major = major
|
||||
@@ -37,5 +36,5 @@ class VersionInfo:
|
||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||
|
||||
|
||||
__version__ = "3.0.0b15"
|
||||
version_info = VersionInfo(3, 0, 0, "beta", 15)
|
||||
__version__ = "3.0.0b17"
|
||||
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:
|
||||
await _conf.guild(guild).bank_name.set(name)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Guild must be provided if setting the name of a guild" "-specific bank."
|
||||
)
|
||||
raise RuntimeError("Guild must be provided if setting the name of a guild-specific bank.")
|
||||
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)
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
@@ -18,12 +18,13 @@ from discord.voice_client import VoiceClient
|
||||
VoiceClient.warn_nacl = False
|
||||
|
||||
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 .sentry import SentryManager
|
||||
|
||||
|
||||
class RedBase(BotBase):
|
||||
class RedBase(BotBase, RPCMixin):
|
||||
"""Mixin for the main bot class.
|
||||
|
||||
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`.
|
||||
"""
|
||||
|
||||
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.db = Config.get_core_conf(force_registration=True)
|
||||
self._co_owners = cli_flags.co_owner
|
||||
@@ -50,6 +51,7 @@ class RedBase(BotBase):
|
||||
locale="en",
|
||||
embeds=True,
|
||||
color=15158332,
|
||||
fuzzy=False,
|
||||
help__page_char_limit=1000,
|
||||
help__max_pages_in_guild=2,
|
||||
help__tagline="",
|
||||
@@ -63,6 +65,7 @@ class RedBase(BotBase):
|
||||
mod_role=None,
|
||||
embeds=None,
|
||||
use_bot_color=False,
|
||||
fuzzy=False,
|
||||
)
|
||||
|
||||
self.db.register_user(embeds=None)
|
||||
@@ -99,16 +102,13 @@ class RedBase(BotBase):
|
||||
|
||||
self.counter = Counter()
|
||||
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.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||
|
||||
super().__init__(formatter=Help(), **kwargs)
|
||||
|
||||
if self.rpc_enabled:
|
||||
self.rpc = rpc.RPC(self)
|
||||
super().__init__(*args, formatter=Help(), **kwargs)
|
||||
|
||||
self.remove_command("help")
|
||||
|
||||
@@ -233,12 +233,24 @@ class RedBase(BotBase):
|
||||
lib_name = lib.__name__ # Thank you
|
||||
|
||||
# find all references to the module
|
||||
cog_names = []
|
||||
|
||||
# remove the cogs registered from the module
|
||||
for cogname, cog in self.cogs.copy().items():
|
||||
if cog.__module__.startswith(lib_name):
|
||||
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
|
||||
for cmd in self.all_commands.copy().values():
|
||||
if cmd.module.startswith(lib_name):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
async def check_overrides(ctx, *, level):
|
||||
@@ -16,7 +16,6 @@ async def check_overrides(ctx, *, level):
|
||||
|
||||
|
||||
def is_owner(**kwargs):
|
||||
|
||||
async def check(ctx):
|
||||
override = await check_overrides(ctx, level="owner")
|
||||
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):
|
||||
|
||||
async def predicate(ctx):
|
||||
override = await check_overrides(ctx, level="mod")
|
||||
return (
|
||||
@@ -86,7 +84,6 @@ def mod_or_permissions(**perms):
|
||||
|
||||
|
||||
def admin_or_permissions(**perms):
|
||||
|
||||
async def predicate(ctx):
|
||||
override = await check_overrides(ctx, level="admin")
|
||||
return (
|
||||
@@ -99,7 +96,6 @@ def admin_or_permissions(**perms):
|
||||
|
||||
|
||||
def bot_in_a_guild(**kwargs):
|
||||
|
||||
async def predicate(ctx):
|
||||
return len(ctx.bot.guilds) > 0
|
||||
|
||||
@@ -107,7 +103,6 @@ def bot_in_a_guild(**kwargs):
|
||||
|
||||
|
||||
def guildowner_or_permissions(**perms):
|
||||
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
|
||||
@@ -36,7 +36,7 @@ def interactive_config(red, token_set, prefix_set):
|
||||
while not prefix:
|
||||
prefix = input("Prefix> ")
|
||||
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("> "):
|
||||
prefix = ""
|
||||
if prefix:
|
||||
@@ -72,7 +72,7 @@ def parse_cli_flags(args):
|
||||
parser.add_argument(
|
||||
"--list-instances",
|
||||
action="store_true",
|
||||
help="List all instance names setup " "with 'redbot-setup'",
|
||||
help="List all instance names setup with 'redbot-setup'",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--owner",
|
||||
@@ -117,7 +117,7 @@ def parse_cli_flags(args):
|
||||
parser.add_argument(
|
||||
"--not-bot",
|
||||
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(
|
||||
"--dry-run",
|
||||
@@ -131,12 +131,22 @@ def parse_cli_flags(args):
|
||||
parser.add_argument(
|
||||
"--mentionable",
|
||||
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(
|
||||
"--rpc",
|
||||
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(
|
||||
"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")
|
||||
except ImportError as e:
|
||||
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
|
||||
return mod.__spec__
|
||||
|
||||
@@ -342,9 +342,7 @@ class CogManagerUI:
|
||||
Add a path to the list of available cog paths.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
await ctx.send(
|
||||
_("That path does not exist or does not" " point to a valid directory.")
|
||||
)
|
||||
await ctx.send(_("That path does not exist or does not point to a valid directory."))
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -419,7 +417,7 @@ class CogManagerUI:
|
||||
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
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()
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
from discord.ext.commands import *
|
||||
from .commands 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.
|
||||
"""
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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"]
|
||||
|
||||
_ = Translator("commands.commands", __file__)
|
||||
|
||||
|
||||
class Command(commands.Command):
|
||||
"""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()
|
||||
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):
|
||||
"""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
|
||||
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
|
||||
|
||||
@@ -152,6 +152,11 @@ class Context(commands.Context):
|
||||
else:
|
||||
return self.bot.color
|
||||
|
||||
@property
|
||||
def embed_color(self):
|
||||
# Rather than double awaiting.
|
||||
return self.embed_colour
|
||||
|
||||
async def embed_requested(self):
|
||||
"""
|
||||
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 collections
|
||||
from copy import deepcopy
|
||||
from typing import Union, Tuple
|
||||
from typing import Union, Tuple, TYPE_CHECKING
|
||||
|
||||
import discord
|
||||
|
||||
from .data_manager import cog_data_path, core_data_path
|
||||
from .drivers import get_driver
|
||||
|
||||
from .utils import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .drivers.red_base import BaseDriver
|
||||
|
||||
@@ -31,12 +29,15 @@ class _ValueCtxManager:
|
||||
def __init__(self, value_obj, coro):
|
||||
self.value_obj = value_obj
|
||||
self.coro = coro
|
||||
self.raw_value = None
|
||||
self.__original_value = None
|
||||
|
||||
def __await__(self):
|
||||
return self.coro.__await__()
|
||||
|
||||
async def __aenter__(self):
|
||||
self.raw_value = await self
|
||||
self.__original_value = deepcopy(self.raw_value)
|
||||
if not isinstance(self.raw_value, (list, dict)):
|
||||
raise TypeError(
|
||||
"Type of retrieved value must be mutable (i.e. "
|
||||
@@ -46,7 +47,8 @@ class _ValueCtxManager:
|
||||
return self.raw_value
|
||||
|
||||
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:
|
||||
@@ -225,7 +227,7 @@ class Group(Value):
|
||||
identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
|
||||
)
|
||||
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:
|
||||
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
||||
|
||||
@@ -337,7 +339,7 @@ class Group(Value):
|
||||
default = poss_default
|
||||
|
||||
try:
|
||||
return deepcopy(await self.driver.get(*self.identifiers, *path))
|
||||
return await self.driver.get(*self.identifiers, *path)
|
||||
except KeyError:
|
||||
if default is not ...:
|
||||
return default
|
||||
@@ -367,7 +369,7 @@ class Group(Value):
|
||||
|
||||
"""
|
||||
if not defaults:
|
||||
defaults = deepcopy(self.defaults)
|
||||
defaults = self.defaults
|
||||
|
||||
for key, value in current.items():
|
||||
if isinstance(value, collections.Mapping):
|
||||
@@ -394,7 +396,7 @@ class Group(Value):
|
||||
# is equivalent to
|
||||
|
||||
data = {"foo": {"bar": None}}
|
||||
d["foo"]["bar"] = "baz"
|
||||
data["foo"]["bar"] = "baz"
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -441,6 +443,7 @@ class Config:
|
||||
attempting to access data.
|
||||
|
||||
"""
|
||||
|
||||
GLOBAL = "GLOBAL"
|
||||
GUILD = "GUILD"
|
||||
CHANNEL = "TEXTCHANNEL"
|
||||
@@ -624,9 +627,7 @@ class Config:
|
||||
existing_is_dict = isinstance(_partial[k], dict)
|
||||
if val_is_dict != existing_is_dict:
|
||||
# != is XOR
|
||||
raise KeyError(
|
||||
"You cannot register a Group and a Value under" " the same name."
|
||||
)
|
||||
raise KeyError("You cannot register a Group and a Value under the same name.")
|
||||
if val_is_dict:
|
||||
Config._update_defaults(v, _partial=_partial[k])
|
||||
else:
|
||||
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
from random import SystemRandom
|
||||
from string import ascii_letters, digits
|
||||
from distutils.version import StrictVersion
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
@@ -21,9 +22,7 @@ import pkg_resources
|
||||
from redbot.core import __version__
|
||||
from redbot.core import checks
|
||||
from redbot.core import i18n
|
||||
from redbot.core import rpc
|
||||
from redbot.core import commands
|
||||
from .utils import TYPE_CHECKING
|
||||
from .utils.chat_formatting import pagify, box, inline
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -44,12 +43,201 @@ OWNER_DISCLAIMER = (
|
||||
_ = 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(_)
|
||||
class Core:
|
||||
class Core(CoreLogic):
|
||||
"""Commands related to core functions"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot # type: Red
|
||||
super().__init__(bot)
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
@@ -87,7 +275,7 @@ class Core:
|
||||
"".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="Python", value=python_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.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:
|
||||
await ctx.send(embed=embed)
|
||||
@@ -153,7 +341,6 @@ class Core:
|
||||
user_setting = await self.bot.db.user(ctx.author).embeds()
|
||||
text += "User setting: {}".format(user_setting)
|
||||
await ctx.send(box(text))
|
||||
await ctx.send_help()
|
||||
|
||||
@embedset.command(name="global")
|
||||
@checks.is_owner()
|
||||
@@ -173,6 +360,7 @@ class Core:
|
||||
|
||||
@embedset.command(name="guild")
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.guild_only()
|
||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
|
||||
"""
|
||||
Toggle the guild's embed setting.
|
||||
@@ -236,8 +424,7 @@ class Core:
|
||||
async def invite(self, ctx):
|
||||
"""Show's Red's invite url"""
|
||||
if self.bot.user.bot:
|
||||
app_info = await self.bot.application_info()
|
||||
await ctx.author.send(discord.utils.oauth_url(app_info.id))
|
||||
await ctx.author.send(await self._invite_url())
|
||||
else:
|
||||
await ctx.send("I'm not a bot account. I have no invite URL.")
|
||||
|
||||
@@ -249,7 +436,7 @@ class Core:
|
||||
author = ctx.author
|
||||
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):
|
||||
return m.author == author
|
||||
@@ -319,149 +506,70 @@ class Core:
|
||||
async def load(self, ctx, *, cog_name: str):
|
||||
"""Loads packages"""
|
||||
|
||||
failed_packages = []
|
||||
loaded_packages = []
|
||||
notfound_packages = []
|
||||
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||
async with ctx.typing():
|
||||
loaded, failed, not_found = await self._load(cog_names)
|
||||
|
||||
cognames = [c.strip() for c in cog_name.split(" ")]
|
||||
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:
|
||||
if loaded:
|
||||
fmt = "Loaded {packs}"
|
||||
formed = self.get_package_strings(loaded_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
formed = self._get_package_strings(loaded, fmt)
|
||||
await ctx.send(formed)
|
||||
|
||||
if failed_packages:
|
||||
if failed:
|
||||
fmt = (
|
||||
"Failed to load package{plural} {packs}. Check your console or "
|
||||
"logs for details."
|
||||
)
|
||||
formed = self.get_package_strings(failed_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
formed = self._get_package_strings(failed, fmt)
|
||||
await ctx.send(formed)
|
||||
|
||||
if notfound_packages:
|
||||
if not_found:
|
||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
||||
await ctx.send(_(formed))
|
||||
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
||||
await ctx.send(formed)
|
||||
|
||||
@commands.group()
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def unload(self, ctx, *, cog_name: str):
|
||||
"""Unloads packages"""
|
||||
cognames = [c.strip() for c in cog_name.split(" ")]
|
||||
failed_packages = []
|
||||
unloaded_packages = []
|
||||
|
||||
for c in cognames:
|
||||
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))
|
||||
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||
|
||||
if unloaded_packages:
|
||||
unloaded, failed = await self._unload(cog_names)
|
||||
|
||||
if 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))
|
||||
|
||||
if failed_packages:
|
||||
if failed:
|
||||
fmt = "The package{plural} {packs} {other} not loaded."
|
||||
formed = self.get_package_strings(failed_packages, fmt, ("is", "are"))
|
||||
await ctx.send(_(formed))
|
||||
formed = self._get_package_strings(failed, fmt, ("is", "are"))
|
||||
await ctx.send(formed)
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
async def _reload(self, ctx, *, cog_name: str):
|
||||
async def reload_(self, ctx, *, cog_name: str):
|
||||
"""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:
|
||||
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:
|
||||
if loaded:
|
||||
fmt = "Package{plural} {packs} {other} reloaded."
|
||||
formed = self.get_package_strings(loaded_packages, fmt, ("was", "were"))
|
||||
await ctx.send(_(formed))
|
||||
formed = self._get_package_strings(loaded, fmt, ("was", "were"))
|
||||
await ctx.send(formed)
|
||||
|
||||
if failed_packages:
|
||||
fmt = "Failed to reload package{plural} {packs}. Check your " "logs for details"
|
||||
formed = self.get_package_strings(failed_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
if failed:
|
||||
fmt = "Failed to reload package{plural} {packs}. Check your logs for details"
|
||||
formed = self._get_package_strings(failed, fmt)
|
||||
await ctx.send(formed)
|
||||
|
||||
if notfound_packages:
|
||||
if not_found:
|
||||
fmt = "The package{plural} {packs} {other} not found in any cog path."
|
||||
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
|
||||
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
|
||||
formed = self._get_package_strings(not_found, fmt, ("was", "were"))
|
||||
await ctx.send(formed)
|
||||
|
||||
@commands.command(name="shutdown")
|
||||
@checks.is_owner()
|
||||
@@ -491,55 +599,32 @@ class Core:
|
||||
pass
|
||||
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")
|
||||
async def _set(self, ctx):
|
||||
"""Changes Red's settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
|
||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
||||
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
||||
if ctx.guild:
|
||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id) or "Not set"
|
||||
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
|
||||
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:
|
||||
prefixes = await ctx.bot.db.prefix()
|
||||
locale = await ctx.bot.db.locale()
|
||||
|
||||
prefix_string = " ".join(prefixes)
|
||||
settings = (
|
||||
"{} Settings:\n\n"
|
||||
"Prefixes: {}\n"
|
||||
"Admin role: {}\n"
|
||||
"Mod role: {}\n"
|
||||
"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,
|
||||
)
|
||||
f"{ctx.bot.user.name} Settings:\n\n"
|
||||
f"Prefixes: {prefix_string}\n"
|
||||
f"{guild_settings}"
|
||||
f"Locale: {locale}"
|
||||
)
|
||||
await ctx.send(box(settings))
|
||||
await ctx.send_help()
|
||||
|
||||
@_set.command()
|
||||
@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"])
|
||||
@checks.is_owner()
|
||||
async def colour(self, ctx, *, colour: discord.Colour = None):
|
||||
"""
|
||||
Sets a default colour to be used for the bot's embeds.
|
||||
|
||||
Acceptable values cor the colour parameter can be found at:
|
||||
Acceptable values for the colour parameter can be found at:
|
||||
|
||||
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):
|
||||
"""Sets Red's username"""
|
||||
try:
|
||||
await ctx.bot.user.edit(username=username)
|
||||
await self._name(name=username)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
_(
|
||||
@@ -735,7 +853,7 @@ class Core:
|
||||
try:
|
||||
await ctx.guild.me.edit(nick=nickname)
|
||||
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:
|
||||
await ctx.send("Done.")
|
||||
|
||||
@@ -746,8 +864,7 @@ class Core:
|
||||
if not prefixes:
|
||||
await ctx.send_help()
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot.db.prefix.set(prefixes)
|
||||
await self._prefixes(prefixes)
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command(aliases=["serverprefixes"])
|
||||
@@ -779,7 +896,7 @@ class Core:
|
||||
|
||||
for i in range(length):
|
||||
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(token)
|
||||
|
||||
@@ -869,8 +986,7 @@ class Core:
|
||||
@checks.is_owner()
|
||||
async def helpset(self, ctx: commands.Context):
|
||||
"""Manage settings for the help command."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@helpset.command(name="pagecharlimit")
|
||||
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"):
|
||||
repo_output = []
|
||||
repo_mgr = downloader_cog._repo_manager
|
||||
for n, repo in repo_mgr._repos:
|
||||
repo_output.append(
|
||||
{{"url": repo.url, "name": repo.name, "branch": repo.branch}}
|
||||
)
|
||||
for repo in repo_mgr._repos.values():
|
||||
repo_output.append({"url": repo.url, "name": repo.name, "branch": repo.branch})
|
||||
repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json"
|
||||
with open(str(repo_filename), "w") as f:
|
||||
f.write(json.dumps(repo_output, indent=4))
|
||||
@@ -1058,7 +1172,7 @@ class Core:
|
||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||
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)
|
||||
|
||||
@@ -1079,7 +1193,7 @@ class Core:
|
||||
await owner.send(content, embed=e)
|
||||
except discord.InvalidArgument:
|
||||
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:
|
||||
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)))
|
||||
except discord.InvalidArgument:
|
||||
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:
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
@@ -1136,7 +1250,7 @@ class Core:
|
||||
await destination.send(embed=e)
|
||||
except:
|
||||
await ctx.send(
|
||||
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
||||
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("Message delivered to {}").format(destination))
|
||||
@@ -1146,7 +1260,7 @@ class Core:
|
||||
await destination.send("{}\n{}".format(box(response), content))
|
||||
except:
|
||||
await ctx.send(
|
||||
_("Sorry, I couldn't deliver your message " "to {}").format(destination)
|
||||
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("Message delivered to {}").format(destination))
|
||||
@@ -1157,8 +1271,7 @@ class Core:
|
||||
"""
|
||||
Whitelist management commands.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@whitelist.command(name="add")
|
||||
async def whitelist_add(self, ctx, user: discord.User):
|
||||
@@ -1180,7 +1293,7 @@ class Core:
|
||||
|
||||
msg = _("Whitelisted Users:")
|
||||
for user in curr_list:
|
||||
msg.append("\n\t- {}".format(user))
|
||||
msg += "\n\t- {}".format(user)
|
||||
|
||||
for page in pagify(msg):
|
||||
await ctx.send(box(page))
|
||||
@@ -1216,8 +1329,7 @@ class Core:
|
||||
"""
|
||||
blacklist management commands.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@blacklist.command(name="add")
|
||||
async def blacklist_add(self, ctx, user: discord.User):
|
||||
@@ -1243,7 +1355,7 @@ class Core:
|
||||
|
||||
msg = _("blacklisted Users:")
|
||||
for user in curr_list:
|
||||
msg.append("\n\t- {}".format(user))
|
||||
msg += "\n\t- {}".format(user)
|
||||
|
||||
for page in pagify(msg):
|
||||
await ctx.send(box(page))
|
||||
@@ -1273,6 +1385,177 @@ class Core:
|
||||
await ctx.bot.db.blacklist.set([])
|
||||
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
|
||||
async def rpc_load(self, request):
|
||||
cog_name = request.params[0]
|
||||
@@ -1281,7 +1564,7 @@ class Core:
|
||||
if spec is None:
|
||||
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)
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@ import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from copy import deepcopy
|
||||
import hashlib
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import appdirs
|
||||
import tempfile
|
||||
|
||||
from .json_io import JsonIO
|
||||
from .utils import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
|
||||
__all__ = [
|
||||
"create_temp_config",
|
||||
"load_basic_configuration",
|
||||
"cog_data_path",
|
||||
"core_data_path",
|
||||
@@ -43,6 +42,26 @@ if not config_dir:
|
||||
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):
|
||||
"""Loads the basic bootstrap configuration necessary for `Config`
|
||||
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:
|
||||
if basic_config is None:
|
||||
raise RuntimeError(
|
||||
"You must load the basic config before you" " can get the base data path."
|
||||
)
|
||||
raise RuntimeError("You must load the basic config before you can get the base data path.")
|
||||
path = basic_config["DATA_PATH"]
|
||||
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())
|
||||
except RuntimeError as e:
|
||||
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
|
||||
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())
|
||||
except RuntimeError as e:
|
||||
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
|
||||
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
|
||||
core_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
@@ -47,9 +47,7 @@ class Dev:
|
||||
"""
|
||||
if e.text is None:
|
||||
return box("{0.__class__.__name__}: {0}".format(e), lang="py")
|
||||
return box(
|
||||
"{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
|
||||
)
|
||||
return box("{0.text}{1:>{0.offset}}\n{2}: {0}".format(e, "^", type(e).__name__), lang="py")
|
||||
|
||||
@staticmethod
|
||||
def get_pages(msg: str):
|
||||
@@ -209,12 +207,12 @@ class Dev:
|
||||
|
||||
if ctx.channel.id in self.sessions:
|
||||
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
|
||||
|
||||
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: (
|
||||
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
|
||||
|
||||
@@ -2,7 +2,6 @@ __all__ = ["BaseDriver"]
|
||||
|
||||
|
||||
class BaseDriver:
|
||||
|
||||
def __init__(self, cog_name, identifier):
|
||||
self.cog_name = cog_name
|
||||
self.unique_cog_identifier = identifier
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
import copy
|
||||
import weakref
|
||||
import logging
|
||||
|
||||
@@ -97,7 +98,7 @@ class JSON(BaseDriver):
|
||||
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
||||
for i in full_identifiers:
|
||||
partial = partial[i]
|
||||
return partial
|
||||
return copy.deepcopy(partial)
|
||||
|
||||
async def set(self, *identifiers: str, value=None):
|
||||
partial = self.data
|
||||
@@ -107,7 +108,7 @@ class JSON(BaseDriver):
|
||||
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)
|
||||
|
||||
async def clear(self, *identifiers: str):
|
||||
|
||||
@@ -78,7 +78,7 @@ class Mongo(BaseDriver):
|
||||
)
|
||||
|
||||
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:
|
||||
partial = partial[i]
|
||||
|
||||
@@ -11,13 +11,13 @@ from pkg_resources import DistributionNotFound
|
||||
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from . import __version__
|
||||
from . import __version__, commands
|
||||
from .data_manager import storage_type
|
||||
from .utils.chat_formatting import inline, bordered, pagify, box
|
||||
from .utils import fuzzy_command_search
|
||||
from colorama import Fore, Style, init
|
||||
from . import rpc
|
||||
|
||||
log = logging.getLogger("red")
|
||||
sentry_log = logging.getLogger("red.sentry")
|
||||
@@ -49,7 +49,6 @@ def should_log_sentry(exception) -> bool:
|
||||
|
||||
|
||||
def init_events(bot, cli_flags):
|
||||
|
||||
@bot.event
|
||||
async def on_connect():
|
||||
if bot.uptime is None:
|
||||
@@ -85,6 +84,9 @@ def init_events(bot, cli_flags):
|
||||
if packages:
|
||||
print("Loaded packages: " + ", ".join(packages))
|
||||
|
||||
if bot.rpc_enabled:
|
||||
await bot.rpc.initialize()
|
||||
|
||||
guilds = len(bot.guilds)
|
||||
users = len(set([m for m in bot.get_all_members()]))
|
||||
|
||||
@@ -97,7 +99,7 @@ def init_events(bot, cli_flags):
|
||||
else:
|
||||
invite_url = None
|
||||
|
||||
prefixes = await bot.db.prefix()
|
||||
prefixes = cli_flags.prefix or (await bot.db.prefix())
|
||||
lang = await bot.db.locale()
|
||||
red_version = __version__
|
||||
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)))
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
INFO.append(
|
||||
"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:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
INFO.append(
|
||||
"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)
|
||||
await owner.send(
|
||||
"Your Red instance is out of date! {} is the current "
|
||||
"version, however you are using {}!".format(
|
||||
data["info"]["version"], red_version
|
||||
)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
INFO2 = []
|
||||
|
||||
sentry = await bot.db.enable_sentry()
|
||||
@@ -173,8 +175,6 @@ def init_events(bot, cli_flags):
|
||||
print("\nInvite URL: {}\n".format(invite_url))
|
||||
|
||||
bot.color = discord.Colour(await bot.db.color())
|
||||
if bot.rpc_enabled:
|
||||
await bot.rpc.initialize()
|
||||
|
||||
@bot.event
|
||||
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):
|
||||
if isinstance(error, commands.MissingRequiredArgument):
|
||||
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):
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, commands.DisabledCommand):
|
||||
@@ -226,7 +231,9 @@ def init_events(bot, cli_flags):
|
||||
term = ctx.invoked_with + " "
|
||||
if len(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):
|
||||
pass
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
|
||||
@@ -3,7 +3,6 @@ from . import commands
|
||||
|
||||
|
||||
def init_global_checks(bot):
|
||||
|
||||
@bot.check
|
||||
async def global_perms(ctx):
|
||||
"""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_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:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
EMPTY_STRING = u"\u200b"
|
||||
EMPTY_STRING = "\u200b"
|
||||
|
||||
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
|
||||
|
||||
@@ -60,6 +60,12 @@ class Help(formatter.HelpFormatter):
|
||||
def pm_check(self, ctx):
|
||||
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
|
||||
def me(self):
|
||||
return self.context.me
|
||||
@@ -76,10 +82,7 @@ class Help(formatter.HelpFormatter):
|
||||
if self.pm_check(self.context):
|
||||
return self.context.bot.color
|
||||
else:
|
||||
if await self.context.bot.db.guild(self.context.guild).use_bot_color():
|
||||
return self.context.bot.color
|
||||
else:
|
||||
return self.me.color
|
||||
return await self.context.embed_colour()
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
@@ -184,15 +187,19 @@ class Help(formatter.HelpFormatter):
|
||||
for category, commands_ in itertools.groupby(data, key=category):
|
||||
commands_ = sorted(commands_)
|
||||
if len(commands_) > 0:
|
||||
field = EmbedField(category, self._add_subcommands(commands_), False)
|
||||
emb["fields"].append(field)
|
||||
for i, page in enumerate(
|
||||
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:
|
||||
# Get list of commands for category
|
||||
filtered = sorted(filtered)
|
||||
if filtered:
|
||||
for i, page in enumerate(
|
||||
pagify(self._add_subcommands(filtered), page_length=1020)
|
||||
pagify(self._add_subcommands(filtered), page_length=1000)
|
||||
):
|
||||
title = (
|
||||
"**__Commands:__**"
|
||||
@@ -202,7 +209,6 @@ class Help(formatter.HelpFormatter):
|
||||
if i > 0:
|
||||
title += " (continued)"
|
||||
field = EmbedField(title, page, False)
|
||||
# This will still break at 6k total chars, hope that isnt an issue later
|
||||
emb["fields"].append(field)
|
||||
|
||||
return emb
|
||||
@@ -281,11 +287,10 @@ class Help(formatter.HelpFormatter):
|
||||
embed.set_author(**self.author)
|
||||
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
|
||||
out = fuzzy_command_search(ctx, " ".join(ctx.args[1:]))
|
||||
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
|
||||
|
||||
@@ -296,7 +301,7 @@ class Help(formatter.HelpFormatter):
|
||||
return embed
|
||||
|
||||
|
||||
@commands.command()
|
||||
@commands.command(hidden=True)
|
||||
async def help(ctx, *cmds: str):
|
||||
"""Shows help documentation.
|
||||
|
||||
@@ -326,11 +331,19 @@ async def help(ctx, *cmds: str):
|
||||
command = ctx.bot.all_commands.get(name)
|
||||
if command is None:
|
||||
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:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
|
||||
)
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||
)
|
||||
return
|
||||
if use_embeds:
|
||||
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)
|
||||
if command is None:
|
||||
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:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
|
||||
)
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
|
||||
return
|
||||
|
||||
for key in cmds[1:]:
|
||||
@@ -354,13 +373,19 @@ async def help(ctx, *cmds: str):
|
||||
command = command.all_commands.get(key)
|
||||
if command is None:
|
||||
if use_embeds:
|
||||
await destination.send(
|
||||
embed=await ctx.bot.formatter.cmd_not_found(ctx, key)
|
||||
)
|
||||
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:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name))
|
||||
)
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||
)
|
||||
return
|
||||
except AttributeError:
|
||||
if use_embeds:
|
||||
|
||||
@@ -461,6 +461,9 @@ async def create_case(
|
||||
if not await case_type.is_enabled():
|
||||
return None
|
||||
|
||||
if user == bot.user:
|
||||
return None
|
||||
|
||||
next_case_number = int(await get_next_case_number(guild))
|
||||
|
||||
case = Case(
|
||||
|
||||
@@ -1,117 +1,74 @@
|
||||
import weakref
|
||||
import asyncio
|
||||
|
||||
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
|
||||
|
||||
__all__ = ["methods", "RPC", "Methods"]
|
||||
|
||||
log = logging.getLogger("red.rpc")
|
||||
|
||||
|
||||
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()
|
||||
__all__ = ["RPC", "RPCMixin", "get_name"]
|
||||
|
||||
|
||||
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):
|
||||
methods.add(self.all_methods, name="all_methods")
|
||||
def _add_method(self, method, prefix=""):
|
||||
if not asyncio.iscoroutinefunction(method):
|
||||
return
|
||||
|
||||
async def all_methods(self):
|
||||
return list(methods.all_methods())
|
||||
name = get_name(method, prefix)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.app = web.Application(loop=bot.loop)
|
||||
self.app.router.add_post("/rpc", self.handle)
|
||||
def __init__(self):
|
||||
self.app = web.Application()
|
||||
self._rpc = RedRpc()
|
||||
self.app.router.add_route("*", "/", self._rpc)
|
||||
|
||||
self.app_handler = self.app.make_handler()
|
||||
|
||||
self.server = None
|
||||
|
||||
super().__init__()
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Finalizes the initialization of the RPC server and allows it to begin
|
||||
@@ -126,10 +83,79 @@ class RPC(BaseRPCMethodMixin):
|
||||
"""
|
||||
self.server.close()
|
||||
|
||||
async def handle(self, request):
|
||||
request = await request.text()
|
||||
response = await methods.dispatch(request)
|
||||
if response.is_notification:
|
||||
return web.Response()
|
||||
else:
|
||||
return web.json_response(response, status=response.http_status)
|
||||
def add_method(self, method, prefix: str = None):
|
||||
if prefix is None:
|
||||
prefix = method.__self__.__class__.__name__.lower()
|
||||
|
||||
if not asyncio.iscoroutinefunction(method):
|
||||
raise TypeError("RPC methods must be coroutines.")
|
||||
|
||||
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
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
from redbot.core import commands
|
||||
from fuzzywuzzy import process
|
||||
from .chat_formatting import box
|
||||
|
||||
try:
|
||||
from typing import TYPE_CHECKING
|
||||
except ImportError:
|
||||
TYPE_CHECKING = False
|
||||
|
||||
try:
|
||||
from typing import NewType
|
||||
except ImportError:
|
||||
def fuzzy_filter(record):
|
||||
return record.funcName != "extractWithoutOrder"
|
||||
|
||||
def NewType(name, tp):
|
||||
return type(name, (tp,), {})
|
||||
|
||||
logging.getLogger().addFilter(fuzzy_filter)
|
||||
|
||||
|
||||
def safe_delete(pth: Path):
|
||||
@@ -31,9 +27,49 @@ def safe_delete(pth: Path):
|
||||
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 = ""
|
||||
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(
|
||||
pos,
|
||||
ctx,
|
||||
|
||||
@@ -18,6 +18,7 @@ class AntiSpam:
|
||||
Where quantity represents the maximum amount of times
|
||||
something should be allowed in an interval.
|
||||
"""
|
||||
|
||||
# TODO : Decorator interface for command check using `spammy`
|
||||
# with insertion of the antispam element into context
|
||||
# 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)
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Union, Iterable
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
_ReactableEmoji = Union[str, discord.Emoji]
|
||||
|
||||
|
||||
async def menu(
|
||||
ctx: commands.Context,
|
||||
@@ -66,8 +70,8 @@ async def menu(
|
||||
message = await ctx.send(embed=current_page)
|
||||
else:
|
||||
message = await ctx.send(current_page)
|
||||
for key in controls.keys():
|
||||
await message.add_reaction(key)
|
||||
# Don't wait for reactions to be added (GH-1797)
|
||||
ctx.bot.loop.create_task(_add_menu_reactions(message, controls.keys()))
|
||||
else:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
@@ -148,4 +152,12 @@ async def close_menu(
|
||||
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}
|
||||
|
||||
@@ -135,16 +135,10 @@ async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Membe
|
||||
|
||||
if isinstance(obj, discord.Role):
|
||||
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
|
||||
elif admin_role and discord.utils.get(user.roles, name=admin_role):
|
||||
return True
|
||||
elif mod_role and discord.utils.get(user.roles, name=mod_role):
|
||||
elif discord.utils.find(lambda r: r.id in (admin_role_id, mod_role_id), user.roles):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -220,17 +214,14 @@ async def is_admin_or_superior(
|
||||
else:
|
||||
raise TypeError("Only messages, members or roles may be passed")
|
||||
|
||||
server = obj.guild
|
||||
admin_role_id = await bot.db.guild(server).admin_role()
|
||||
admin_role_id = await bot.db.guild(obj.guild).admin_role()
|
||||
|
||||
if isinstance(obj, discord.Role):
|
||||
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):
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -5,9 +5,11 @@ import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
import pkg_resources
|
||||
from pathlib import Path
|
||||
from distutils.version import StrictVersion
|
||||
from redbot.setup import (
|
||||
basic_setup,
|
||||
load_existing_config,
|
||||
@@ -16,18 +18,17 @@ from redbot.setup import (
|
||||
create_backup,
|
||||
save_config,
|
||||
)
|
||||
from redbot.core import __version__
|
||||
from redbot.core.utils import safe_delete
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
if sys.platform == "linux":
|
||||
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
|
||||
|
||||
INTRO = (
|
||||
"==========================\n" "Red Discord Bot - Launcher\n" "==========================\n"
|
||||
)
|
||||
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
||||
|
||||
IS_WINDOWS = os.name == "nt"
|
||||
IS_MAC = sys.platform == "darwin"
|
||||
@@ -383,15 +384,30 @@ def debug_info():
|
||||
+ "User: {}\n".format(user_who_ran)
|
||||
)
|
||||
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():
|
||||
if IS_WINDOWS:
|
||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||
clear_screen()
|
||||
loop = asyncio.get_event_loop()
|
||||
outdated, new_version = loop.run_until_complete(is_outdated())
|
||||
while True:
|
||||
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("2. Run Red")
|
||||
print("3. Update Red")
|
||||
@@ -420,13 +436,12 @@ def main_menu():
|
||||
basic_setup()
|
||||
wait()
|
||||
elif choice == "5":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
||||
loop.run_until_complete(remove_instance_interaction())
|
||||
wait()
|
||||
elif choice == "6":
|
||||
debug_info()
|
||||
elif choice == "7":
|
||||
while True:
|
||||
loop = asyncio.get_event_loop()
|
||||
clear_screen()
|
||||
print("==== Reinstall Red ====")
|
||||
print(
|
||||
@@ -457,7 +472,7 @@ def main_menu():
|
||||
def main():
|
||||
if not PYTHON_OK:
|
||||
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
|
||||
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
|
||||
|
||||
__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")
|
||||
def monkeysession(request):
|
||||
@@ -27,7 +47,6 @@ def override_data_path(tmpdir):
|
||||
|
||||
@pytest.fixture()
|
||||
def coroutine():
|
||||
|
||||
async def some_coro(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
@@ -74,7 +93,6 @@ def guild_factory():
|
||||
mock_guild = namedtuple("Guild", "id members")
|
||||
|
||||
class GuildFactory:
|
||||
|
||||
def get(self):
|
||||
return mock_guild(random.randint(1, 999999999), [])
|
||||
|
||||
@@ -103,7 +121,6 @@ def member_factory(guild_factory):
|
||||
mock_member = namedtuple("Member", "id guild display_name")
|
||||
|
||||
class MemberFactory:
|
||||
|
||||
def get(self):
|
||||
return mock_member(random.randint(1, 999999999), guild_factory.get(), "Testing_Name")
|
||||
|
||||
@@ -120,7 +137,6 @@ def user_factory():
|
||||
mock_user = namedtuple("User", "id")
|
||||
|
||||
class UserFactory:
|
||||
|
||||
def get(self):
|
||||
return mock_user(random.randint(1, 999999999))
|
||||
|
||||
@@ -158,7 +174,7 @@ def red(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
|
||||
|
||||
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:
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
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)
|
||||
config_file = config_dir / "config.json"
|
||||
|
||||
@@ -101,7 +101,7 @@ def get_data_dir():
|
||||
)
|
||||
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):"):
|
||||
print("Please start the process over.")
|
||||
sys.exit(0)
|
||||
|
||||
@@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
|
||||
appdirs==1.4.3
|
||||
raven==6.5.0
|
||||
colorama==0.3.9
|
||||
jsonrpcserver
|
||||
aiohttp-json-rpc==0.8.7
|
||||
pyyaml==3.12
|
||||
fuzzywuzzy[speedup]<=0.16.0
|
||||
Red-Trivia>=1.1.1
|
||||
|
||||
33
setup.py
33
setup.py
@@ -1,6 +1,9 @@
|
||||
from distutils.core import setup
|
||||
from distutils import ccompiler
|
||||
from distutils.errors import CCompilerError, DistutilsPlatformError
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -11,7 +14,9 @@ IS_TRAVIS = "TRAVIS" in os.environ
|
||||
IS_DEPLOYING = "DEPLOYING" 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:
|
||||
dep_links = []
|
||||
|
||||
@@ -21,6 +26,20 @@ def get_package_list():
|
||||
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():
|
||||
with open("requirements.txt") as f:
|
||||
requirements = f.read().splitlines()
|
||||
@@ -31,6 +50,9 @@ def get_requirements():
|
||||
except ValueError:
|
||||
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):
|
||||
requirements.append("discord.py>=1.0.0a0")
|
||||
if sys.platform.startswith("linux"):
|
||||
@@ -89,10 +111,10 @@ setup(
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Framework :: AsyncIO",
|
||||
"Framework :: Pytest",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Documentation :: Sphinx",
|
||||
@@ -102,9 +124,10 @@ setup(
|
||||
"redbot=redbot.__main__:main",
|
||||
"redbot-setup=redbot.setup: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(),
|
||||
install_requires=get_requirements(),
|
||||
dependency_links=dep_links,
|
||||
@@ -113,6 +136,6 @@ setup(
|
||||
"mongo": ["motor"],
|
||||
"docs": ["sphinx>=1.7", "sphinxcontrib-asyncio", "sphinx_rtd_theme"],
|
||||
"voice": ["red-lavalink>=0.0.4"],
|
||||
"style": ["black"],
|
||||
"style": ["black==18.5b1"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,20 +2,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from redbot.cogs.admin import Admin
|
||||
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()
|
||||
from redbot.pytest.admin import *
|
||||
|
||||
|
||||
@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