Compare commits

...

58 Commits

Author SHA1 Message Date
palmtree5
219367e7c1 Bump version to 3.0.0b16 (#1804) 2018-06-10 15:07:57 -08:00
Will
7b64f10fc7 [V3] Pin discord.py for beta 16 release (#1848)
* Update main requirements

* Update docs requirements

* Black reformat after version update

* Pin dpy  in docs
2018-06-10 19:01:58 -04:00
Twentysix
1ad1744054 [V3 General] Fix online user count in [p]serverinfo (#1844)
* [General] Fix online user count in [p]serverinfo

* [General] Attempt to please the black gods
2018-06-10 23:25:54 +10:00
palmtree5
7b825f2cd7 [V3] Add some tests (#1590)
* Add some tests related to economy

* Add a test for repo removal

* black style formatting
2018-06-09 22:00:21 -04:00
aikaterna
3759fce090 [V3 Audio] Empty channel disconnect setting (#1832)
* [V3 Audio] Empty channel disconnect setting

* [V3 Audio] Small fixes

* [V3 Audio] Remove unused variable

* [V3 Audio] Timer task
2018-06-09 21:55:28 -04:00
Will
470521f7c8 [V3 DataConverter] Fix past nicks conversion for mod (#1840)
* Fix existing tests permanently modifying attributes

* Add dataconverter tests file

* Fix past nicks spec converter

* Update gitignore for dataconverter data files

* Add data file for dataconverter test

* Simplify fix
2018-06-09 20:27:06 -04:00
Tobotimus
a070dffb93 [V3 Streams] Fix streams race condition (#1834) 2018-06-10 00:12:58 +10:00
Will
9e7bc94aab Catch another error on windows compiler failure (#1830) 2018-06-09 03:48:03 +02:00
Tobotimus
033d0113a5 [V3] Send meaningful responses on conversion failure (#1817)
* [V3] Send meaningful responses on conversion failures

* Replace existing `discord.ext.commands` imports

Just to be sure

* Better Permissions converter response
2018-06-08 21:20:40 -04:00
Tobotimus
d0a53ed2df [V3 Core] Fix backup error (#1820) 2018-06-08 21:12:16 -04:00
Michael H
49b80e9fe3 [V3 Report] Patch issue with attachment grabbing (#1822)
* This fixes the issue on report's side

* This prevents delete_delay from causing future issues where message objects are needed

* black format pass

* use the tools we have to clean this logic up a lot
2018-06-08 21:08:00 -04:00
Michael H
d5f5ddbec5 [V3 Help] Fix formatter field pagination (#1813)
* fix prefix

* help formatter pagination fix

* index comp fix
2018-06-08 21:02:28 -04:00
Michael H
17c7dd658d [V3 Core] Command group automatic help (#1790)
* decorator inheritence

* black format

* add autohelp

* modify commands to use autohelp
2018-06-08 20:54:36 -04:00
Will
ca19ecaefc [V3 Downloader] Add a requirements list to cog info (#1827) 2018-06-08 20:48:46 -04:00
Tobotimus
c149f00f82 [V3 Menu] Don't block on adding reactions (#1808)
* [V3 Menu] Don't block on adding reactions

* Add a comment
2018-06-08 20:43:42 -04:00
Will
b041d59fc7 [V3 Downloader] Make hidden hidden and add disabled (#1828)
* Make hidden hidden and add disabled

* Add documentation
2018-06-08 20:39:07 -04:00
Will
b983d5904b [V3 RPC] Swap back to initial RPC library and hook into core commands (#1780)
* Switch RPC libs for websockets support

* Implement RPC handling for core

* Black reformat

* Fix docs for build on travis

* Modify RPC to use a Cog base class

* Refactor rpc server reference as global

* Handle cogbase unload method

* Add an init call to handle mutable base attributes

* Move RPC server reference back to the bot object

* Remove unused import

* Add tests for rpc method add/removal

* Add tests for rpc method add/removal and cog base unloading

* Add one more test

* Black reformat

* Add RPC mixin...fix MRO

* Correct internal rpc method names

* Add rpc test html file for debugging/example purposes

* Add documentation

* Add get_method_info

* Update docs with an example RPC call specifying parameter formatting

* Make rpc methods UPPER

* Black reformat

* Fix doc example

* Modify this to match new method naming convention

* Add more tests
2018-06-08 20:31:38 -04:00
Michael H
8b15053dd4 [V3 Mod/Modlog] prevent self-casing the bot + feedback for heirarchy (#1777)
* prevent the bot from being a modlog target

* prevent heirarchy issues in mod

* modify this comparison to avoid more complex mocking of the guild object in mod test

* spelling
2018-06-08 20:27:07 -04:00
Tobotimus
e15815cd97 [V3 Downloader] Don't do 3rd party agreement without command args (#1821) 2018-06-08 20:18:51 -04:00
palmtree5
94a64d8fae [V3 Downloader] Split available and installed cogs (#1826) 2018-06-08 19:58:20 -04:00
Michael H
fd7088de1a [V3 Help formatter] Better name-as-prefix handling (#1823)
* prefix handling

* actually, integration role isn't a valid way
2018-06-08 11:11:44 -04:00
Will
7d4946560d [V3] Fix typo in load (#1814) 2018-06-08 10:56:03 -04:00
Michael H
b7c9647e1a [V3] Fix dm help set (#1806) 2018-06-07 01:23:26 -04:00
Will
36b9f64aae [V3 Alias] Fix missing await (#1805) 2018-06-07 14:53:12 +10:00
Eslyium
60a72b2ba4 [V3] Cleanup quotes in cogs (#1782)
* Cleanup quotes in cogs

* More quote cleanup that I missed

fixed a little bit of grammar here and there as well.

* [V3 Warnings] Change allowcustomreasons docstring

To help not confuse users who would believe that the command would use allow or disallow.

* Run black reformat
2018-06-07 00:42:59 -04:00
jjay12365
f830f73ae6 [V3 Streams] Fixed issue with making YT Stream Embed (#1812)
fixed error not allowing bot to make yt stream embed
2018-06-06 14:57:25 -04:00
Michael H
95f51e1126 [V3] Add missing await for [p]set prefix (#1809) 2018-06-06 17:08:33 +10:00
Michael H
8916f55d52 [V3] permissions canrun fix (#1787)
* permissions canrun fix

* Missing await

* async gen
2018-06-05 12:33:45 -08:00
Michael H
4aaef9558a [V3 Core] local whitelist/blacklist (#1776)
* implements local whitelist/blacklist which had unused bot.db settings

This includes a role listing

* format pass

* Update core_commands.py

* .

* black format pass
2018-06-05 12:19:44 -08:00
palmtree5
0b78664792 [V3 Fuzzy search] fix several issues with this feature (#1788)
* [V3 Fuzzy search] fix several issues with this feature

* Make it check if parent commands are hidden

* Check if compiler available in setup.py

* Let's just compile a dummy C file to check compiler availability

* Add a missing import + remove unneeded code
2018-06-05 22:14:11 +02:00
Michael H
db5d4d5158 [V3 launcher] token can be an empty string (#1794)
* token can be an empty string

* I like this sytlistically more
2018-06-05 11:59:42 -08:00
Tobotimus
0dfd8b6453 [V3 Docs] Add intersphinx link to discord.ext.commands api reference (#1764) 2018-06-03 17:32:25 -08:00
rngesus-wept
11a2fb1088 [V3 Core] Fix display of whitelist and blacklist members (#1789) 2018-06-03 21:41:36 +10:00
Michael H
40feeff442 [V3 core.commands] decorator inheritence fix (#1786)
* decorator inheritence

* black format
2018-06-02 18:34:30 -08:00
Michael H
a0a2976e0a [V3] Fixes issue preventing token reset from setup (#1771)
* fixes issue preventing token reset.

Also removes a faulty assumption about not needing cleanup tasks

* Update __main__.py

remove unneeded condition
2018-06-02 18:43:26 -04:00
Michael H
741f3cbdcc [V3 CLI] CLI prefix args correctly display in the on_ready print (#1770) 2018-06-02 18:36:13 -04:00
Michael H
a6965c4b5a [V3] Hide help command from help (#1772) 2018-06-02 18:29:16 -04:00
Tobotimus
19b05e632c [V3] Use typing.TYPE_CHECKING instead of utils.TYPE_CHECKING (#1778)
Not needed any more as we no longer support python<3.6
2018-06-02 18:24:16 -04:00
Michael H
8610b47a68 [V3 Admin] Announce ignore parameter modification (#1781) 2018-06-02 18:20:01 -04:00
aikaterna
2ab8890540 [V3 Audio] Check for empty queue in [p]skip (#1769)
Privileged users outside of the channel could invoke skip with an empty queue.
2018-06-02 18:15:10 -04:00
Michael H
5de5a519c3 [V3 Permissions] Don't rely on load order to be consistent (#1760)
* Modifies permissions re #1758

* requested changes in
2018-06-02 18:10:40 -04:00
Tobotimus
0d193d3e9e [V3] Make bot send typing whilst loading cogs (#1756)
* Show bot is responsive during cog load

* Log download of Lavalink.jar event

* Fix #1709's other bug

* Reformat

* Update core_commands.py from merge
2018-06-02 18:06:10 -04:00
Tobotimus
622382f425 [V3] Clean up some ugly auto-formatted strings (#1753)
* [V3] Cleanup some ugly auto-formatted strings

* Reformat
2018-06-02 18:01:14 -04:00
Tobotimus
c1f09326cc [V3] Use sys.exit() over exit() (#1755) 2018-06-02 17:56:28 -04:00
Tobotimus
ddbbba4aaa [V3] Ignore .idea/ directory entirely (#1754)
* [V3] Ignore .idea folder entirely

Since we're not including anything from it currently, so there's no reason not to ignore it.

* Ignore IDEA project files
2018-06-02 11:55:34 +10:00
Tobotimus
bcf7ea30c5 [V3 Core] Add a much-needed forward reference (#1763) 2018-06-02 11:20:06 +10:00
Will
35e9fab701 [V3 Admin] Add notes about case sensitivity for selfrole (#1762) 2018-06-02 11:01:08 +10:00
Will
864b6d313e [V3 Core Commands] Refactor some commands for testing/RPC (#1691)
* Extract load/unload/reload

* Add a few more commands

* Refactor load/unload signature

* Add invite URL and version info

* Black fixes

* Split the incoming cog names in reload correctly

* Reformat

* Remove meta.bot
2018-06-02 10:49:59 +10:00
aikaterna
d47d12e961 [V3 Audio] Restrict check for reactions on [p]now (#1752) 2018-06-02 02:44:30 +02:00
Michael H
9f0e752318 [V3 Core] Fix error on [p]set command when used in DM (#1748)
* issue #1741 fix

* requested changes
2018-06-02 09:26:12 +10:00
palmtree5
34bd5ead15 [V3] Drop 3.5 support (#1721) 2018-06-01 19:20:21 -04:00
Redjumpman
1fd5dffdc7 [V3/Misc] Spelling, Grammar, and doc string fixes. (#1747)
* Update streams.py

* Update filter.py

* Update permissions.py

* Update customcom.py

* Update image.py

* Update trivia.py

* Update warnings.py
2018-06-01 19:20:12 +10:00
Michael H
6d7a900bbb [V3] Use Embed.Empty for unset embed colour (#1750) 2018-05-31 14:31:44 +10:00
Tobotimus
fb4f921159 [V3 Docs] Pin RTD yarl version (#1744) 2018-05-29 18:42:30 -08:00
Tobotimus
14cc701b25 [V3] Update black version and reformat (#1745)
* Update black version and reformat

* Pin black in extras_require
2018-05-29 18:37:00 -08:00
Tobotimus
d8c4113d24 Remove 3.5 from tox environments (#1740) 2018-05-30 02:18:12 +02:00
Michael H
9eb6bb7738 [V3 permissions] more docs + minor addition. (#1737)
* more info

* docstring
2018-05-28 12:02:42 -08:00
Michael H
de96f8b9f9 Fixed Readme (#1730)
* twine

* twine

* twine

* twine
2018-05-28 19:03:50 +02:00
88 changed files with 1886 additions and 1091 deletions

29
.gitignore vendored
View File

@@ -1,40 +1,15 @@
# Trivia list repo injection
redbot/trivia/
*.json *.json
*.exe *.exe
*.dll *.dll
.data .data
!/tests/cogs/dataconverter/data/**/*.json
### JetBrains template ### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff: # User-specific stuff:
.idea/**/workspace.xml .idea/
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws *.iws
## Plugin-specific files: ## Plugin-specific files:

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true} "discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = '7eb918b19e3e60b56eb9039eb267f8f3477c5e17', editable = true}
"e1839a8" = {path = ".", editable = true} "e1839a8" = {path = ".", editable = true}
[dev-packages] [dev-packages]
@@ -14,7 +14,7 @@ pytest-asyncio = "*"
sphinx = ">1.7" sphinx = ">1.7"
sphinxcontrib-asyncio = "*" sphinxcontrib-asyncio = "*"
sphinx-rtd-theme = "*" sphinx-rtd-theme = "*"
black = {version = "*", python_version = ">= '3.6'"} black = "*"
[pipenv] [pipenv]
allow_prereleases = true allow_prereleases = true

81
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "d340e4a19777736703970e45766d05d67b973db38b87382b6ef8696cb53abb60" "sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -32,6 +32,13 @@
], ],
"version": "==2.2.5" "version": "==2.2.5"
}, },
"aiohttp-json-rpc": {
"hashes": [
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
],
"version": "==0.8.7"
},
"appdirs": { "appdirs": {
"hashes": [ "hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
@@ -63,7 +70,7 @@
"discord.py": { "discord.py": {
"editable": true, "editable": true,
"git": "git://github.com/Rapptz/discord.py", "git": "git://github.com/Rapptz/discord.py",
"ref": "rewrite" "ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
}, },
"distro": { "distro": {
"hashes": [ "hashes": [
@@ -76,13 +83,6 @@
"editable": true, "editable": true,
"path": "." "path": "."
}, },
"funcsigs": {
"hashes": [
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
],
"version": "==1.0.2"
},
"fuzzywuzzy": { "fuzzywuzzy": {
"hashes": [ "hashes": [
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e", "sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
@@ -97,19 +97,6 @@
], ],
"version": "==2.6" "version": "==2.6"
}, },
"jsonrpcserver": {
"hashes": [
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
],
"version": "==3.5.4"
},
"jsonschema": {
"hashes": [
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
],
"version": "==2.6.0"
},
"multidict": { "multidict": {
"hashes": [ "hashes": [
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0", "sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
@@ -166,13 +153,6 @@
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"websockets": { "websockets": {
"hashes": [ "hashes": [
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3", "sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
@@ -248,19 +228,18 @@
}, },
"babel": { "babel": {
"hashes": [ "hashes": [
"sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14", "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80" "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
], ],
"version": "==2.5.3" "version": "==2.6.0"
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:4fec2566f9fbbd4a58de50a168cbe3ab952713530410d227e82e4c65d1fad946", "sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
"sha256:5fec0f25486046b9edb97961c946412ced96021247dd1a60ecd9f0567b68b030" "sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.6'", "version": "==18.6b2"
"version": "==18.5b0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@@ -369,11 +348,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d", "sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a" "sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.6.0" "version": "==3.6.1"
}, },
"pytest-asyncio": { "pytest-asyncio": {
"hashes": [ "hashes": [
@@ -413,19 +392,19 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b", "sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10" "sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.7.4" "version": "==1.7.5"
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33", "sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d" "sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.3.1" "version": "==0.4.0"
}, },
"sphinxcontrib-asyncio": { "sphinxcontrib-asyncio": {
"hashes": [ "hashes": [
@@ -436,10 +415,16 @@
}, },
"sphinxcontrib-websupport": { "sphinxcontrib-websupport": {
"hashes": [ "hashes": [
"sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9", "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2" "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
], ],
"version": "==1.0.1" "version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
],
"version": "==0.9.4"
}, },
"tox": { "tox": {
"hashes": [ "hashes": [

View File

@@ -1,46 +1,31 @@
.. raw:: html .. class:: center
<h1 align="center"> .. image:: https://imgur.com/pY1WUFX.png
<br> :target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red Discord Bot"></a> :alt: Red Discord Bot
<br>
Red Discord Bot
<br> .. class:: center
</h1>
Music, Moderation, Trivia, Stream Alerts and fully customizable.
.. raw:: html .. class:: center
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and fully customizable.</h4> .. 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
.. raw:: html .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: Code style: black
<p align="center"> .. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
<a href="https://discord.gg/red"> :target: https://crowdin.com/project/red-discordbot
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield"> :alt: Crowdin
</a>
<a href="https://www.patreon.com/Red_Devs">
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg">
</a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/Made%20With-Python%203.6-blue.svg?style=for-the-badge">
</a>
<a href="https://crowdin.com/project/red-discordbot">
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg">
</a>
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
<img src="https://img.shields.io/badge/discord-py-blue.svg">
</a>
</p>
.. raw:: html .. image:: https://img.shields.io/badge/Support-Red!-orange.svg
:target: https://www.patreon.com/Red_Devs
<p align="center"> :alt: Patreon
<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>
========== ==========
Overview Overview

View File

@@ -37,6 +37,7 @@ For each of those, settings have varying priorities (listed below, highest to lo
7. Role settings (see below) 7. Role settings (see below)
8. Server whitelist 8. Server whitelist
9. Server blacklist 9. Server blacklist
10. Default settings
For the role whitelist and blacklist settings, For the role whitelist and blacklist settings,
roles will be checked individually in order from highest to lowest role the user has roles will be checked individually in order from highest to lowest role the user has
@@ -73,3 +74,32 @@ An example of the expected format is shown below.
- 96733288462286848 - 96733288462286848
default: allow default: allow
----------------------
Example configurations
----------------------
Locking Audio cog to approved server(s) as a bot owner
.. code-block:: none
[p]permissions setglobaldefault Audio deny
[p]permissions addglobalrule allow Audio [server ID or name]
Locking Audio to specific voice channel(s) as a serverowner or admin:
.. code-block:: none
[p]permissions setguilddefault Audio deny
[p]permissions addguildrule allow Audio [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]

View File

@@ -13,6 +13,9 @@ RedBase
:members: :members:
:exclude-members: get_context :exclude-members: get_context
.. automethod:: register_rpc_handler
.. automethod:: unregister_rpc_handler
Red Red
^^^ ^^^

View File

@@ -4,8 +4,9 @@
Commands Package Commands Package
================ ================
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
attributes. Some of these attributes, however, have been slightly modified, as outlined below. they both have the same attributes. Some of these attributes, however, have been slightly modified,
as outlined below.
.. autofunction:: redbot.core.commands.command .. autofunction:: redbot.core.commands.command

View File

@@ -29,7 +29,9 @@ Keys specific to the cog info.json (case sensitive)
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)`` - ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
- ``hidden`` (bool) - Determines if a cog is available for install. - ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
- ``disabled`` (bool) - Determines if a cog is available for install.
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on. - ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
Downloader will not deal with this functionality but it may be useful for other cogs. Downloader will not deal with this functionality but it may be useful for other cogs.

View File

@@ -4,36 +4,60 @@
RPC RPC
=== ===
.. currentmodule:: redbot.core.rpc
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways. V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Cogs must register functions to be exposed to RPC clients. Cogs must register functions to be exposed to RPC clients.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects. Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
To begin, register all methods using individual calls to the :func:`Methods.add` method. To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
******** ********
Examples Examples
******** ********
Coming soon to a docs page near you! .. code-block:: Python
def setup(bot):
c = Cog()
bot.add_cog(c)
bot.register_rpc_handler(c.rpc_method)
*******************************
Interacting with the RPC Server
*******************************
The RPC server opens a websocket bound to port ``6133`` on ``127.0.0.1``.
This is not configurable for security reasons as broad access to this server gives anyone complete control over your bot.
To access the server you must find a library that implements websocket based JSONRPC in the language of your choice.
There are a few built-in RPC methods to note:
* ``GET_METHODS`` - Returns a list of available RPC methods.
* ``GET_METHOD_INFO`` - Will return the docstring for an available RPC method. Useful for finding information about the method's parameters and return values.
* ``GET_TOPIC`` - Returns a list of available RPC message topics.
* ``GET_SUBSCRIPTIONS`` - Returns a list of RPC subscriptions.
* ``SUBSCRIBE`` - Subscribes to an available RPC message topic.
* ``UNSUBSCRIBE`` - Unsubscribes from an RPC message topic.
All RPC methods accept a list of parameters.
The built-in methods above expect their parameters to be in list format.
All cog-based methods expect their parameter list to take one argument, a JSON object, in the following format::
params = [
{
"args": [], # A list of positional arguments
"kwargs": {}, # A dictionary of keyword arguments
}
]
# As an example, here's a call to "get_method_info"
rpc_call("GET_METHOD_INFO", ["get_methods",])
# And here's a call to "core__load"
rpc_call("CORE__LOAD", {"args": [["general", "economy", "downloader"],], "kwargs": {}})
************* *************
API Reference API Reference
************* *************
.. py:attribute:: redbot.core.rpc.methods Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
An instance of the :class:`Methods` class.
All attempts to register new RPC methods **MUST** use this object.
You should never create a new instance of the :class:`Methods` class!
RPC
^^^
.. autoclass:: redbot.core.rpc.RPC
:members:
Methods
^^^^^^^
.. autoclass:: redbot.core.rpc.Methods
:members:

View File

@@ -1,29 +1,37 @@
-i https://pypi.org/simple -i https://pypi.org/simple
alabaster==0.7.10 alabaster==0.7.10
appdirs==1.4.3
atomicwrites==1.1.5
attrs==18.1.0 attrs==18.1.0
babel==2.5.3 babel==2.6.0
black==18.6b2
certifi==2018.4.16 certifi==2018.4.16
chardet==3.0.4 chardet==3.0.4
click==6.7
docutils==0.14 docutils==0.14
idna==2.6 idna==2.6
imagesize==1.0.0 imagesize==1.0.0
jinja2==2.10 jinja2==2.10
markupsafe==1.0 markupsafe==1.0
more-itertools==4.1.0 more-itertools==4.2.0
packaging==17.1 packaging==17.1
pluggy==0.6.0 pluggy==0.6.0
py==1.5.3 py==1.5.3
pygments==2.2.0 pygments==2.2.0
pyparsing==2.2.0 pyparsing==2.2.0
pytest-asyncio==0.8.0 pytest-asyncio==0.8.0
pytest==3.5.1 pytest==3.6.1
pytz==2018.4 pytz==2018.4
requests==2.18.4 requests==2.18.4
six==1.11.0 six==1.11.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
sphinx-rtd-theme==0.3.1 sphinx-rtd-theme==0.4.0
sphinx==1.7.4 sphinx==1.7.5
sphinxcontrib-asyncio==0.2.0 sphinxcontrib-asyncio==0.2.0
sphinxcontrib-websupport==1.0.1 sphinxcontrib-websupport==1.1.0
toml==0.9.4
tox==3.0.0
urllib3==1.22 urllib3==1.22
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0 virtualenv==16.0.0
yarl==0.18.0
git+https://github.com/Rapptz/discord.py@7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0

14
generate_strings.py Normal file → Executable file
View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import subprocess import subprocess
import os import os
import sys import sys
@@ -13,25 +14,24 @@ def main():
os.chdir(os.path.join("redbot/cogs", d, "locales")) os.chdir(os.path.join("redbot/cogs", d, "locales"))
if "regen_messages.py" not in os.listdir(os.getcwd()): if "regen_messages.py" not in os.listdir(os.getcwd()):
print( print(
"Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format( f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
d
)
) )
exit(1) return 1
else: else:
print("Running 'regen_messages.py' for {}".format(d)) print("Running 'regen_messages.py' for {}".format(d))
retval = subprocess.run([interpreter, "regen_messages.py"]) retval = subprocess.run([interpreter, "regen_messages.py"])
if retval.returncode != 0: if retval.returncode != 0:
exit(1) return 1
os.chdir(root_dir) os.chdir(root_dir)
os.chdir("redbot/core/locales") os.chdir("redbot/core/locales")
print("Running 'regen_messages.py' for core") print("Running 'regen_messages.py' for core")
retval = subprocess.run([interpreter, "regen_messages.py"]) retval = subprocess.run([interpreter, "regen_messages.py"])
if retval.returncode != 0: if retval.returncode != 0:
exit(1) return 1
os.chdir(root_dir) os.chdir(root_dir)
subprocess.run(["crowdin", "upload"]) subprocess.run(["crowdin", "upload"])
return 0
if __name__ == "__main__": if __name__ == "__main__":
main() sys.exit(main())

View File

@@ -13,7 +13,7 @@ from redbot.core.events import init_events
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
from redbot.core.core_commands import Core from redbot.core.core_commands import Core
from redbot.core.dev_commands import Dev from redbot.core.dev_commands import Dev
from redbot.core import rpc, __version__ from redbot.core import __version__
import asyncio import asyncio
import logging.handlers import logging.handlers
import logging import logging
@@ -40,7 +40,7 @@ def init_loggers(cli_flags):
logger = logging.getLogger("red") logger = logging.getLogger("red")
red_format = logging.Formatter( red_format = logging.Formatter(
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: " "%(message)s", "%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
datefmt="[%d/%m/%Y %H:%M]", datefmt="[%d/%m/%Y %H:%M]",
) )
@@ -111,7 +111,7 @@ def main():
sys.exit(1) sys.exit(1)
load_basic_configuration(cli_flags.instance_name) load_basic_configuration(cli_flags.instance_name)
log, sentry_log = init_loggers(cli_flags) log, sentry_log = init_loggers(cli_flags)
red = Red(cli_flags, description=description, pm_help=None) red = Red(cli_flags=cli_flags, description=description, pm_help=None)
init_global_checks(red) init_global_checks(red)
init_events(red, cli_flags) init_events(red, cli_flags)
red.add_cog(Core(red)) red.add_cog(Core(red))
@@ -123,7 +123,7 @@ def main():
loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
token = os.environ.get("RED_TOKEN", tmp_data["token"]) token = os.environ.get("RED_TOKEN", tmp_data["token"])
prefix = cli_flags.prefix or tmp_data["prefix"] prefix = cli_flags.prefix or tmp_data["prefix"]
if token is None or not prefix: if not (token and prefix):
if cli_flags.no_prompt is False: if cli_flags.no_prompt is False:
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix)) new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
if new_token: if new_token:
@@ -138,18 +138,16 @@ def main():
sys.exit(0) sys.exit(0)
if tmp_data["enable_sentry"]: if tmp_data["enable_sentry"]:
red.enable_sentry() red.enable_sentry()
cleanup_tasks = True
try: try:
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot)) loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
except discord.LoginFailure: except discord.LoginFailure:
cleanup_tasks = False # No login happened, no need for this
log.critical( log.critical(
"This token doesn't seem to be valid. If it belongs to " "This token doesn't seem to be valid. If it belongs to "
"a user account, remember that the --not-bot flag " "a user account, remember that the --not-bot flag "
"must be used. For self-bot functionalities instead, " "must be used. For self-bot functionalities instead, "
"--self-bot" "--self-bot"
) )
db_token = red.db.token() db_token = loop.run_until_complete(red.db.token())
if db_token and not cli_flags.no_prompt: if db_token and not cli_flags.no_prompt:
print("\nDo you want to reset the token? (y/n)") print("\nDo you want to reset the token? (y/n)")
if confirm("> "): if confirm("> "):
@@ -164,10 +162,13 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e) sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout()) loop.run_until_complete(red.logout())
finally: finally:
if cleanup_tasks: pending = asyncio.Task.all_tasks(loop=red.loop)
pending = asyncio.Task.all_tasks(loop=red.loop) gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True) gathered.cancel()
gathered.cancel() try:
red.rpc.server.close()
except AttributeError:
pass
sys.exit(red._shutdown_mode.value) sys.exit(red._shutdown_mode.value)

View File

@@ -1,9 +1,8 @@
from typing import Tuple from typing import Tuple
import discord import discord
from discord.ext import commands
from redbot.core import Config, checks from redbot.core import Config, checks, commands
import logging import logging
@@ -40,7 +39,6 @@ RUNNING_ANNOUNCEMENT = (
class Admin: class Admin:
def __init__(self, config=Config): def __init__(self, config=Config):
self.conf = config.get_conf(self, 8237492837454039, force_registration=True) self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
@@ -158,13 +156,12 @@ class Admin:
else: else:
await self.complain(ctx, USER_HIERARCHY_ISSUE) await self.complain(ctx, USER_HIERARCHY_ISSUE)
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
async def editrole(self, ctx: commands.Context): async def editrole(self, ctx: commands.Context):
"""Edits roles settings""" """Edits roles settings"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@editrole.command(name="colour", aliases=["color"]) @editrole.command(name="colour", aliases=["color"])
async def editrole_colour( async def editrole_colour(
@@ -265,20 +262,16 @@ class Admin:
@announce.command(name="ignore") @announce.command(name="ignore")
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def announce_ignore(self, ctx, *, guild: discord.Guild = None): async def announce_ignore(self, ctx):
""" """
Toggles whether the announcements will ignore the given server. Toggles whether the announcements will ignore the current server.
Defaults to the current server if none is provided.
""" """
if guild is None: ignored = await self.conf.guild(ctx.guild).announce_ignore()
guild = ctx.guild await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
ignored = await self.conf.guild(guild).announce_ignore()
await self.conf.guild(guild).announce_ignore.set(not ignored)
verb = "will" if ignored else "will not" verb = "will" if ignored else "will not"
await ctx.send("The server {} {} receive announcements.".format(guild.name, verb)) await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]: async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
""" """
@@ -303,6 +296,8 @@ class Admin:
""" """
Add a role to yourself that server admins have configured as Add a role to yourself that server admins have configured as
user settable. user settable.
NOTE: The role is case sensitive!
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._addrole(ctx, ctx.author, selfrole) await self._addrole(ctx, ctx.author, selfrole)
@@ -311,6 +306,8 @@ class Admin:
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole): async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
""" """
Removes a selfrole from yourself. Removes a selfrole from yourself.
NOTE: The role is case sensitive!
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._removerole(ctx, ctx.author, selfrole) await self._removerole(ctx, ctx.author, selfrole)
@@ -320,6 +317,8 @@ class Admin:
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role): async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
""" """
Add a role to the list of available selfroles. Add a role to the list of available selfroles.
NOTE: The role is case sensitive!
""" """
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles: async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
if role.id not in curr_selfroles: if role.id not in curr_selfroles:
@@ -332,6 +331,8 @@ class Admin:
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole): async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
""" """
Removes a role from the list of available selfroles. Removes a role from the list of available selfroles.
NOTE: The role is case sensitive!
""" """
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles: async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
curr_selfroles.remove(role.id) curr_selfroles.remove(role.id)

View File

@@ -1,11 +1,10 @@
import asyncio import asyncio
import discord import discord
from discord.ext import commands from redbot.core import commands
class Announcer: class Announcer:
def __init__(self, ctx: commands.Context, message: str, config=None): def __init__(self, ctx: commands.Context, message: str, config=None):
""" """
:param ctx: :param ctx:

View File

@@ -1,9 +1,8 @@
import discord import discord
from discord.ext import commands from redbot.core import commands
class MemberDefaultAuthor(commands.Converter): class MemberDefaultAuthor(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member: async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
member_converter = commands.MemberConverter() member_converter = commands.MemberConverter()
try: try:
@@ -17,7 +16,6 @@ class MemberDefaultAuthor(commands.Converter):
class SelfRole(commands.Converter): class SelfRole(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role: async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
admin = ctx.command.instance admin = ctx.command.instance
if admin is None: if admin is None:
@@ -30,5 +28,5 @@ class SelfRole(commands.Converter):
role = await role_converter.convert(ctx, arg) role = await role_converter.convert(ctx, arg)
if role.id not in selfroles: if role.id not in selfroles:
raise commands.BadArgument("The provided role is not a valid" " selfrole.") raise commands.BadArgument("The provided role is not a valid selfrole.")
return role return role

View File

@@ -1,6 +1,6 @@
from .alias import Alias from .alias import Alias
from discord.ext import commands from redbot.core.bot import Red
def setup(bot: commands.Bot): def setup(bot: Red):
bot.add_cog(Alias(bot)) bot.add_cog(Alias(bot))

View File

@@ -170,20 +170,18 @@ class Alias:
new_message.content = "{}{} {}".format(prefix, alias.command, args) new_message.content = "{}{} {}".format(prefix, alias.command, args)
await self.bot.process_commands(new_message) await self.bot.process_commands(new_message)
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
async def alias(self, ctx: commands.Context): async def alias(self, ctx: commands.Context):
"""Manage per-server aliases for commands""" """Manage per-server aliases for commands"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@alias.group(name="global") @alias.group(name="global", autohelp=True)
async def global_(self, ctx: commands.Context): async def global_(self, ctx: commands.Context):
""" """
Manage global aliases. Manage global aliases.
""" """
if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group): pass
await ctx.send_help()
@checks.mod_or_permissions(manage_guild=True) @checks.mod_or_permissions(manage_guild=True)
@alias.command(name="add") @alias.command(name="add")
@@ -233,9 +231,7 @@ class Alias:
await self.add_alias(ctx, alias_name, command) await self.add_alias(ctx, alias_name, command)
await ctx.send( await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
_("A new alias with the trigger `{}`" " has been created.").format(alias_name)
)
@checks.is_owner() @checks.is_owner()
@global_.command(name="add") @global_.command(name="add")
@@ -282,14 +278,14 @@ class Alias:
await self.add_alias(ctx, alias_name, command, global_=True) await self.add_alias(ctx, alias_name, command, global_=True)
await ctx.send( await ctx.send(
_("A new global alias with the trigger `{}`" " has been created.").format(alias_name) _("A new global alias with the trigger `{}` has been created.").format(alias_name)
) )
@alias.command(name="help") @alias.command(name="help")
@commands.guild_only() @commands.guild_only()
async def _help_alias(self, ctx: commands.Context, alias_name: str): async def _help_alias(self, ctx: commands.Context, alias_name: str):
"""Tries to execute help for the base command of the alias""" """Tries to execute help for the base command of the alias"""
is_alias, alias = self.is_alias(ctx.guild, alias_name=alias_name) is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
if is_alias: if is_alias:
base_cmd = alias.command[0] base_cmd = alias.command[0]
@@ -307,9 +303,7 @@ class Alias:
if is_alias: if is_alias:
await ctx.send( await ctx.send(
_("The `{}` alias will execute the" " command `{}`").format( _("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
alias_name, alias.command
)
) )
else: else:
await ctx.send(_("There is no alias with the name `{}`").format(alias_name)) await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
@@ -330,7 +324,7 @@ class Alias:
if await self.delete_alias(ctx, alias_name): if await self.delete_alias(ctx, alias_name):
await ctx.send( await ctx.send(
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name) _("Alias with the name `{}` was successfully deleted.").format(alias_name)
) )
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@@ -350,7 +344,7 @@ class Alias:
if await self.delete_alias(ctx, alias_name, global_=True): if await self.delete_alias(ctx, alias_name, global_=True):
await ctx.send( await ctx.send(
_("Alias with the name `{}` was successfully" " deleted.").format(alias_name) _("Alias with the name `{}` was successfully deleted.").format(alias_name)
) )
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))

View File

@@ -5,7 +5,6 @@ from redbot.core import commands
class AliasEntry: class AliasEntry:
def __init__( def __init__(
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
): ):

View File

@@ -1,15 +1,18 @@
from pathlib import Path from pathlib import Path
from aiohttp import ClientSession from aiohttp import ClientSession
import shutil import shutil
import logging
from .audio import Audio from .audio import Audio
from .manager import start_lavalink_server from .manager import start_lavalink_server
from discord.ext import commands from redbot.core import commands
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
import redbot.core import redbot.core
log = logging.getLogger("red.audio")
LAVALINK_DOWNLOAD_URL = ( LAVALINK_DOWNLOAD_URL = (
"https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar" "https://github.com/Cog-Creators/Red-DiscordBot/releases/download/{}/Lavalink.jar"
).format(redbot.core.__version__) ).format(redbot.core.__version__)
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio") LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
@@ -33,15 +36,13 @@ async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists() jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo(*await cog.config.current_build()) current_build = redbot.core.VersionInfo(*await cog.config.current_build())
session = ClientSession(loop=loop)
if not jar_exists or current_build < redbot.core.version_info: if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
await download_lavalink(session) async with ClientSession(loop=loop) as session:
await download_lavalink(session)
await cog.config.current_build.set(redbot.core.version_info.to_json()) await cog.config.current_build.set(redbot.core.version_info.to_json())
session.close()
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE)) shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
@@ -52,4 +53,5 @@ async def setup(bot: commands.Bot):
await start_lavalink_server(bot.loop) await start_lavalink_server(bot.loop)
bot.add_cog(cog) bot.add_cog(cog)
bot.loop.create_task(cog.disconnect_timer())
bot.loop.create_task(cog.init_config()) bot.loop.create_task(cog.init_config())

View File

@@ -6,6 +6,7 @@ import heapq
import lavalink import lavalink
import math import math
import re import re
import time
import redbot.core import redbot.core
from redbot.core import Config, commands, checks, bank from redbot.core import Config, commands, checks, bank
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
@@ -14,13 +15,12 @@ from .manager import shutdown_lavalink_server
_ = Translator("Audio", __file__) _ = Translator("Audio", __file__)
__version__ = "0.0.6a" __version__ = "0.0.6b"
__author__ = ["aikaterna", "billy/bollo/ati"] __author__ = ["aikaterna", "billy/bollo/ati"]
@cog_i18n(_) @cog_i18n(_)
class Audio: class Audio:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, 2711759130, force_registration=True) self.config = Config.get_conf(self, 2711759130, force_registration=True)
@@ -38,6 +38,8 @@ class Audio:
default_guild = { default_guild = {
"dj_enabled": False, "dj_enabled": False,
"dj_role": None, "dj_role": None,
"emptydc_enabled": False,
"emptydc_timer": 0,
"jukebox": False, "jukebox": False,
"jukebox_price": 0, "jukebox_price": 0,
"playlists": {}, "playlists": {},
@@ -164,12 +166,11 @@ class Audio:
await message_channel.send(embed=embed) await message_channel.send(embed=embed)
await player.skip() await player.skip()
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
async def audioset(self, ctx): async def audioset(self, ctx):
"""Music configuration options.""" """Music configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@audioset.command() @audioset.command()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
@@ -197,6 +198,26 @@ class Audio:
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled)) await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled))
@audioset.command()
@checks.mod_or_permissions(administrator=True)
async def emptydisconnect(self, ctx, seconds: int):
"""Auto-disconnection after x seconds while stopped. 0 to disable."""
if seconds < 0:
return await self._embed_msg(ctx, "Can't be less than zero.")
if seconds < 10 and seconds > 0:
seconds = 10
if seconds == 0:
enabled = False
await self._embed_msg(ctx, "Empty disconnect disabled.")
else:
enabled = True
await self._embed_msg(
ctx, "Empty disconnect timer set to {}.".format(self._dynamic_time(seconds))
)
await self.config.guild(ctx.guild).emptydc_timer.set(seconds)
await self.config.guild(ctx.guild).emptydc_enabled.set(enabled)
@audioset.command() @audioset.command()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
async def role(self, ctx, role_name: discord.Role): async def role(self, ctx, role_name: discord.Role):
@@ -242,12 +263,16 @@ class Audio:
global_data = await self.config.all() global_data = await self.config.all()
dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"]) dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"])
dj_enabled = data["dj_enabled"] dj_enabled = data["dj_enabled"]
emptydc_enabled = data["emptydc_enabled"]
emptydc_timer = data["emptydc_timer"]
jukebox = data["jukebox"] jukebox = data["jukebox"]
jukebox_price = data["jukebox_price"] jukebox_price = data["jukebox_price"]
jarbuild = redbot.core.__version__ jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"] vote_percent = data["vote_percent"]
msg = "```ini\n" "----Server Settings----\n" msg = "```ini\n" "----Server Settings----\n"
if emptydc_enabled:
msg += "Disconnect timer: [{0}]\n".format(self._dynamic_time(emptydc_timer))
if dj_enabled: if dj_enabled:
msg += "DJ Role: [{}]\n".format(dj_role_obj.name) msg += "DJ Role: [{}]\n".format(dj_role_obj.name)
if jukebox: if jukebox:
@@ -426,7 +451,11 @@ class Audio:
await message.add_reaction(expected[i]) await message.add_reaction(expected[i])
def check(r, u): def check(r, u):
return r.message.id == message.id and u == ctx.message.author return (
r.message.id == message.id
and u == ctx.message.author
and any(e in str(r.emoji) for e in expected)
)
try: try:
(r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0) (r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0)
@@ -590,7 +619,7 @@ class Audio:
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
before_queue_length = len(player.queue) + 1 before_queue_length = len(player.queue)
if "list" in query and "ytsearch:" not in query: if "list" in query and "ytsearch:" not in query:
for track in tracks: for track in tracks:
@@ -603,7 +632,7 @@ class Audio:
if not shuffle and queue_duration > 0: if not shuffle and queue_duration > 0:
embed.set_footer( embed.set_footer(
text="{} until start of playlist playback: starts at #{} in queue".format( text="{} until start of playlist playback: starts at #{} in queue".format(
queue_total_duration, before_queue_length queue_total_duration, before_queue_length + 1
) )
) )
if not player.current: if not player.current:
@@ -619,21 +648,20 @@ class Audio:
if not shuffle and queue_duration > 0: if not shuffle and queue_duration > 0:
embed.set_footer( embed.set_footer(
text="{} until track playback: #{} in queue".format( text="{} until track playback: #{} in queue".format(
queue_total_duration, before_queue_length queue_total_duration, before_queue_length + 1
) )
) )
elif queue_duration > 0: elif queue_duration > 0:
embed.set_footer(text="#{} in queue".format(len(player.queue) + 1)) embed.set_footer(text="#{} in queue".format(len(player.queue)))
if not player.current: if not player.current:
await player.play() await player.play()
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
async def playlist(self, ctx): async def playlist(self, ctx):
"""Playlist configuration options.""" """Playlist configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@playlist.command(name="append") @playlist.command(name="append")
async def _playlist_append(self, ctx, playlist_name, *url): async def _playlist_append(self, ctx, playlist_name, *url):
@@ -1195,10 +1223,10 @@ class Audio:
query = query.strip("<>") query = query.strip("<>")
if query.startswith("list "): if query.startswith("list "):
query = "ytsearch:{}".format(query.lstrip("list ")) query = "ytsearch:{}".format(query.replace("list ", ""))
tracks = await player.get_tracks(query) tracks = await player.get_tracks(query)
if not tracks: if not tracks:
return await self._embed_msg(ctx, "Nothing found 👀") return await self._embed_msg(ctx, "Nothing found.")
songembed = discord.Embed( songembed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=ctx.guild.me.top_role.colour,
title="Queued {} track(s).".format(len(tracks)), title="Queued {} track(s).".format(len(tracks)),
@@ -1217,12 +1245,12 @@ class Audio:
await player.play() await player.play()
return await ctx.send(embed=songembed) return await ctx.send(embed=songembed)
if query.startswith("sc "): if query.startswith("sc "):
query = "scsearch:{}".format(query.lstrip("sc ")) query = "scsearch:{}".format(query.replace("sc ", ""))
elif not query.startswith("http"): elif not query.startswith("http"):
query = "ytsearch:{}".format(query) query = "ytsearch:{}".format(query)
tracks = await player.get_tracks(query) tracks = await player.get_tracks(query)
if not tracks: if not tracks:
return await self._embed_msg(ctx, "Nothing found 👀") return await self._embed_msg(ctx, "Nothing found.")
len_search_pages = math.ceil(len(tracks) / 5) len_search_pages = math.ceil(len(tracks) / 5)
search_page_list = [] search_page_list = []
@@ -1485,11 +1513,13 @@ class Audio:
else: else:
return False return False
@staticmethod async def _skip_action(self, ctx):
async def _skip_action(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if not player.queue: if not player.queue:
pos, dur = player.position, player.current.length try:
pos, dur = player.position, player.current.length
except AttributeError:
return await self._embed_msg(ctx, "There's nothing in the queue.")
time_remain = lavalink.utils.format_time(dur - pos) time_remain = lavalink.utils.format_time(dur - pos)
if player.current.is_stream: if player.current.is_stream:
embed = discord.Embed( embed = discord.Embed(
@@ -1588,13 +1618,12 @@ class Audio:
embed.set_footer(text="Nothing playing.") embed.set_footer(text="Nothing playing.")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.group(aliases=["llset"]) @commands.group(aliases=["llset"], autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.is_owner() @checks.is_owner()
async def llsetup(self, ctx): async def llsetup(self, ctx):
"""Lavalink server configuration options.""" """Lavalink server configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@llsetup.command() @llsetup.command()
async def external(self, ctx): async def external(self, ctx):
@@ -1711,6 +1740,34 @@ class Audio:
if player.volume != volume: if player.volume != volume:
await player.set_volume(volume) await player.set_volume(volume)
async def disconnect_timer(self):
stop_times = {}
while self == self.bot.get_cog("Audio"):
for p in lavalink.players:
server = p.channel.guild
if server.id not in stop_times:
stop_times[server.id] = None
if p.current is None and [self.bot.user] == p.channel.members:
if stop_times[server.id] is None:
stop_times[server.id] = int(time.time())
for sid in stop_times:
server_obj = self.bot.get_guild(sid)
emptydc_enabled = await self.config.guild(server_obj).emptydc_enabled()
if emptydc_enabled:
if stop_times[sid] is not None and [self.bot.user] == p.channel.members:
emptydc_timer = await self.config.guild(server_obj).emptydc_timer()
if stop_times[sid] and (
int(time.time()) - stop_times[sid] > emptydc_timer
):
stop_times[sid] = None
await lavalink.get_player(sid).disconnect()
await asyncio.sleep(5)
@staticmethod @staticmethod
async def _draw_time(ctx): async def _draw_time(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)

View File

@@ -4,6 +4,9 @@ import asyncio
from subprocess import Popen, DEVNULL, PIPE from subprocess import Popen, DEVNULL, PIPE
import os import os
import logging import logging
from typing import Optional, Tuple
_JavaVersion = Tuple[int, int]
log = logging.getLogger("red.audio.manager") log = logging.getLogger("red.audio.manager")
@@ -36,16 +39,16 @@ async def monitor_lavalink_server(loop):
) )
async def has_java(loop): async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
java_available = shutil.which("java") is not None java_available = shutil.which("java") is not None
if not java_available: if not java_available:
return False return False, None
version = await get_java_version(loop) version = await get_java_version(loop)
return version >= (1, 8), version return version >= (1, 8), version
async def get_java_version(loop): async def get_java_version(loop) -> _JavaVersion:
""" """
This assumes we've already checked that java exists. This assumes we've already checked that java exists.
""" """

View File

@@ -58,7 +58,7 @@ class Bank:
# SECTION commands # SECTION commands
@commands.group() @commands.group(autohelp=True)
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def bankset(self, ctx: commands.Context): async def bankset(self, ctx: commands.Context):
"""Base command for bank settings""" """Base command for bank settings"""
@@ -69,17 +69,15 @@ class Bank:
default_balance = await bank._conf.default_balance() default_balance = await bank._conf.default_balance()
else: else:
if not ctx.guild: if not ctx.guild:
await ctx.send_help()
return return
bank_name = await bank._conf.guild(ctx.guild).bank_name() bank_name = await bank._conf.guild(ctx.guild).bank_name()
currency_name = await bank._conf.guild(ctx.guild).currency() currency_name = await bank._conf.guild(ctx.guild).currency()
default_balance = await bank._conf.guild(ctx.guild).default_balance() default_balance = await bank._conf.guild(ctx.guild).default_balance()
settings = _( settings = _(
"Bank settings:\n\n" "Bank name: {}\n" "Currency: {}\n" "Default balance: {}" "" "Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
).format(bank_name, currency_name, default_balance) ).format(bank_name, currency_name, default_balance)
await ctx.send(box(settings)) await ctx.send(box(settings))
await ctx.send_help()
@bankset.command(name="toggleglobal") @bankset.command(name="toggleglobal")
@checks.is_owner() @checks.is_owner()

View File

@@ -92,12 +92,11 @@ class Cleanup:
before = message before = message
return to_delete return to_delete
@commands.group() @commands.group(autohelp=True)
@checks.mod_or_permissions(manage_messages=True) @checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx: commands.Context): async def cleanup(self, ctx: commands.Context):
"""Deletes messages.""" """Deletes messages."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@@ -139,7 +138,7 @@ class Cleanup:
delete_pinned=delete_pinned, delete_pinned=delete_pinned,
) )
reason = "{}({}) deleted {} messages " " containing '{}' in channel {}.".format( reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
author.name, author.id, len(to_delete), text, channel.id author.name, author.id, len(to_delete), text, channel.id
) )
log.info(reason) log.info(reason)
@@ -229,7 +228,7 @@ class Cleanup:
is_bot = self.bot.user.bot is_bot = self.bot.user.bot
if not is_bot: if not is_bot:
await ctx.send(_("This command can only be used on bots with " "bot accounts.")) await ctx.send(_("This command can only be used on bots with bot accounts."))
return return
after = await channel.get_message(message_id) after = await channel.get_message(message_id)
@@ -242,7 +241,7 @@ class Cleanup:
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
) )
reason = "{}({}) deleted {} messages in channel {}." "".format( reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name author.name, author.id, len(to_delete), channel.name
) )
log.info(reason) log.info(reason)
@@ -273,7 +272,7 @@ class Cleanup:
) )
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}." "".format( reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, number, channel.name author.name, author.id, number, channel.name
) )
log.info(reason) log.info(reason)

View File

@@ -25,7 +25,6 @@ class AlreadyExists(CCError):
class CommandObj: class CommandObj:
def __init__(self, **kwargs): def __init__(self, **kwargs):
config = kwargs.get("config") config = kwargs.get("config")
self.bot = kwargs.get("bot") self.bot = kwargs.get("bot")
@@ -43,7 +42,7 @@ class CommandObj:
intro = _( intro = _(
"Welcome to the interactive random {} maker!\n" "Welcome to the interactive random {} maker!\n"
"Every message you send will be added as one of the random " "Every message you send will be added as one of the random "
"response to choose from once this {} is " "responses to choose from once this {} is "
"triggered. To exit this interactive menu, type `{}`" "triggered. To exit this interactive menu, type `{}`"
).format("customcommand", "customcommand", "exit()") ).format("customcommand", "customcommand", "exit()")
await ctx.send(intro) await ctx.send(intro)
@@ -75,7 +74,7 @@ class CommandObj:
return ccinfo["response"] return ccinfo["response"]
async def create(self, ctx: commands.Context, command: str, response): async def create(self, ctx: commands.Context, command: str, response):
"""Create a customcommand""" """Create a custom command"""
# Check if this command is already registered as a customcommand # Check if this command is already registered as a customcommand
if await self.db(ctx.guild).commands.get_raw(command, default=None): if await self.db(ctx.guild).commands.get_raw(command, default=None):
raise AlreadyExists() raise AlreadyExists()
@@ -132,6 +131,7 @@ class CommandObj:
@cog_i18n(_) @cog_i18n(_)
class CustomCommands: class CustomCommands:
"""Custom commands """Custom commands
Creates commands used to display text""" Creates commands used to display text"""
def __init__(self, bot): def __init__(self, bot):
@@ -141,14 +141,13 @@ class CustomCommands:
self.config.register_guild(commands={}) self.config.register_guild(commands={})
self.commandobj = CommandObj(config=self.config, bot=self.bot) self.commandobj = CommandObj(config=self.config, bot=self.bot)
@commands.group(aliases=["cc"], no_pm=True) @commands.group(aliases=["cc"], autohelp=True)
@commands.guild_only() @commands.guild_only()
async def customcom(self, ctx: commands.Context): async def customcom(self, ctx: commands.Context):
"""Custom commands management""" """Custom commands management"""
if not ctx.invoked_subcommand: pass
await ctx.send_help()
@customcom.group(name="add") @customcom.group(name="add", autohelp=True)
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add(self, ctx: commands.Context): async def cc_add(self, ctx: commands.Context):
""" """
@@ -166,14 +165,14 @@ class CustomCommands:
{server} message.guild {server} message.guild
""" """
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group): pass
await ctx.send_help()
@cc_add.command(name="random") @cc_add.command(name="random")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add_random(self, ctx: commands.Context, command: str): async def cc_add_random(self, ctx: commands.Context, command: str):
""" """
Create a CC where it will randomly choose a response! Create a CC where it will randomly choose a response!
Note: This is interactive Note: This is interactive
""" """
channel = ctx.channel channel = ctx.channel
@@ -185,7 +184,7 @@ class CustomCommands:
await ctx.send(_("Custom command successfully added.")) await ctx.send(_("Custom command successfully added."))
except AlreadyExists: except AlreadyExists:
await ctx.send( await ctx.send(
_("This command already exists. Use " "`{}` to edit it.").format( _("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
) )
) )
@@ -196,6 +195,7 @@ class CustomCommands:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add_simple(self, ctx, command: str, *, text): async def cc_add_simple(self, ctx, command: str, *, text):
"""Adds a simple custom command """Adds a simple custom command
Example: Example:
[p]customcom add simple yourcommand Text you want [p]customcom add simple yourcommand Text you want
""" """
@@ -209,7 +209,7 @@ class CustomCommands:
await ctx.send(_("Custom command successfully added.")) await ctx.send(_("Custom command successfully added."))
except AlreadyExists: except AlreadyExists:
await ctx.send( await ctx.send(
_("This command already exists. Use " "`{}` to edit it.").format( _("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
) )
) )
@@ -218,6 +218,7 @@ class CustomCommands:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_edit(self, ctx, command: str, *, text=None): async def cc_edit(self, ctx, command: str, *, text=None):
"""Edits a custom command """Edits a custom command
Example: Example:
[p]customcom edit yourcommand Text you want [p]customcom edit yourcommand Text you want
""" """
@@ -229,7 +230,7 @@ class CustomCommands:
await ctx.send(_("Custom command successfully edited.")) await ctx.send(_("Custom command successfully edited."))
except NotFound: except NotFound:
await ctx.send( await ctx.send(
_("That command doesn't exist. Use " "`{}` to add it.").format( _("That command doesn't exist. Use `{}` to add it.").format(
"{}customcom add".format(ctx.prefix) "{}customcom add".format(ctx.prefix)
) )
) )

View File

@@ -119,11 +119,10 @@ class SpecResolver(object):
def past_nicknames_conv_spec(self, data: dict): def past_nicknames_conv_spec(self, data: dict):
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data)) flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
ret = {} ret = {}
for k, v in flatscoped.items(): for config_identifiers, v2data in flatscoped.items():
outerkey, innerkey = (*k[:-1],), (k[-1],) if config_identifiers not in ret:
if outerkey not in ret: ret[config_identifiers] = {}
ret[outerkey] = {} ret[config_identifiers].update({("past_nicks",): v2data})
ret[outerkey].update({innerkey: v})
return ret return ret
def customcom_conv_spec(self, data: dict): def customcom_conv_spec(self, data: dict):
@@ -144,18 +143,28 @@ class SpecResolver(object):
ret[outerkey].update({innerkey: ccinfo}) ret[outerkey].update({innerkey: ccinfo})
return ret return ret
async def convert(self, bot: Red, prettyname: str): def get_config_object(self, bot, cogname, attr, _id):
if prettyname not in self.available:
raise NotImplementedError("No Conversion Specs for this")
info = self.available_core_conversions[prettyname]
filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info["cfg"]
try: try:
config = getattr(bot.get_cog(cogname), attr) config = getattr(bot.get_cog(cogname), attr)
except (TypeError, AttributeError): except (TypeError, AttributeError):
config = Config.get_conf(None, _id, cog_name=cogname) config = Config.get_conf(None, _id, cog_name=cogname)
return config
def get_conversion_info(self, prettyname: str):
info = self.available_core_conversions[prettyname]
filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info["cfg"]
return filepath, converter, cogname, attr, _id
async def convert(self, bot: Red, prettyname: str, config=None):
if prettyname not in self.available:
raise NotImplementedError("No Conversion Specs for this")
filepath, converter, cogname, attr, _id = self.get_conversion_info(prettyname)
if config is None:
config = self.get_config_object(bot, cogname, attr, _id)
try: try:
items = converter(dc.json_load(filepath)) items = converter(dc.json_load(filepath))
await dc(config).dict_import(items) await dc(config).dict_import(items)

View File

@@ -41,7 +41,7 @@ class DataConverter:
) )
) )
while resolver.available: while resolver.available:
menu = _("Please select a set of data to import by number" ", or 'exit' to exit") menu = _("Please select a set of data to import by number, or 'exit' to exit")
for index, entry in enumerate(resolver.available, 1): for index, entry in enumerate(resolver.available, 1):
menu += "\n{}. {}".format(index, entry) menu += "\n{}. {}".format(index, entry)

View File

@@ -1,9 +1,9 @@
import asyncio import asyncio
import discord import discord
from discord.ext import commands from redbot.core import commands
__all__ = ["install_agreement"] __all__ = ["do_install_agreement"]
REPO_INSTALL_MSG = ( REPO_INSTALL_MSG = (
"You're about to add a 3rd party repository. The creator of Red" "You're about to add a 3rd party repository. The creator of Red"
@@ -16,33 +16,21 @@ REPO_INSTALL_MSG = (
) )
def install_agreement(): async def do_install_agreement(ctx: commands.Context):
downloader = ctx.cog
async def pred(ctx: commands.Context): if downloader is None or downloader.already_agreed:
downloader = ctx.command.instance
if downloader is None:
return True
elif downloader.already_agreed:
return True
elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
return True
def does_agree(msg: discord.Message):
return (
ctx.author == msg.author
and ctx.channel == msg.channel
and msg.content == "I agree"
)
await ctx.send(REPO_INSTALL_MSG)
try:
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
return False
downloader.already_agreed = True
return True return True
return commands.check(pred) def does_agree(msg: discord.Message):
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG)
try:
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
return False
downloader.already_agreed = True
return True

View File

@@ -1,11 +1,9 @@
import discord import discord
from discord.ext import commands from redbot.core import commands
from .repo_manager import RepoManager
from .installable import Installable from .installable import Installable
class InstalledCog(commands.Converter): class InstalledCog(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Installable: async def convert(self, ctx: commands.Context, arg: str) -> Installable:
downloader = ctx.bot.get_cog("Downloader") downloader = ctx.bot.get_cog("Downloader")
if downloader is None: if downloader is None:

View File

@@ -15,7 +15,7 @@ from redbot.core.utils.chat_formatting import box, pagify
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from .checks import install_agreement from .checks import do_install_agreement
from .converters import InstalledCog from .converters import InstalledCog
from .errors import CloningError, ExistingGitRepo from .errors import CloningError, ExistingGitRepo
from .installable import Installable from .installable import Installable
@@ -27,7 +27,6 @@ _ = Translator("Downloader", __file__)
@cog_i18n(_) @cog_i18n(_)
class Downloader: class Downloader:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@@ -54,7 +53,7 @@ class Downloader:
async def cog_install_path(self): async def cog_install_path(self):
"""Get the current cog install path. """Get the current cog install path.
Returns Returns
------- -------
pathlib.Path pathlib.Path
@@ -65,7 +64,7 @@ class Downloader:
async def installed_cogs(self) -> Tuple[Installable]: async def installed_cogs(self) -> Tuple[Installable]:
"""Get info on installed cogs. """Get info on installed cogs.
Returns Returns
------- -------
`tuple` of `Installable` `tuple` of `Installable`
@@ -78,7 +77,7 @@ class Downloader:
async def _add_to_installed(self, cog: Installable): async def _add_to_installed(self, cog: Installable):
"""Mark a cog as installed. """Mark a cog as installed.
Parameters Parameters
---------- ----------
cog : Installable cog : Installable
@@ -94,7 +93,7 @@ class Downloader:
async def _remove_from_installed(self, cog: Installable): async def _remove_from_installed(self, cog: Installable):
"""Remove a cog from the saved list of installed cogs. """Remove a cog from the saved list of installed cogs.
Parameters Parameters
---------- ----------
cog : Installable cog : Installable
@@ -205,17 +204,15 @@ class Downloader:
) )
) )
@commands.group() @commands.group(autohelp=True)
@checks.is_owner() @checks.is_owner()
async def repo(self, ctx): async def repo(self, ctx):
""" """
Command group for managing Downloader repos. Command group for managing Downloader repos.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@repo.command(name="add") @repo.command(name="add")
@install_agreement()
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None): async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
""" """
Add a new repo to Downloader. Add a new repo to Downloader.
@@ -223,6 +220,9 @@ class Downloader:
Name can only contain characters A-z, numbers and underscore Name can only contain characters A-z, numbers and underscore
Branch will default to master if not specified Branch will default to master if not specified
""" """
agreed = await do_install_agreement(ctx)
if not agreed:
return
try: try:
# noinspection PyTypeChecker # noinspection PyTypeChecker
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch) repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
@@ -272,14 +272,13 @@ class Downloader:
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "") msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
await ctx.send(box(msg)) await ctx.send(box(msg))
@commands.group() @commands.group(autohelp=True)
@checks.is_owner() @checks.is_owner()
async def cog(self, ctx): async def cog(self, ctx):
""" """
Command group for managing installable Cogs. Command group for managing installable Cogs.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@cog.command(name="install") @cog.command(name="install")
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str): async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
@@ -289,7 +288,7 @@ class Downloader:
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
if cog is None: if cog is None:
await ctx.send( await ctx.send(
_("Error, there is no cog by the name of" " `{}` in the `{}` repo.").format( _("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
cog_name, repo_name.name cog_name, repo_name.name
) )
) )
@@ -306,7 +305,7 @@ class Downloader:
if not await repo_name.install_requirements(cog, self.LIB_PATH): if not await repo_name.install_requirements(cog, self.LIB_PATH):
await ctx.send( await ctx.send(
_("Failed to install the required libraries for" " `{}`: `{}`").format( _("Failed to install the required libraries for `{}`: `{}`").format(
cog.name, cog.requirements cog.name, cog.requirements
) )
) )
@@ -381,11 +380,25 @@ class Downloader:
""" """
Lists all available cogs from a single repo. Lists all available cogs from a single repo.
""" """
installed = await self.installed_cogs()
installed_str = ""
if installed:
installed_str = _("Installed Cogs:\n") + "\n".join(
[
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
for i in installed
if i.repo_name == repo_name.name
]
)
cogs = repo_name.available_cogs cogs = repo_name.available_cogs
cogs = _("Available Cogs:\n") + "\n".join( cogs = _("Available Cogs:\n") + "\n".join(
["+ {}: {}".format(c.name, c.short or "") for c in cogs] [
"+ {}: {}".format(c.name, c.short or "")
for c in cogs
if not (c.hidden or c in installed)
]
) )
cogs = cogs + "\n\n" + installed_str
for page in pagify(cogs, ["\n"], shorten_by=16): for page in pagify(cogs, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff")) await ctx.send(box(page.lstrip(" "), lang="diff"))
@@ -401,7 +414,9 @@ class Downloader:
) )
return return
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "") msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
)
await ctx.send(box(msg)) await ctx.send(box(msg))
async def is_installed( async def is_installed(
@@ -437,7 +452,7 @@ class Downloader:
Name of the command which belongs to the cog. Name of the command which belongs to the cog.
cog_installable : `Installable` or `object` cog_installable : `Installable` or `object`
Can be an `Installable` instance or a Cog instance. Can be an `Installable` instance or a Cog instance.
Returns Returns
------- -------
str str
@@ -462,12 +477,12 @@ class Downloader:
"""Determines the cog name that Downloader knows from the cog instance. """Determines the cog name that Downloader knows from the cog instance.
Probably. Probably.
Parameters Parameters
---------- ----------
instance : object instance : object
The cog instance. The cog instance.
Returns Returns
------- -------
str str

View File

@@ -17,6 +17,7 @@ class DownloaderException(Exception):
""" """
Base class for Downloader exceptions. Base class for Downloader exceptions.
""" """
pass pass
@@ -31,6 +32,7 @@ class InvalidRepoName(DownloaderException):
Throw when a repo name is invalid. Check Throw when a repo name is invalid. Check
the message for a more detailed reason. the message for a more detailed reason.
""" """
pass pass
@@ -39,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
Thrown when trying to clone into a folder where a Thrown when trying to clone into a folder where a
git repo already exists. git repo already exists.
""" """
pass pass
@@ -47,6 +50,7 @@ class MissingGitRepo(DownloaderException):
Thrown when a git repo is expected to exist but Thrown when a git repo is expected to exist but
does not. does not.
""" """
pass pass
@@ -54,6 +58,7 @@ class CloningError(GitException):
""" """
Thrown when git clone returns a non zero exit code. Thrown when git clone returns a non zero exit code.
""" """
pass pass
@@ -62,6 +67,7 @@ class CurrentHashError(GitException):
Thrown when git returns a non zero exit code attempting Thrown when git returns a non zero exit code attempting
to determine the current commit hash. to determine the current commit hash.
""" """
pass pass
@@ -70,6 +76,7 @@ class HardResetError(GitException):
Thrown when there is an issue trying to execute a hard reset Thrown when there is an issue trying to execute a hard reset
(usually prior to a repo update). (usually prior to a repo update).
""" """
pass pass
@@ -77,6 +84,7 @@ class UpdateError(GitException):
""" """
Thrown when git pull returns a non zero error code. Thrown when git pull returns a non zero error code.
""" """
pass pass
@@ -84,6 +92,7 @@ class GitDiffError(GitException):
""" """
Thrown when a git diff fails. Thrown when a git diff fails.
""" """
pass pass
@@ -91,4 +100,5 @@ class PipError(DownloaderException):
""" """
Thrown when pip returns a non-zero return code. Thrown when pip returns a non-zero return code.
""" """
pass pass

View File

@@ -3,9 +3,8 @@ import distutils.dir_util
import shutil import shutil
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import MutableMapping, Any from typing import MutableMapping, Any, TYPE_CHECKING
from redbot.core.utils import TYPE_CHECKING
from .log import log from .log import log
from .json_mixins import RepoJSONMixin from .json_mixins import RepoJSONMixin
@@ -25,7 +24,7 @@ class Installable(RepoJSONMixin):
- Modules - Modules
- Repo Libraries - Repo Libraries
- Other stuff? - Other stuff?
The attributes of this class will mostly come from the installation's The attributes of this class will mostly come from the installation's
info.json. info.json.
@@ -76,6 +75,7 @@ class Installable(RepoJSONMixin):
self.bot_version = (3, 0, 0) self.bot_version = (3, 0, 0)
self.min_python_version = (3, 5, 1) self.min_python_version = (3, 5, 1)
self.hidden = False self.hidden = False
self.disabled = False
self.required_cogs = {} # Cog name -> repo URL self.required_cogs = {} # Cog name -> repo URL
self.requirements = () self.requirements = ()
self.tags = () self.tags = ()
@@ -117,7 +117,7 @@ class Installable(RepoJSONMixin):
try: try:
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem)) copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
except: except:
log.exception("Error occurred when copying path:" " {}".format(self._location)) log.exception("Error occurred when copying path: {}".format(self._location))
return False return False
return True return True
@@ -146,9 +146,7 @@ class Installable(RepoJSONMixin):
info = json.load(f) info = json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
info = {} info = {}
log.exception( log.exception("Invalid JSON information file at path: {}".format(info_file_path))
"Invalid JSON information file at path:" " {}".format(info_file_path)
)
else: else:
self._info = info self._info = info
@@ -176,6 +174,12 @@ class Installable(RepoJSONMixin):
hidden = False hidden = False
self.hidden = hidden self.hidden = hidden
try:
disabled = bool(info.get("disabled", False))
except ValueError:
disabled = False
self.disabled = disabled
self.required_cogs = info.get("required_cogs", {}) self.required_cogs = info.get("required_cogs", {})
self.requirements = info.get("requirements", ()) self.requirements = info.get("requirements", ())

View File

@@ -2,17 +2,13 @@ import asyncio
import functools import functools
import os import os
import pkgutil import pkgutil
import shutil
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from subprocess import run as sp_run, PIPE from subprocess import run as sp_run, PIPE
from sys import executable from sys import executable
from typing import Tuple, MutableMapping, Union from typing import Tuple, MutableMapping, Union
from discord.ext import commands from redbot.core import data_manager, commands
from redbot.core import Config
from redbot.core import data_manager
from redbot.core.utils import safe_delete from redbot.core.utils import safe_delete
from .errors import * from .errors import *
from .installable import Installable, InstallableType from .installable import Installable, InstallableType
@@ -27,10 +23,8 @@ class Repo(RepoJSONMixin):
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}" GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q" GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
GIT_PULL = "git -C {path} pull -q --ff-only" GIT_PULL = "git -C {path} pull -q --ff-only"
GIT_DIFF_FILE_STATUS = ( GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
"git -C {path} diff --no-commit-id --name-status" " {old_hash} {new_hash}" GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
)
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.." " {relative_file_path}"
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url" GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}" PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
@@ -98,7 +92,7 @@ class Repo(RepoJSONMixin):
) )
if p.returncode != 0: if p.returncode != 0:
raise GitDiffError("Git diff failed for repo at path:" " {}".format(self.folder_path)) raise GitDiffError("Git diff failed for repo at path: {}".format(self.folder_path))
stdout = p.stdout.strip().decode().split("\n") stdout = p.stdout.strip().decode().split("\n")
@@ -222,7 +216,7 @@ class Repo(RepoJSONMixin):
if p.returncode != 0: if p.returncode != 0:
raise GitException( raise GitException(
"Could not determine current branch" " at path: {}".format(self.folder_path) "Could not determine current branch at path: {}".format(self.folder_path)
) )
return p.stdout.decode().strip() return p.stdout.decode().strip()
@@ -234,7 +228,7 @@ class Repo(RepoJSONMixin):
---------- ----------
branch : `str`, optional branch : `str`, optional
Override for repo's branch attribute. Override for repo's branch attribute.
Returns Returns
------- -------
str str
@@ -383,7 +377,7 @@ class Repo(RepoJSONMixin):
Directory to install shared libraries to. Directory to install shared libraries to.
libraries : `tuple` of `Installable` libraries : `tuple` of `Installable`
A subset of available libraries. A subset of available libraries.
Returns Returns
------- -------
bool bool
@@ -405,7 +399,7 @@ class Repo(RepoJSONMixin):
async def install_requirements(self, cog: Installable, target_dir: Path) -> bool: async def install_requirements(self, cog: Installable, target_dir: Path) -> bool:
"""Install a cog's requirements. """Install a cog's requirements.
Requirements will be installed via pip directly into Requirements will be installed via pip directly into
:code:`target_dir`. :code:`target_dir`.
@@ -467,12 +461,12 @@ class Repo(RepoJSONMixin):
@property @property
def available_cogs(self) -> Tuple[Installable]: def available_cogs(self) -> Tuple[Installable]:
"""`tuple` of `installable` : All available cogs in this Repo. """`tuple` of `installable` : All available cogs in this Repo.
This excludes hidden or shared packages. This excludes hidden or shared packages.
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
[m for m in self.available_modules if m.type == InstallableType.COG and not m.hidden] [m for m in self.available_modules if m.type == InstallableType.COG and not m.disabled]
) )
@property @property
@@ -495,7 +489,6 @@ class Repo(RepoJSONMixin):
class RepoManager: class RepoManager:
def __init__(self): def __init__(self):
self._repos = {} self._repos = {}

View File

@@ -74,7 +74,6 @@ SLOT_PAYOUTS_MSG = _(
def guild_only_check(): def guild_only_check():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
if await bank.is_global(): if await bank.is_global():
return True return True
@@ -87,7 +86,6 @@ def guild_only_check():
class SetParser: class SetParser:
def __init__(self, argument): def __init__(self, argument):
allowed = ("+", "-") allowed = ("+", "-")
self.sum = int(argument) self.sum = int(argument)
@@ -139,11 +137,10 @@ class Economy:
self.config.register_role(**self.default_role_settings) self.config.register_role(**self.default_role_settings)
self.slot_register = defaultdict(dict) self.slot_register = defaultdict(dict)
@commands.group(name="bank") @commands.group(name="bank", autohelp=True)
async def _bank(self, ctx: commands.Context): async def _bank(self, ctx: commands.Context):
"""Bank operations""" """Bank operations"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@_bank.command() @_bank.command()
async def balance(self, ctx: commands.Context, user: discord.Member = None): async def balance(self, ctx: commands.Context, user: discord.Member = None):
@@ -228,7 +225,7 @@ class Economy:
else: else:
await bank.wipe_bank() await bank.wipe_bank()
await ctx.send( await ctx.send(
_("All bank accounts for {} have been " "deleted.").format( _("All bank accounts for {} have been deleted.").format(
self.bot.user.name if await bank.is_global() else "this server" self.bot.user.name if await bank.is_global() else "this server"
) )
) )
@@ -267,7 +264,7 @@ class Economy:
else: else:
dtime = self.display_time(next_payday - cur_time) dtime = self.display_time(next_payday - cur_time)
await ctx.send( await ctx.send(
_("{} Too soon. For your next payday you have to" " wait {}.").format( _("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime author.mention, dtime
) )
) )
@@ -301,7 +298,7 @@ class Economy:
else: else:
dtime = self.display_time(next_payday - cur_time) dtime = self.display_time(next_payday - cur_time)
await ctx.send( await ctx.send(
_("{} Too soon. For your next payday you have to" " wait {}.").format( _("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime author.mention, dtime
) )
) )
@@ -427,7 +424,7 @@ class Economy:
now = then - bid + pay now = then - bid + pay
await bank.set_balance(author, now) await bank.set_balance(author, now)
await channel.send( await channel.send(
_("{}\n{} {}\n\nYour bid: {}\n{}{}!" "").format( _("{}\n{} {}\n\nYour bid: {}\n{}{}!").format(
slot, author.mention, payout["phrase"], bid, then, now slot, author.mention, payout["phrase"], bid, then, now
) )
) )
@@ -436,19 +433,18 @@ class Economy:
await bank.withdraw_credits(author, bid) await bank.withdraw_credits(author, bid)
now = then - bid now = then - bid
await channel.send( await channel.send(
_("{}\n{} Nothing!\nYour bid: {}\n{}{}!" "").format( _("{}\n{} Nothing!\nYour bid: {}\n{}{}!").format(
slot, author.mention, bid, then, now slot, author.mention, bid, then, now
) )
) )
@commands.group() @commands.group(autohelp=True)
@guild_only_check() @guild_only_check()
@check_global_setting_admin() @check_global_setting_admin()
async def economyset(self, ctx: commands.Context): async def economyset(self, ctx: commands.Context):
"""Changes economy module settings""" """Changes economy module settings"""
guild = ctx.guild guild = ctx.guild
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
if await bank.is_global(): if await bank.is_global():
slot_min = await self.config.SLOT_MIN() slot_min = await self.config.SLOT_MIN()
slot_max = await self.config.SLOT_MAX() slot_max = await self.config.SLOT_MAX()
@@ -497,7 +493,7 @@ class Economy:
"""Maximum slot machine bid""" """Maximum slot machine bid"""
slot_min = await self.config.SLOT_MIN() slot_min = await self.config.SLOT_MIN()
if bid < 1 or bid < slot_min: if bid < 1 or bid < slot_min:
await ctx.send(_("Invalid slotmax bid amount. Must be greater" " than slotmin.")) await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
return return
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
@@ -526,9 +522,7 @@ class Economy:
else: else:
await self.config.guild(guild).PAYDAY_TIME.set(seconds) await self.config.guild(guild).PAYDAY_TIME.set(seconds)
await ctx.send( await ctx.send(
_("Value modified. At least {} seconds must pass " "between each payday.").format( _("Value modified. At least {} seconds must pass between each payday.").format(seconds)
seconds
)
) )
@economyset.command() @economyset.command()
@@ -543,7 +537,7 @@ class Economy:
await self.config.PAYDAY_CREDITS.set(creds) await self.config.PAYDAY_CREDITS.set(creds)
else: else:
await self.config.guild(guild).PAYDAY_CREDITS.set(creds) await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
await ctx.send(_("Every payday will now give {} {}." "").format(creds, credits_name)) await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
@economyset.command() @economyset.command()
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
@@ -555,7 +549,7 @@ class Economy:
else: else:
await self.config.role(role).PAYDAY_CREDITS.set(creds) await self.config.role(role).PAYDAY_CREDITS.set(creds)
await ctx.send( await ctx.send(
_("Every payday will now give {} {} to people with the role {}." "").format( _("Every payday will now give {} {} to people with the role {}.").format(
creds, credits_name, role.name creds, credits_name, role.name
) )
) )
@@ -569,7 +563,7 @@ class Economy:
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
await bank.set_default_balance(creds, guild) await bank.set_default_balance(creds, guild)
await ctx.send( await ctx.send(
_("Registering an account will now give {} {}." "").format(creds, credits_name) _("Registering an account will now give {} {}.").format(creds, credits_name)
) )
# What would I ever do without stackoverflow? # What would I ever do without stackoverflow?

View File

@@ -39,7 +39,7 @@ class Filter:
except RuntimeError: except RuntimeError:
pass pass
@commands.group(name="filter") @commands.group(name="filter", autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_messages=True) @checks.mod_or_permissions(manage_messages=True)
async def _filter(self, ctx: commands.Context): async def _filter(self, ctx: commands.Context):
@@ -49,7 +49,6 @@ class Filter:
Using this command with no subcommands will send Using this command with no subcommands will send
the list of the server's filtered words.""" the list of the server's filtered words."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
server = ctx.guild server = ctx.guild
author = ctx.author author = ctx.author
word_list = await self.settings.guild(server).filter() word_list = await self.settings.guild(server).filter()
@@ -124,36 +123,35 @@ class Filter:
@_filter.command(name="names") @_filter.command(name="names")
async def filter_names(self, ctx: commands.Context): async def filter_names(self, ctx: commands.Context):
""" """Toggles whether or not to check names and nicknames against the filter
Toggles whether or not to check names and nicknames against the filter
This is disabled by default This is disabled by default
""" """
guild = ctx.guild guild = ctx.guild
current_setting = await self.settings.guild(guild).filter_names() current_setting = await self.settings.guild(guild).filter_names()
await self.settings.guild(guild).filter_names.set(not current_setting) await self.settings.guild(guild).filter_names.set(not current_setting)
if current_setting: if current_setting:
await ctx.send( await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
_("Names and nicknames will no longer be " "checked against the filter")
)
else: else:
await ctx.send(_("Names and nicknames will now be checked against " "the filter")) await ctx.send(_("Names and nicknames will now be checked against the filter."))
@_filter.command(name="defaultname") @_filter.command(name="defaultname")
async def filter_default_name(self, ctx: commands.Context, name: str): async def filter_default_name(self, ctx: commands.Context, name: str):
""" """Sets the default name to use if filtering names is enabled
Sets the default name to use if filtering names is enabled
Note that this has no effect if filtering names is disabled Note that this has no effect if filtering names is disabled
The default name used is John Doe The default name used is John Doe
""" """
guild = ctx.guild guild = ctx.guild
await self.settings.guild(guild).filter_default_name.set(name) await self.settings.guild(guild).filter_default_name.set(name)
await ctx.send(_("The name to use on filtered names has been set")) await ctx.send(_("The name to use on filtered names has been set."))
@_filter.command(name="ban") @_filter.command(name="ban")
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int): async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
""" """Autobans if the specified number of messages are filtered in the timeframe
Sets up an autoban if the specified number of messages are
filtered in the specified amount of time (in seconds) The timeframe is represented by seconds.
""" """
if (count <= 0) != (timeframe <= 0): if (count <= 0) != (timeframe <= 0):
await ctx.send( await ctx.send(
@@ -223,7 +221,7 @@ class Filter:
user_count >= filter_count user_count >= filter_count
and message.created_at.timestamp() < next_reset_time and message.created_at.timestamp() < next_reset_time
): ):
reason = "Autoban (too many filtered messages)" reason = "Autoban (too many filtered messages.)"
try: try:
await server.ban(author, reason=reason) await server.ban(author, reason=reason)
except: except:

View File

@@ -21,7 +21,6 @@ class RPS(Enum):
class RPSParser: class RPSParser:
def __init__(self, argument): def __init__(self, argument):
argument = argument.lower() argument = argument.lower()
if argument == "rock": if argument == "rock":
@@ -98,7 +97,7 @@ class General:
msg = "" msg = ""
if user.id == ctx.bot.user.id: if user.id == ctx.bot.user.id:
user = ctx.author user = ctx.author
msg = _("Nice try. You think this is funny?\n" "How about *this* instead:\n\n") msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n")
char = "abcdefghijklmnopqrstuvwxyz" char = "abcdefghijklmnopqrstuvwxyz"
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz" tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
table = str.maketrans(char, tran) table = str.maketrans(char, tran)
@@ -192,18 +191,12 @@ class General:
async def serverinfo(self, ctx): async def serverinfo(self, ctx):
"""Shows server's informations""" """Shows server's informations"""
guild = ctx.guild guild = ctx.guild
online = len( online = len([m.status for m in guild.members if m.status != discord.Status.offline])
[
m.status
for m in guild.members
if m.status == discord.Status.online or m.status == discord.Status.idle
]
)
total_users = len(guild.members) total_users = len(guild.members)
text_channels = len(guild.text_channels) text_channels = len(guild.text_channels)
voice_channels = len(guild.voice_channels) voice_channels = len(guild.voice_channels)
passed = (ctx.message.created_at - guild.created_at).days passed = (ctx.message.created_at - guild.created_at).days
created_at = _("Since {}. That's over {} days ago!" "").format( created_at = _("Since {}. That's over {} days ago!").format(
guild.created_at.strftime("%d %b %Y %H:%M"), passed guild.created_at.strftime("%d %b %Y %H:%M"), passed
) )
@@ -228,7 +221,7 @@ class General:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission " "to send this.")) await ctx.send(_("I need the `Embed links` permission to send this."))
@commands.command() @commands.command()
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1): async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
@@ -265,7 +258,7 @@ class General:
definition = item_list[pos]["definition"] definition = item_list[pos]["definition"]
example = item_list[pos]["example"] example = item_list[pos]["example"]
defs = len(item_list) defs = len(item_list)
msg = "**Definition #{} out of {}:\n**{}\n\n" "**Example:\n**{}".format( msg = "**Definition #{} out of {}:\n**{}\n\n**Example:\n**{}".format(
pos + 1, defs, definition, example pos + 1, defs, definition, example
) )
msg = pagify(msg, ["\n"]) msg = pagify(msg, ["\n"])

View File

@@ -13,6 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_) @cog_i18n(_)
class Image: class Image:
"""Image related commands.""" """Image related commands."""
default_global = {"imgur_client_id": None} default_global = {"imgur_client_id": None}
def __init__(self, bot): def __init__(self, bot):
@@ -25,14 +26,13 @@ class Image:
def __unload(self): def __unload(self):
self.session.close() self.session.close()
@commands.group(name="imgur") @commands.group(name="imgur", autohelp=True)
async def _imgur(self, ctx): async def _imgur(self, ctx):
"""Retrieves pictures from imgur """Retrieves pictures from imgur
Make sure to set the client ID using Make sure to set the client ID using
[p]imgurcreds""" [p]imgurcreds"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@_imgur.command(name="search") @_imgur.command(name="search")
async def imgur_search(self, ctx, *, term: str): async def imgur_search(self, ctx, *, term: str):
@@ -42,7 +42,7 @@ class Image:
imgur_client_id = await self.settings.imgur_client_id() imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id: if not imgur_client_id:
await ctx.send( await ctx.send(
_("A client ID has not been set! Please set one with {}").format( _("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix) "`{}imgurcreds`".format(ctx.prefix)
) )
) )
@@ -54,7 +54,7 @@ class Image:
if data["success"]: if data["success"]:
results = data["data"] results = data["data"]
if not results: if not results:
await ctx.send(_("Your search returned no results")) await ctx.send(_("Your search returned no results."))
return return
shuffle(results) shuffle(results)
msg = _("Search results...\n") msg = _("Search results...\n")
@@ -63,7 +63,7 @@ class Image:
msg += "\n" msg += "\n"
await ctx.send(msg) await ctx.send(msg)
else: else:
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"])) await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
@_imgur.command(name="subreddit") @_imgur.command(name="subreddit")
async def imgur_subreddit( async def imgur_subreddit(
@@ -91,7 +91,7 @@ class Image:
imgur_client_id = await self.settings.imgur_client_id() imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id: if not imgur_client_id:
await ctx.send( await ctx.send(
_("A client ID has not been set! Please set one with {}").format( _("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix) "`{}imgurcreds`".format(ctx.prefix)
) )
) )
@@ -116,12 +116,13 @@ class Image:
else: else:
await ctx.send(_("No results found.")) await ctx.send(_("No results found."))
else: else:
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"])) await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
@checks.is_owner() @checks.is_owner()
@commands.command() @commands.command()
async def imgurcreds(self, ctx, imgur_client_id: str): async def imgurcreds(self, ctx, imgur_client_id: str):
"""Sets the imgur client id """Sets the imgur client id
You will need an account on Imgur to get this You will need an account on Imgur to get this
You can get these by visiting https://api.imgur.com/oauth2/addclient You can get these by visiting https://api.imgur.com/oauth2/addclient
@@ -130,7 +131,7 @@ class Image:
set the authorization callback url to 'https://localhost' set the authorization callback url to 'https://localhost'
leave the app website blank, enter a valid email address, and leave the app website blank, enter a valid email address, and
enter a description. Check the box for the captcha, then click Next. enter a description. Check the box for the captcha, then click Next.
Your client ID will be on the page that loads""" Your client ID will be on the page that loads."""
await self.settings.imgur_client_id.set(imgur_client_id) await self.settings.imgur_client_id.set(imgur_client_id)
await ctx.send(_("Set the imgur client id!")) await ctx.send(_("Set the imgur client id!"))
@@ -143,7 +144,7 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}" "".format( url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
GIPHY_API_KEY, keywords GIPHY_API_KEY, keywords
) )
@@ -155,7 +156,7 @@ class Image:
else: else:
await ctx.send(_("No results found.")) await ctx.send(_("No results found."))
else: else:
await ctx.send(_("Error contacting the API")) await ctx.send(_("Error contacting the API."))
@commands.command(pass_context=True, no_pm=True) @commands.command(pass_context=True, no_pm=True)
async def gifr(self, ctx, *keywords): async def gifr(self, ctx, *keywords):
@@ -166,7 +167,7 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}" "".format( url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
GIPHY_API_KEY, keywords GIPHY_API_KEY, keywords
) )
@@ -178,4 +179,4 @@ class Image:
else: else:
await ctx.send(_("No results found.")) await ctx.send(_("No results found."))
else: else:
await ctx.send(_("Error contacting the API")) await ctx.send(_("Error contacting the API."))

View File

@@ -1,9 +1,8 @@
from discord.ext import commands from redbot.core import commands
import discord import discord
def mod_or_voice_permissions(**perms): def mod_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -31,7 +30,6 @@ def mod_or_voice_permissions(**perms):
def admin_or_voice_permissions(**perms): def admin_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -54,7 +52,6 @@ def admin_or_voice_permissions(**perms):
def bot_has_voice_permissions(**perms): def bot_has_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
guild = ctx.guild guild = ctx.guild
for vc in guild.voice_channels: for vc in guild.voice_channels:

View File

@@ -161,14 +161,12 @@ class Mod:
except RuntimeError: except RuntimeError:
pass pass
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def modset(self, ctx: commands.Context): async def modset(self, ctx: commands.Context):
"""Manages server administration settings.""" """Manages server administration settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
guild = ctx.guild
await ctx.send_help()
# Display current settings # Display current settings
delete_repeats = await self.settings.guild(guild).delete_repeats() delete_repeats = await self.settings.guild(guild).delete_repeats()
@@ -199,12 +197,12 @@ class Mod:
if not toggled: if not toggled:
await self.settings.guild(guild).respect_hierarchy.set(True) await self.settings.guild(guild).respect_hierarchy.set(True)
await ctx.send( await ctx.send(
_("Role hierarchy will be checked when " "moderation commands are issued.") _("Role hierarchy will be checked when moderation commands are issued.")
) )
else: else:
await self.settings.guild(guild).respect_hierarchy.set(False) await self.settings.guild(guild).respect_hierarchy.set(False)
await ctx.send( await ctx.send(
_("Role hierarchy will be ignored when " "moderation commands are issued.") _("Role hierarchy will be ignored when moderation commands are issued.")
) )
@modset.command() @modset.command()
@@ -241,7 +239,7 @@ class Mod:
cur_setting = await self.settings.guild(guild).delete_repeats() cur_setting = await self.settings.guild(guild).delete_repeats()
if not cur_setting: if not cur_setting:
await self.settings.guild(guild).delete_repeats.set(True) await self.settings.guild(guild).delete_repeats.set(True)
await ctx.send(_("Messages repeated up to 3 times will " "be deleted.")) await ctx.send(_("Messages repeated up to 3 times will be deleted."))
else: else:
await self.settings.guild(guild).delete_repeats.set(False) await self.settings.guild(guild).delete_repeats.set(False)
await ctx.send(_("Repeated messages will be ignored.")) await ctx.send(_("Repeated messages will be ignored."))
@@ -304,7 +302,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -316,6 +314,9 @@ class Mod:
) )
) )
return return
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
audit_reason = get_audit_reason(author, reason) audit_reason = get_audit_reason(author, reason)
try: try:
await guild.kick(user, reason=audit_reason) await guild.kick(user, reason=audit_reason)
@@ -357,7 +358,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -369,6 +370,9 @@ class Mod:
) )
) )
return return
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
if days: if days:
if days.isdigit(): if days.isdigit():
@@ -451,15 +455,15 @@ class Mod:
self.ban_queue.append(queue_entry) self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason) await guild.ban(user, reason=audit_reason)
log.info("{}({}) hackbanned {}" "".format(author.name, author.id, user_id)) log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound: except discord.NotFound:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("User not found. Have you provided the " "correct user ID?")) await ctx.send(_("User not found. Have you provided the correct user ID?"))
except discord.Forbidden: except discord.Forbidden:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("I lack the permissions to do this.")) await ctx.send(_("I lack the permissions to do this."))
else: else:
await ctx.send(_("Done. The user will not be able to join this " "server.")) await ctx.send(_("Done. The user will not be able to join this server."))
user_info = await self.bot.get_user_info(user_id) user_info = await self.bot.get_user_info(user_id)
try: try:
@@ -547,7 +551,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -753,7 +757,7 @@ class Mod:
else: else:
await ctx.send(_("That user is already muted and deafened server-wide!")) await ctx.send(_("That user is already muted and deafened server-wide!"))
return return
await ctx.send(_("User has been banned from speaking or " "listening in voice channels")) await ctx.send(_("User has been banned from speaking or listening in voice channels"))
try: try:
await modlog.create_case( await modlog.create_case(
@@ -825,16 +829,15 @@ class Mod:
await ctx.send("Done.") await ctx.send("Done.")
except discord.Forbidden: except discord.Forbidden:
await ctx.send( await ctx.send(
_("I cannot do that, I lack the " "'{}' permission.").format("Manage Nicknames") _("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
) )
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channel=True)
async def mute(self, ctx: commands.Context): async def mute(self, ctx: commands.Context):
"""Mutes user in the channel/server""" """Mutes user in the channel/server"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@mute.command(name="voice") @mute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@@ -995,15 +998,14 @@ class Mod:
await self.settings.member(user).perms_cache.set(perms_cache) await self.settings.member(user).perms_cache.set(perms_cache)
return True, None return True, None
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channel=True)
async def unmute(self, ctx: commands.Context): async def unmute(self, ctx: commands.Context):
"""Unmutes user in the channel/server """Unmutes user in the channel/server
Defaults to channel""" Defaults to channel"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@unmute.command(name="voice") @unmute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@@ -1162,7 +1164,7 @@ class Mod:
await self.settings.member(user).perms_cache.set(perms_cache) await self.settings.member(user).perms_cache.set(perms_cache)
return True, None return True, None
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @checks.admin_or_permissions(manage_channels=True)
async def ignore(self, ctx: commands.Context): async def ignore(self, ctx: commands.Context):
@@ -1195,13 +1197,12 @@ class Mod:
else: else:
await ctx.send(_("This server is already being ignored.")) await ctx.send(_("This server is already being ignored."))
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @checks.admin_or_permissions(manage_channels=True)
async def unignore(self, ctx: commands.Context): async def unignore(self, ctx: commands.Context):
"""Removes servers/channels from ignorelist""" """Removes servers/channels from ignorelist"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@unignore.command(name="channel") @unignore.command(name="channel")
@@ -1319,7 +1320,7 @@ class Mod:
value="{0.name} (ID {0.id})".format(voice_state.channel), value="{0.name} (ID {0.id})".format(voice_state.channel),
inline=False, inline=False,
) )
data.set_footer(text=_("Member #{} | User ID: {}" "").format(member_number, user.id)) data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
name = str(user) name = str(user)
name = " ~ ".join((name, user.nick)) if user.nick else name name = " ~ ".join((name, user.nick)) if user.nick else name
@@ -1335,7 +1336,7 @@ class Mod:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission " "to send this.")) await ctx.send(_("I need the `Embed links` permission to send this."))
@commands.command() @commands.command()
async def names(self, ctx: commands.Context, user: discord.Member): async def names(self, ctx: commands.Context, user: discord.Member):
@@ -1355,7 +1356,7 @@ class Mod:
if msg: if msg:
await ctx.send(msg) await ctx.send(msg)
else: else:
await ctx.send(_("That user doesn't have any recorded name or " "nickname change.")) await ctx.send(_("That user doesn't have any recorded name or nickname change."))
async def get_names_and_nicks(self, user): async def get_names_and_nicks(self, user):
names = await self.settings.user(user).past_names() names = await self.settings.user(user).past_names()
@@ -1418,7 +1419,7 @@ class Mod:
await guild.ban(author, reason="Mention spam (Autoban)") await guild.ban(author, reason="Mention spam (Autoban)")
except discord.HTTPException: except discord.HTTPException:
log.info( log.info(
"Failed to ban member for mention spam in " "server {}.".format(guild.id) "Failed to ban member for mention spam in server {}.".format(guild.id)
) )
else: else:
try: try:
@@ -1439,7 +1440,13 @@ class Mod:
return True return True
return False return False
async def on_command(self, ctx: commands.Context): async def on_command_completion(self, ctx: commands.Context):
await self._delete_delay(ctx)
async def on_command_error(self, ctx: commands.Context, error):
await self._delete_delay(ctx)
async def _delete_delay(self, ctx: commands.Context):
"""Currently used for: """Currently used for:
* delete delay""" * delete delay"""
guild = ctx.guild guild = ctx.guild

View File

@@ -15,12 +15,11 @@ class ModLog:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@commands.group() @commands.group(autohelp=True)
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: commands.Context): async def modlogset(self, ctx: commands.Context):
"""Settings for the mod log""" """Settings for the mod log"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@modlogset.command() @modlogset.command()
@commands.guild_only() @commands.guild_only()
@@ -35,9 +34,7 @@ class ModLog:
await ctx.send(_("Mod events will be sent to {}").format(channel.mention)) await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
else: else:
await ctx.send( await ctx.send(
_("I do not have permissions to " "send messages in {}!").format( _("I do not have permissions to send messages in {}!").format(channel.mention)
channel.mention
)
) )
else: else:
try: try:

View File

@@ -3,7 +3,6 @@ from typing import Tuple
class CogOrCommand(commands.Converter): class CogOrCommand(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]: async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
ret = ctx.bot.get_cog(arg) ret = ctx.bot.get_cog(arg)
if ret: if ret:
@@ -12,15 +11,19 @@ class CogOrCommand(commands.Converter):
if ret: if ret:
return "commands", ret.qualified_name return "commands", ret.qualified_name
raise commands.BadArgument() raise commands.BadArgument(
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
"".format(arg=arg)
)
class RuleType(commands.Converter): class RuleType(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> str: async def convert(self, ctx: commands.Context, arg: str) -> str:
if arg.lower() in ("allow", "whitelist", "allowed"): if arg.lower() in ("allow", "whitelist", "allowed"):
return "allow" return "allow"
if arg.lower() in ("deny", "blacklist", "denied"): if arg.lower() in ("deny", "blacklist", "denied"):
return "deny" return "deny"
raise commands.BadArgument() raise commands.BadArgument(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
)

View File

@@ -12,7 +12,7 @@ from .resolvers import val_if_check_is_valid, resolve_models
from .yaml_handler import yamlset_acl, yamlget_acl from .yaml_handler import yamlset_acl, yamlget_acl
from .converters import CogOrCommand, RuleType from .converters import CogOrCommand, RuleType
_models = ["owner", "guildowner", "admin", "mod"] _models = ["owner", "guildowner", "admin", "mod", "all"]
_ = Translator("Permissions", __file__) _ = Translator("Permissions", __file__)
@@ -32,60 +32,9 @@ class Permissions:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True) self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
self._before = []
self._after = []
self.config.register_global(owner_models={}) self.config.register_global(owner_models={})
self.config.register_guild(owner_models={}) self.config.register_guild(owner_models={})
def add_check(self, check_obj: object, before_or_after: str):
"""
adds a check to the check ordering
checks should be a function taking 2 arguments:
ctx: commands.Context
level: str
and returning:
None: do not interfere
True: command should be allowed even if they dont
have role or perm requirements for the check
False: command should be blocked
before_or_after:
Should literally be a str equaling 'before' or 'after'
This should be based on if this should take priority
over set rules or not
3rd party cogs adding checks using this should only allow
the owner to add checks before, and ensure only the owner
can add checks recieving the level 'owner'
3rd party cogs should keep a copy of of any checks they registered
and deregister then on unload
"""
if before_or_after == "before":
self._before.append(check_obj)
elif before_or_after == "after":
self._after.append(check_obj)
else:
raise TypeError("RTFM")
def remove_check(self, check_obj: object, before_or_after: str):
"""
removes a previously registered check object
3rd party cogs should keep a copy of of any checks they registered
and deregister then on unload
"""
if before_or_after == "before":
self._before.remove(check_obj)
elif before_or_after == "after":
self._after.remove(check_obj)
else:
raise TypeError("RTFM")
async def __global_check(self, ctx): async def __global_check(self, ctx):
""" """
Yes, this is needed on top of hooking into checks.py Yes, this is needed on top of hooking into checks.py
@@ -94,7 +43,7 @@ class Permissions:
defering to check logic defering to check logic
This works since all checks must be True to run This works since all checks must be True to run
""" """
v = await self.check_overrides(ctx, "mod") v = await self.check_overrides(ctx, "all")
if v is False: if v is False:
return False return False
@@ -109,7 +58,7 @@ class Permissions:
ctx: `redbot.core.context.commands.Context` ctx: `redbot.core.context.commands.Context`
The context of the command The context of the command
level: `str` level: `str`
One of 'owner', 'guildowner', 'admin', 'mod' One of 'owner', 'guildowner', 'admin', 'mod', 'all'
Returns Returns
------- -------
@@ -126,7 +75,13 @@ class Permissions:
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else [] roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
entries.extend([x.id for x in roles]) entries.extend([x.id for x in roles])
for check in self._before: before = [
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
for cog in ctx.bot.cogs.values()
]
for check in before:
if check is None:
continue
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level) override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
if override is not None: if override is not None:
return override return override
@@ -137,7 +92,11 @@ class Permissions:
if override is not None: if override is not None:
return override return override
for check in self._after: after = [
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
for cog in ctx.bot.cogs.values()
]
for check in after:
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level) override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
if override is not None: if override is not None:
return override return override
@@ -166,13 +125,12 @@ class Permissions:
# async def admin_model(self, ctx: commands.Context) -> bool: # async def admin_model(self, ctx: commands.Context) -> bool:
# async def mod_model(self, ctx: commands.Context) -> bool: # async def mod_model(self, ctx: commands.Context) -> bool:
@commands.group(aliases=["p"]) @commands.group(aliases=["p"], autohelp=True)
async def permissions(self, ctx: commands.Context): async def permissions(self, ctx: commands.Context):
""" """
Permission management tools Permission management tools
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@permissions.command() @permissions.command()
async def explain(self, ctx: commands.Context): async def explain(self, ctx: commands.Context):
@@ -206,10 +164,10 @@ class Permissions:
"\n" "\n"
"1. Rules about a user.\n" "1. Rules about a user.\n"
"2. Rules about the voice channel a user is in.\n" "2. Rules about the voice channel a user is in.\n"
"3. Rules about the text channel a command was issued in\n" "3. Rules about the text channel a command was issued in.\n"
"4. Rules about a role the user has " "4. Rules about a role the user has "
"(The highest role they have with a rule will be used)\n" "(The highest role they have with a rule will be used).\n"
"5. Rules about the guild a user is in (Owner level only)" "5. Rules about the guild a user is in (Owner level only)."
"\n\nFor more details, please read the official documentation." "\n\nFor more details, please read the official documentation."
) )
@@ -236,7 +194,9 @@ class Permissions:
else: else:
try: try:
testcontext = await self.bot.get_context(message, cls=commands.Context) testcontext = await self.bot.get_context(message, cls=commands.Context)
can = await com.can_run(testcontext) can = await com.can_run(testcontext) and all(
[await p.can_run(testcontext) for p in com.parents]
)
except commands.CheckFailure: except commands.CheckFailure:
can = False can = False
@@ -254,13 +214,13 @@ class Permissions:
Take a YAML file upload to set permissions from Take a YAML file upload to set permissions from
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.owner_models, update=False) await yamlset_acl(ctx, config=self.config.owner_models, update=False)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
@@ -280,13 +240,13 @@ class Permissions:
Take a YAML file upload to set permissions from Take a YAML file upload to set permissions from
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False) await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
@@ -309,13 +269,13 @@ class Permissions:
Use this to not lose existing rules Use this to not lose existing rules
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True) await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
@@ -328,13 +288,13 @@ class Permissions:
Use this to not lose existing rules Use this to not lose existing rules
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.owner_models, update=True) await yamlset_acl(ctx, config=self.config.owner_models, update=True)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
@@ -348,7 +308,7 @@ class Permissions:
who_or_what: str, who_or_what: str,
): ):
""" """
adds something to the rules Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify allow_or_deny: "allow" or "deny", depending on the rule to modify
@@ -363,7 +323,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.owner_models() as models: async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -392,7 +352,7 @@ class Permissions:
who_or_what: str, who_or_what: str,
): ):
""" """
adds something to the rules Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify allow_or_deny: "allow" or "deny", depending on the rule to modify
@@ -407,7 +367,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -450,7 +410,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.owner_models() as models: async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -494,7 +454,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -540,7 +500,7 @@ class Permissions:
data[model_type][type_name]["default"] = val_to_set data[model_type][type_name]["default"] = val_to_set
models.update(data) models.update(data)
await ctx.send(_("Defualt set.")) await ctx.send(_("Default set."))
@checks.is_owner() @checks.is_owner()
@permissions.command(name="setdefaultglobalrule") @permissions.command(name="setdefaultglobalrule")
@@ -570,7 +530,7 @@ class Permissions:
data[model_type][type_name]["default"] = val_to_set data[model_type][type_name]["default"] = val_to_set
models.update(data) models.update(data)
await ctx.send(_("Defualt set.")) await ctx.send(_("Default set."))
@commands.bot_has_permissions(add_reactions=True) @commands.bot_has_permissions(add_reactions=True)
@checks.is_owner() @checks.is_owner()
@@ -592,7 +552,7 @@ class Permissions:
if REACTS.get(str(reaction)): if REACTS.get(str(reaction)):
await self.config.owner_models.clear() await self.config.owner_models.clear()
await ctx.send(_("Global settings cleared")) await ctx.send(_("Global settings cleared."))
else: else:
await ctx.send(_("Okay.")) await ctx.send(_("Okay."))
@@ -617,7 +577,7 @@ class Permissions:
if REACTS.get(str(reaction)): if REACTS.get(str(reaction)):
await self.config.guild(ctx.guild).owner_models.clear() await self.config.guild(ctx.guild).owner_models.clear()
await ctx.send(_("Guild settings cleared")) await ctx.send(_("Guild settings cleared."))
else: else:
await ctx.send(_("Okay.")) await ctx.send(_("Okay."))

View File

@@ -12,21 +12,10 @@ async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level:
Returns the value from a check if it is valid Returns the value from a check if it is valid
""" """
# Non staticmethods should not be run without their parent
# class, even if the parent class did not deregister them
if check.__module__ is None:
pass
elif isinstance(check, types.FunctionType):
if (
next(filter(lambda x: check.__module__ == x.__module__, ctx.bot.cogs.values()), None)
is None
):
return None
val = None val = None
# let's not spam the console with improperly made 3rd party checks # let's not spam the console with improperly made 3rd party checks
try: try:
if asyncio.iscoroutine(check) or asyncio.iscoroutinefunction(check): if asyncio.iscoroutinefunction(check):
val = await check(ctx, level=level) val = await check(ctx, level=level)
else: else:
val = check(ctx, level=level) val = check(ctx, level=level)

View File

@@ -56,7 +56,7 @@ class Reports:
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@commands.guild_only() @commands.guild_only()
@commands.group(name="reportset") @commands.group(name="reportset", autohelp=True)
async def reportset(self, ctx: commands.Context): async def reportset(self, ctx: commands.Context):
""" """
settings for reports settings for reports
@@ -212,11 +212,6 @@ class Reports:
guild = await self.discover_guild( guild = await self.discover_guild(
author, prompt=_("Select a server to make a report in by number.") author, prompt=_("Select a server to make a report in by number.")
) )
else:
try:
await ctx.message.delete()
except discord.Forbidden:
pass
if guild is None: if guild is None:
return return
g_active = await self.config.guild(guild).active() g_active = await self.config.guild(guild).active()
@@ -234,17 +229,10 @@ class Reports:
"later." "later."
) )
) )
if author.id in self.user_cache: if author.id in self.user_cache:
return await author.send( return await author.send(
_("Finish making your prior report " "before making an additional one") _("Please finish making your prior report before making an additional one")
) )
if ctx.guild:
try:
await ctx.message.delete()
except (discord.Forbidden, discord.HTTPException):
pass
self.user_cache.append(author.id) self.user_cache.append(author.id)
if _report: if _report:
@@ -261,9 +249,7 @@ class Reports:
) )
) )
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("This requires DMs enabled.")) return await ctx.send(_("This requires DMs enabled."))
self.user_cache.remove(author.id)
return
def pred(m): def pred(m):
return m.author == author and m.channel == dm.channel return m.author == author and m.channel == dm.channel
@@ -271,7 +257,7 @@ class Reports:
try: try:
message = await self.bot.wait_for("message", check=pred, timeout=180) message = await self.bot.wait_for("message", check=pred, timeout=180)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await author.send(_("You took too long. Try again later.")) return await author.send(_("You took too long. Try again later."))
else: else:
val = await self.send_report(message, guild) val = await self.send_report(message, guild)
@@ -280,9 +266,21 @@ class Reports:
await author.send(_("There was an error sending your report.")) await author.send(_("There was an error sending your report."))
else: else:
await author.send(_("Your report was submitted. (Ticket #{})").format(val)) await author.send(_("Your report was submitted. (Ticket #{})").format(val))
self.antispam[guild.id][author.id].stamp() self.antispam[guild.id][author.id].stamp()
self.user_cache.remove(author.id) @report.after_invoke
async def report_cleanup(self, ctx: commands.Context):
"""
The logic is cleaner this way
"""
if ctx.author.id in self.user_cache:
self.user_cache.remove(ctx.author.id)
if ctx.guild and ctx.invoked_subcommand is None:
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
try:
await ctx.message.delete()
except discord.NotFound:
pass
async def on_raw_reaction_add(self, payload): async def on_raw_reaction_add(self, payload):
""" """

View File

@@ -1,5 +1,7 @@
from .streams import Streams from .streams import Streams
def setup(bot): async def setup(bot):
bot.add_cog(Streams(bot)) cog = Streams(bot)
await cog.initialize()
bot.add_cog(cog)

View File

@@ -4,6 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from .streamtypes import ( from .streamtypes import (
Stream,
TwitchStream, TwitchStream,
HitboxStream, HitboxStream,
MixerStream, MixerStream,
@@ -25,6 +26,7 @@ from . import streamtypes as StreamClasses
from collections import defaultdict from collections import defaultdict
import asyncio import asyncio
import re import re
from typing import Optional, List
CHECK_DELAY = 60 CHECK_DELAY = 60
@@ -50,9 +52,11 @@ class Streams:
self.db.register_role(**self.role_defaults) self.db.register_role(**self.role_defaults)
self.bot = bot self.bot: Red = bot
self.bot.loop.create_task(self._initialize_lists()) self.streams: List[Stream] = []
self.communities: List[TwitchCommunity] = []
self.task: Optional[asyncio.Task] = None
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$") self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
@@ -62,7 +66,8 @@ class Streams:
return True return True
return False return False
async def _initialize_lists(self): async def initialize(self) -> None:
"""Should be called straight after cog instantiation."""
self.streams = await self.load_streams() self.streams = await self.load_streams()
self.communities = await self.load_communities() self.communities = await self.load_communities()
@@ -77,9 +82,7 @@ class Streams:
@commands.command() @commands.command()
async def youtube(self, ctx: commands.Context, channel_id_or_name: str): async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
""" """Checks if a Youtube channel is streaming"""
Checks if a Youtube channel is streaming
"""
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None) apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
is_name = self.check_name_or_id(channel_id_or_name) is_name = self.check_name_or_id(channel_id_or_name)
if is_name: if is_name:
@@ -115,35 +118,33 @@ class Streams:
await ctx.send(_("The channel doesn't seem to exist.")) await ctx.send(_("The channel doesn't seem to exist."))
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( await ctx.send(
_("The twitch token is either invalid or has not been set. " "See `{}`.").format( _("The twitch token is either invalid or has not been set. See `{}`.").format(
"{}streamset twitchtoken".format(ctx.prefix) "{}streamset twitchtoken".format(ctx.prefix)
) )
) )
except InvalidYoutubeCredentials: except InvalidYoutubeCredentials:
await ctx.send( await ctx.send(
_("The Youtube API key is either invalid or has not been set. " "See {}.").format( _("The Youtube API key is either invalid or has not been set. See {}.").format(
"`{}streamset youtubekey`".format(ctx.prefix) "`{}streamset youtubekey`".format(ctx.prefix)
) )
) )
except APIError: except APIError:
await ctx.send( await ctx.send(
_("Something went wrong whilst trying to contact the " "stream service's API.") _("Something went wrong whilst trying to contact the stream service's API.")
) )
else: else:
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.mod() @checks.mod()
async def streamalert(self, ctx: commands.Context): async def streamalert(self, ctx: commands.Context):
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@streamalert.group(name="twitch") @streamalert.group(name="twitch", autohelp=True)
async def _twitch(self, ctx: commands.Context): async def _twitch(self, ctx: commands.Context):
"""Twitch stream alerts""" """Twitch stream alerts"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch: pass
await ctx.send_help()
@_twitch.command(name="channel") @_twitch.command(name="channel")
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str): async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
@@ -152,8 +153,7 @@ class Streams:
@_twitch.command(name="community") @_twitch.command(name="community")
async def twitch_alert_community(self, ctx: commands.Context, community: str): async def twitch_alert_community(self, ctx: commands.Context, community: str):
"""Sets a Twitch stream alert notification in the channel """Sets a Twitch stream alert notification in the channel for the specified community."""
for the specified community."""
await self.community_alert(ctx, TwitchCommunity, community.lower()) await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube") @streamalert.command(name="youtube")
@@ -202,7 +202,7 @@ class Streams:
self.streams = streams self.streams = streams
await self.save_streams() await self.save_streams()
msg = _("All {}'s stream alerts have been disabled." "").format( msg = _("All {}'s stream alerts have been disabled.").format(
"server" if _all else "channel" "server" if _all else "channel"
) )
@@ -243,21 +243,21 @@ class Streams:
exists = await self.check_exists(stream) exists = await self.check_exists(stream)
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( await ctx.send(
_("The twitch token is either invalid or has not been set. " "See {}.").format( _("The twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix) "`{}streamset twitchtoken`".format(ctx.prefix)
) )
) )
return return
except InvalidYoutubeCredentials: except InvalidYoutubeCredentials:
await ctx.send( await ctx.send(
_( _("The Youtube API key is either invalid or has not been set. See {}.").format(
"The Youtube API key is either invalid or has not been set. " "See {}." "`{}streamset youtubekey`".format(ctx.prefix)
).format("`{}streamset youtubekey`".format(ctx.prefix)) )
) )
return return
except APIError: except APIError:
await ctx.send( await ctx.send(
_("Something went wrong whilst trying to contact the " "stream service's API.") _("Something went wrong whilst trying to contact the stream service's API.")
) )
return return
else: else:
@@ -276,7 +276,7 @@ class Streams:
await community.get_community_streams() await community.get_community_streams()
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( await ctx.send(
_("The twitch token is either invalid or has not been set. " "See {}.").format( _("The twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix) "`{}streamset twitchtoken`".format(ctx.prefix)
) )
) )
@@ -286,7 +286,7 @@ class Streams:
return return
except APIError: except APIError:
await ctx.send( await ctx.send(
_("Something went wrong whilst trying to contact the " "stream service's API.") _("Something went wrong whilst trying to contact the stream service's API.")
) )
return return
except OfflineCommunity: except OfflineCommunity:
@@ -294,11 +294,10 @@ class Streams:
await self.add_or_remove_community(ctx, community) await self.add_or_remove_community(ctx, community)
@commands.group() @commands.group(autohelp=True)
@checks.mod() @checks.mod()
async def streamset(self, ctx: commands.Context): async def streamset(self, ctx: commands.Context):
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@streamset.command() @streamset.command()
@checks.is_owner() @checks.is_owner()
@@ -334,12 +333,11 @@ class Streams:
await self.db.tokens.set_raw("YoutubeStream", value=key) await self.db.tokens.set_raw("YoutubeStream", value=key)
await ctx.send(_("Youtube key set.")) await ctx.send(_("Youtube key set."))
@streamset.group() @streamset.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
async def mention(self, ctx: commands.Context): async def mention(self, ctx: commands.Context):
"""Sets mentions for stream alerts.""" """Sets mentions for stream alerts."""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention: pass
await ctx.send_help()
@mention.command(aliases=["everyone"]) @mention.command(aliases=["everyone"])
@commands.guild_only() @commands.guild_only()
@@ -350,16 +348,14 @@ class Streams:
if current_setting: if current_setting:
await self.db.guild(guild).mention_everyone.set(False) await self.db.guild(guild).mention_everyone.set(False)
await ctx.send( await ctx.send(
_("{} will no longer be mentioned " "for a stream alert.").format( _("{} will no longer be mentioned for a stream alert.").format("@\u200beveryone")
"@\u200beveryone"
)
) )
else: else:
await self.db.guild(guild).mention_everyone.set(True) await self.db.guild(guild).mention_everyone.set(True)
await ctx.send( await ctx.send(
_( _(
"When a stream configured for stream alerts " "When a stream configured for stream alerts "
"comes online, {} will be mentioned" "comes online, {} will be mentioned."
).format("@\u200beveryone") ).format("@\u200beveryone")
) )
@@ -372,14 +368,14 @@ class Streams:
if current_setting: if current_setting:
await self.db.guild(guild).mention_here.set(False) await self.db.guild(guild).mention_here.set(False)
await ctx.send( await ctx.send(
_("{} will no longer be mentioned " "for a stream alert.").format("@\u200bhere") _("{} will no longer be mentioned for a stream alert.").format("@\u200bhere")
) )
else: else:
await self.db.guild(guild).mention_here.set(True) await self.db.guild(guild).mention_here.set(True)
await ctx.send( await ctx.send(
_( _(
"When a stream configured for stream alerts " "When a stream configured for stream alerts "
"comes online, {} will be mentioned" "comes online, {} will be mentioned."
).format("@\u200bhere") ).format("@\u200bhere")
) )
@@ -394,7 +390,7 @@ class Streams:
if current_setting: if current_setting:
await self.db.role(role).mention.set(False) await self.db.role(role).mention.set(False)
await ctx.send( await ctx.send(
_("{} will no longer be mentioned " "for a stream alert").format( _("{} will no longer be mentioned for a stream alert.").format(
"@\u200b{}".format(role.name) "@\u200b{}".format(role.name)
) )
) )
@@ -403,7 +399,7 @@ class Streams:
await ctx.send( await ctx.send(
_( _(
"When a stream configured for stream alerts " "When a stream configured for stream alerts "
"comes online, {} will be mentioned" "comes online, {} will be mentioned."
"" ""
).format("@\u200b{}".format(role.name)) ).format("@\u200b{}".format(role.name))
) )
@@ -414,7 +410,7 @@ class Streams:
"""Toggles automatic deletion of notifications for streams that go offline""" """Toggles automatic deletion of notifications for streams that go offline"""
await self.db.guild(ctx.guild).autodelete.set(on_off) await self.db.guild(ctx.guild).autodelete.set(on_off)
if on_off: if on_off:
await ctx.send("The notifications will be deleted once " "streams go offline.") await ctx.send("The notifications will be deleted once streams go offline.")
else: else:
await ctx.send("Notifications will never be deleted.") await ctx.send("Notifications will never be deleted.")
@@ -424,7 +420,7 @@ class Streams:
if stream not in self.streams: if stream not in self.streams:
self.streams.append(stream) self.streams.append(stream)
await ctx.send( await ctx.send(
_("I'll send a notification in this channel when {} " "is online.").format( _("I'll send a notification in this channel when {} is online.").format(
stream.name stream.name
) )
) )
@@ -433,7 +429,7 @@ class Streams:
if not stream.channels: if not stream.channels:
self.streams.remove(stream) self.streams.remove(stream)
await ctx.send( await ctx.send(
_("I won't send notifications about {} in this " "channel anymore.").format( _("I won't send notifications about {} in this channel anymore.").format(
stream.name stream.name
) )
) )
@@ -448,7 +444,7 @@ class Streams:
await ctx.send( await ctx.send(
_( _(
"I'll send a notification in this channel when a " "I'll send a notification in this channel when a "
"channel is streaming to the {} community" "channel is streaming to the {} community."
"" ""
).format(community.name) ).format(community.name)
) )
@@ -459,7 +455,7 @@ class Streams:
await ctx.send( await ctx.send(
_( _(
"I won't send notifications about channels streaming " "I won't send notifications about channels streaming "
"to the {} community in this channel anymore" "to the {} community in this channel anymore."
"" ""
).format(community.name) ).format(community.name)
) )
@@ -671,4 +667,5 @@ class Streams:
await self.db.communities.set(raw_communities) await self.db.communities.set(raw_communities)
def __unload(self): def __unload(self):
self.task.cancel() if self.task:
self.task.cancel()

View File

@@ -30,7 +30,6 @@ def rnd(url):
class TwitchCommunity: class TwitchCommunity:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.name = kwargs.pop("name") self.name = kwargs.pop("name")
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
@@ -119,7 +118,6 @@ class TwitchCommunity:
class Stream: class Stream:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.name = kwargs.pop("name", None) self.name = kwargs.pop("name", None)
self.channels = kwargs.pop("channels", []) self.channels = kwargs.pop("channels", [])
@@ -148,7 +146,6 @@ class Stream:
class YoutubeStream(Stream): class YoutubeStream(Stream):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
@@ -183,7 +180,7 @@ class YoutubeStream(Stream):
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"]) video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
title = vid_data["snippet"]["title"] title = vid_data["snippet"]["title"]
thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"] thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"]
channel_title = data["snippet"]["channelTitle"] channel_title = vid_data["snippet"]["channelTitle"]
embed = discord.Embed(title=title, url=video_url) embed = discord.Embed(title=title, url=video_url)
embed.set_author(name=channel_title) embed.set_author(name=channel_title)
embed.set_image(url=rnd(thumbnail)) embed.set_image(url=rnd(thumbnail))
@@ -213,7 +210,6 @@ class YoutubeStream(Stream):
class TwitchStream(Stream): class TwitchStream(Stream):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
@@ -266,7 +262,7 @@ class TwitchStream(Stream):
url = channel["url"] url = channel["url"]
logo = channel["logo"] logo = channel["logo"]
if logo is None: if logo is None:
logo = "https://static-cdn.jtvnw.net/" "jtv_user_pictures/xarth/404_user_70x70.png" logo = "https://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_70x70.png"
status = channel["status"] status = channel["status"]
if not status: if not status:
status = "Untitled broadcast" status = "Untitled broadcast"
@@ -288,7 +284,6 @@ class TwitchStream(Stream):
class HitboxStream(Stream): class HitboxStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://api.hitbox.tv/media/live/" + self.name url = "https://api.hitbox.tv/media/live/" + self.name
@@ -326,7 +321,6 @@ class HitboxStream(Stream):
class MixerStream(Stream): class MixerStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://mixer.com/api/v1/channels/" + self.name url = "https://mixer.com/api/v1/channels/" + self.name
@@ -348,7 +342,7 @@ class MixerStream(Stream):
raise APIError() raise APIError()
def make_embed(self, data): def make_embed(self, data):
default_avatar = "https://mixer.com/_latest/assets/images/main/" "avatars/default.jpg" default_avatar = "https://mixer.com/_latest/assets/images/main/avatars/default.jpg"
user = data["user"] user = data["user"]
url = "https://mixer.com/" + data["token"] url = "https://mixer.com/" + data["token"]
embed = discord.Embed(title=data["name"], url=url) embed = discord.Embed(title=data["name"], url=url)
@@ -368,7 +362,6 @@ class MixerStream(Stream):
class PicartoStream(Stream): class PicartoStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://api.picarto.tv/v1/channel/name/" + self.name url = "https://api.picarto.tv/v1/channel/name/" + self.name
@@ -390,7 +383,7 @@ class PicartoStream(Stream):
def make_embed(self, data): def make_embed(self, data):
avatar = rnd( avatar = rnd(
"https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" "".format(data["name"].lower()) "https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg".format(data["name"].lower())
) )
url = "https://picarto.tv/" + data["name"] url = "https://picarto.tv/" + data["name"]
thumbnail = data["thumbnails"]["web"] thumbnail = data["thumbnails"]["web"]
@@ -412,5 +405,5 @@ class PicartoStream(Stream):
data["adult"] = "" data["adult"] = ""
embed.color = 0x4C90F3 embed.color = 0x4C90F3
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" "".format(**data)) embed.set_footer(text="{adult}Category: {category} | Tags: {tags}".format(**data))
return embed return embed

View File

@@ -2,7 +2,7 @@
from collections import Counter from collections import Counter
import yaml import yaml
import discord import discord
from discord.ext import commands from redbot.core import commands
from redbot.ext import trivia as ext_trivia from redbot.ext import trivia as ext_trivia
from redbot.core import Config, checks from redbot.core import Config, checks
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
@@ -18,6 +18,7 @@ UNIQUE_ID = 0xb3c0e453
class InvalidListError(Exception): class InvalidListError(Exception):
"""A Trivia list file is in invalid format.""" """A Trivia list file is in invalid format."""
pass pass
@@ -40,13 +41,12 @@ class Trivia:
self.conf.register_member(wins=0, games=0, total_score=0) self.conf.register_member(wins=0, games=0, total_score=0)
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def triviaset(self, ctx: commands.Context): async def triviaset(self, ctx: commands.Context):
"""Manage trivia settings.""" """Manage trivia settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
settings = self.conf.guild(ctx.guild) settings = self.conf.guild(ctx.guild)
settings_dict = await settings.all() settings_dict = await settings.all()
msg = box( msg = box(
@@ -81,7 +81,7 @@ class Trivia:
return return
settings = self.conf.guild(ctx.guild) settings = self.conf.guild(ctx.guild)
await settings.delay.set(seconds) await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}." "".format(seconds)) await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
@triviaset.command(name="stopafter") @triviaset.command(name="stopafter")
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float): async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
@@ -160,7 +160,7 @@ class Trivia:
return return
await settings.payout_multiplier.set(multiplier) await settings.payout_multiplier.set(multiplier)
if not multiplier: if not multiplier:
await ctx.send("Done. I will no longer reward the winner with a" " payout.") await ctx.send("Done. I will no longer reward the winner with a payout.")
return return
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier)) await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
@@ -206,7 +206,7 @@ class Trivia:
return return
if not trivia_dict: if not trivia_dict:
await ctx.send( await ctx.send(
"The trivia list was parsed successfully, however" " it appears to be empty!" "The trivia list was parsed successfully, however it appears to be empty!"
) )
return return
settings = await self.conf.guild(ctx.guild).all() settings = await self.conf.guild(ctx.guild).all()
@@ -245,13 +245,13 @@ class Trivia:
"""List available trivia categories.""" """List available trivia categories."""
lists = set(p.stem for p in self._all_lists()) lists = set(p.stem for p in self._all_lists())
msg = box("**Available trivia lists**\n\n{}" "".format(", ".join(sorted(lists)))) msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
if len(msg) > 1000: if len(msg) > 1000:
await ctx.author.send(msg) await ctx.author.send(msg)
return return
await ctx.send(msg) await ctx.send(msg)
@trivia.group(name="leaderboard", aliases=["lboard"]) @trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
async def trivia_leaderboard(self, ctx: commands.Context): async def trivia_leaderboard(self, ctx: commands.Context):
"""Leaderboard for trivia. """Leaderboard for trivia.
@@ -382,7 +382,7 @@ class Trivia:
try: try:
priority.remove(key) priority.remove(key)
except ValueError: except ValueError:
raise ValueError("{} is not a valid key".format(key)) raise ValueError("{} is not a valid key.".format(key))
# Put key last in reverse priority # Put key last in reverse priority
priority.append(key) priority.append(key)
items = data.items() items = data.items()
@@ -480,13 +480,13 @@ class Trivia:
try: try:
path = next(p for p in self._all_lists() if p.stem == category) path = next(p for p in self._all_lists() if p.stem == category)
except StopIteration: except StopIteration:
raise FileNotFoundError("Could not find the `{}` category" "".format(category)) raise FileNotFoundError("Could not find the `{}` category.".format(category))
with path.open(encoding="utf-8") as file: with path.open(encoding="utf-8") as file:
try: try:
dict_ = yaml.load(file) dict_ = yaml.load(file)
except yaml.error.YAMLError as exc: except yaml.error.YAMLError as exc:
raise InvalidListError("YAML parsing failed") from exc raise InvalidListError("YAML parsing failed.") from exc
else: else:
return dict_ return dict_

View File

@@ -69,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
check_str = inspect.getsource(checks.is_owner) check_str = inspect.getsource(checks.is_owner)
if any(inspect.getsource(x) in check_str for x in com.checks): if any(inspect.getsource(x) in check_str for x in com.checks):
# command the user specified has the is_owner check # command the user specified has the is_owner check
return None, _( return (
"That command requires bot owner. I can't " "allow you to use that for an action" None,
_("That command requires bot owner. I can't allow you to use that for an action"),
) )
return "{prefix}" + orig, None return "{prefix}" + orig, None

View File

@@ -41,31 +41,29 @@ class Warnings:
except RuntimeError: except RuntimeError:
pass pass
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: commands.Context): async def warningset(self, ctx: commands.Context):
"""Warning settings""" """Warning settings"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@warningset.command() @warningset.command()
@commands.guild_only() @commands.guild_only()
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool): async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
"""Allow or disallow custom reasons for a warning""" """Enable or Disable custom reasons for a warning"""
guild = ctx.guild guild = ctx.guild
await self.config.guild(guild).allow_custom_reasons.set(allowed) await self.config.guild(guild).allow_custom_reasons.set(allowed)
await ctx.send( await ctx.send(
_("Custom reasons have been {}").format(_("enabled") if allowed else _("disabled")) _("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
) )
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: commands.Context): async def warnaction(self, ctx: commands.Context):
"""Action management""" """Action management"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@warnaction.command(name="add") @warnaction.command(name="add")
@commands.guild_only() @commands.guild_only()
@@ -84,7 +82,7 @@ class Warnings:
try: try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30) msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await ctx.send(_("Ok then")) await ctx.send(_("Ok then."))
return return
if msg.content.lower() == "y": if msg.content.lower() == "y":
@@ -136,13 +134,12 @@ class Warnings:
else: else:
await ctx.send(_("No action named {} exists!").format(action_name)) await ctx.send(_("No action named {} exists!").format(action_name))
@commands.group() @commands.group(autohelp=True)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: commands.Context): async def warnreason(self, ctx: commands.Context):
"""Add reasons for warnings""" """Add reasons for warnings"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@warnreason.command(name="add") @warnreason.command(name="add")
@commands.guild_only() @commands.guild_only()
@@ -161,7 +158,7 @@ class Warnings:
async with guild_settings.reasons() as registered_reasons: async with guild_settings.reasons() as registered_reasons:
registered_reasons.update(completed) registered_reasons.update(completed)
await ctx.send(_("That reason has been registered")) await ctx.send(_("That reason has been registered."))
@warnreason.command(name="del") @warnreason.command(name="del")
@commands.guild_only() @commands.guild_only()
@@ -173,7 +170,7 @@ class Warnings:
if registered_reasons.pop(reason_name.lower(), None): if registered_reasons.pop(reason_name.lower(), None):
await ctx.tick() await ctx.tick()
else: else:
await ctx.send(_("That is not a registered reason name")) await ctx.send(_("That is not a registered reason name."))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@@ -230,7 +227,7 @@ class Warnings:
await ctx.send( await ctx.send(
_( _(
"Custom reasons are not allowed! Please see {} for " "Custom reasons are not allowed! Please see {} for "
"a complete list of valid reasons" "a complete list of valid reasons."
).format("`{}reasonlist`".format(ctx.prefix)) ).format("`{}reasonlist`".format(ctx.prefix))
) )
return return
@@ -275,7 +272,7 @@ class Warnings:
else: else:
if not await is_admin_or_superior(self.bot, ctx.author): if not await is_admin_or_superior(self.bot, ctx.author):
await ctx.send( await ctx.send(
warning(_("You are not allowed to check " "warnings for other users!")) warning(_("You are not allowed to check warnings for other users!"))
) )
return return
else: else:
@@ -336,7 +333,7 @@ class Warnings:
try: try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30) msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await ctx.send(_("Ok then")) await ctx.send(_("Ok then."))
return return
try: try:
int(msg.content) int(msg.content)
@@ -349,11 +346,11 @@ class Warnings:
return return
to_add["points"] = int(msg.content) to_add["points"] = int(msg.content)
await ctx.send(_("Enter a description for this reason")) await ctx.send(_("Enter a description for this reason."))
try: try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30) msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await ctx.send(_("Ok then")) await ctx.send(_("Ok then."))
return return
to_add["description"] = msg.content to_add["description"] = msg.content
return to_add return to_add

View File

@@ -4,7 +4,6 @@ __all__ = ["Config", "__version__"]
class VersionInfo: class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial): def __init__(self, major, minor, micro, releaselevel, serial):
self._levels = ["alpha", "beta", "final"] self._levels = ["alpha", "beta", "final"]
self.major = major self.major = major
@@ -37,5 +36,5 @@ class VersionInfo:
return [self.major, self.minor, self.micro, self.releaselevel, self.serial] return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
__version__ = "3.0.0b15" __version__ = "3.0.0b16"
version_info = VersionInfo(3, 0, 0, "beta", 15) version_info = VersionInfo(3, 0, 0, "beta", 16)

View File

@@ -508,9 +508,7 @@ async def set_bank_name(name: str, guild: discord.Guild = None) -> str:
elif guild is not None: elif guild is not None:
await _conf.guild(guild).bank_name.set(name) await _conf.guild(guild).bank_name.set(name)
else: else:
raise RuntimeError( raise RuntimeError("Guild must be provided if setting the name of a guild-specific bank.")
"Guild must be provided if setting the name of a guild" "-specific bank."
)
return name return name
@@ -570,7 +568,7 @@ async def set_currency_name(name: str, guild: discord.Guild = None) -> str:
await _conf.guild(guild).currency.set(name) await _conf.guild(guild).currency.set(name)
else: else:
raise RuntimeError( raise RuntimeError(
"Guild must be provided if setting the currency" " name of a guild-specific bank." "Guild must be provided if setting the currency name of a guild-specific bank."
) )
return name return name

View File

@@ -18,12 +18,13 @@ from discord.voice_client import VoiceClient
VoiceClient.warn_nacl = False VoiceClient.warn_nacl = False
from .cog_manager import CogManager from .cog_manager import CogManager
from . import Config, i18n, commands, rpc from . import Config, i18n, commands
from .rpc import RPCMixin
from .help_formatter import Help, help as help_ from .help_formatter import Help, help as help_
from .sentry import SentryManager from .sentry import SentryManager
class RedBase(BotBase): class RedBase(BotBase, RPCMixin):
"""Mixin for the main bot class. """Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which This exists because `Red` inherits from `discord.AutoShardedClient`, which
@@ -33,7 +34,7 @@ class RedBase(BotBase):
Selfbots should inherit from this mixin along with `discord.Client`. Selfbots should inherit from this mixin along with `discord.Client`.
""" """
def __init__(self, cli_flags, bot_dir: Path = Path.cwd(), **kwargs): def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
self._shutdown_mode = ExitCodes.CRITICAL self._shutdown_mode = ExitCodes.CRITICAL
self.db = Config.get_core_conf(force_registration=True) self.db = Config.get_core_conf(force_registration=True)
self._co_owners = cli_flags.co_owner self._co_owners = cli_flags.co_owner
@@ -50,6 +51,7 @@ class RedBase(BotBase):
locale="en", locale="en",
embeds=True, embeds=True,
color=15158332, color=15158332,
fuzzy=False,
help__page_char_limit=1000, help__page_char_limit=1000,
help__max_pages_in_guild=2, help__max_pages_in_guild=2,
help__tagline="", help__tagline="",
@@ -63,6 +65,7 @@ class RedBase(BotBase):
mod_role=None, mod_role=None,
embeds=None, embeds=None,
use_bot_color=False, use_bot_color=False,
fuzzy=False,
) )
self.db.register_user(embeds=None) self.db.register_user(embeds=None)
@@ -99,16 +102,13 @@ class RedBase(BotBase):
self.counter = Counter() self.counter = Counter()
self.uptime = None self.uptime = None
self.color = None self.color = discord.Embed.Empty # This is needed or color ends up 0x000000
self.main_dir = bot_dir self.main_dir = bot_dir
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),)) self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
super().__init__(formatter=Help(), **kwargs) super().__init__(*args, formatter=Help(), **kwargs)
if self.rpc_enabled:
self.rpc = rpc.RPC(self)
self.remove_command("help") self.remove_command("help")
@@ -233,12 +233,24 @@ class RedBase(BotBase):
lib_name = lib.__name__ # Thank you lib_name = lib.__name__ # Thank you
# find all references to the module # find all references to the module
cog_names = []
# remove the cogs registered from the module # remove the cogs registered from the module
for cogname, cog in self.cogs.copy().items(): for cogname, cog in self.cogs.copy().items():
if cog.__module__.startswith(lib_name): if cog.__module__.startswith(lib_name):
self.remove_cog(cogname) self.remove_cog(cogname)
cog_names.append(cogname)
# remove all rpc handlers
for cogname in cog_names:
if cogname.upper() in self.rpc_handlers:
methods = self.rpc_handlers[cogname]
for meth in methods:
self.unregister_rpc_handler(meth)
del self.rpc_handlers[cogname]
# first remove all the commands from the module # first remove all the commands from the module
for cmd in self.all_commands.copy().values(): for cmd in self.all_commands.copy().values():
if cmd.module.startswith(lib_name): if cmd.module.startswith(lib_name):

View File

@@ -1,5 +1,5 @@
import discord import discord
from discord.ext import commands from redbot.core import commands
async def check_overrides(ctx, *, level): async def check_overrides(ctx, *, level):
@@ -16,7 +16,6 @@ async def check_overrides(ctx, *, level):
def is_owner(**kwargs): def is_owner(**kwargs):
async def check(ctx): async def check(ctx):
override = await check_overrides(ctx, level="owner") override = await check_overrides(ctx, level="owner")
return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs) return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs)
@@ -73,7 +72,6 @@ async def is_admin_or_superior(ctx):
def mod_or_permissions(**perms): def mod_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
override = await check_overrides(ctx, level="mod") override = await check_overrides(ctx, level="mod")
return ( return (
@@ -86,7 +84,6 @@ def mod_or_permissions(**perms):
def admin_or_permissions(**perms): def admin_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
override = await check_overrides(ctx, level="admin") override = await check_overrides(ctx, level="admin")
return ( return (
@@ -99,7 +96,6 @@ def admin_or_permissions(**perms):
def bot_in_a_guild(**kwargs): def bot_in_a_guild(**kwargs):
async def predicate(ctx): async def predicate(ctx):
return len(ctx.bot.guilds) > 0 return len(ctx.bot.guilds) > 0
@@ -107,7 +103,6 @@ def bot_in_a_guild(**kwargs):
def guildowner_or_permissions(**perms): def guildowner_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
has_perms_or_is_owner = await check_permissions(ctx, perms) has_perms_or_is_owner = await check_permissions(ctx, perms)
if ctx.guild is None: if ctx.guild is None:

View File

@@ -36,7 +36,7 @@ def interactive_config(red, token_set, prefix_set):
while not prefix: while not prefix:
prefix = input("Prefix> ") prefix = input("Prefix> ")
if len(prefix) > 10: if len(prefix) > 10:
print("Your prefix seems overly long. Are you sure it " "is correct? (y/n)") print("Your prefix seems overly long. Are you sure that it's correct? (y/n)")
if not confirm("> "): if not confirm("> "):
prefix = "" prefix = ""
if prefix: if prefix:
@@ -72,7 +72,7 @@ def parse_cli_flags(args):
parser.add_argument( parser.add_argument(
"--list-instances", "--list-instances",
action="store_true", action="store_true",
help="List all instance names setup " "with 'redbot-setup'", help="List all instance names setup with 'redbot-setup'",
) )
parser.add_argument( parser.add_argument(
"--owner", "--owner",
@@ -117,7 +117,7 @@ def parse_cli_flags(args):
parser.add_argument( parser.add_argument(
"--not-bot", "--not-bot",
action="store_true", action="store_true",
help="Specifies if the token used belongs to a bot " "account.", help="Specifies if the token used belongs to a bot account.",
) )
parser.add_argument( parser.add_argument(
"--dry-run", "--dry-run",
@@ -131,12 +131,12 @@ def parse_cli_flags(args):
parser.add_argument( parser.add_argument(
"--mentionable", "--mentionable",
action="store_true", action="store_true",
help="Allows mentioning the bot as an alternative " "to using the bot prefix", help="Allows mentioning the bot as an alternative to using the bot prefix",
) )
parser.add_argument( parser.add_argument(
"--rpc", "--rpc",
action="store_true", action="store_true",
help="Enables the built-in RPC server. Please read the docs" "prior to enabling this!", help="Enables the built-in RPC server. Please read the docs prior to enabling this!",
) )
parser.add_argument( parser.add_argument(
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`." "instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."

View File

@@ -250,7 +250,7 @@ class CogManager:
mod = import_module(real_name, package="redbot.cogs") mod = import_module(real_name, package="redbot.cogs")
except ImportError as e: except ImportError as e:
raise RuntimeError( raise RuntimeError(
"No core cog by the name of '{}' could" "be found.".format(name) "No core cog by the name of '{}' could be found.".format(name)
) from e ) from e
return mod.__spec__ return mod.__spec__
@@ -342,9 +342,7 @@ class CogManagerUI:
Add a path to the list of available cog paths. Add a path to the list of available cog paths.
""" """
if not path.is_dir(): if not path.is_dir():
await ctx.send( await ctx.send(_("That path does not exist or does not point to a valid directory."))
_("That path does not exist or does not" " point to a valid directory.")
)
return return
try: try:
@@ -419,7 +417,7 @@ class CogManagerUI:
install_path = await ctx.bot.cog_mgr.install_path() install_path = await ctx.bot.cog_mgr.install_path()
await ctx.send( await ctx.send(
_("The bot will install new cogs to the `{}`" " directory.").format(install_path) _("The bot will install new cogs to the `{}` directory.").format(install_path)
) )
@commands.command() @commands.command()

View File

@@ -2,3 +2,4 @@
from discord.ext.commands import * from discord.ext.commands import *
from .commands import * from .commands import *
from .context import * from .context import *
from .errors import *

View File

@@ -4,12 +4,20 @@ This module contains extended classes and functions which are intended to
replace those from the `discord.ext.commands` module. replace those from the `discord.ext.commands` module.
""" """
import inspect import inspect
from typing import TYPE_CHECKING
from discord.ext import commands from discord.ext import commands
from .errors import ConversionFailure
from ..i18n import Translator
if TYPE_CHECKING:
from .context import Context
__all__ = ["Command", "Group", "command", "group"] __all__ = ["Command", "Group", "command", "group"]
_ = Translator("commands.commands", __file__)
class Command(commands.Command): class Command(commands.Command):
"""Command class for Red. """Command class for Red.
@@ -48,6 +56,78 @@ class Command(commands.Command):
# We don't want our help property to be overwritten, namely by super() # We don't want our help property to be overwritten, namely by super()
pass pass
@property
def parents(self):
"""
Returns all parent commands of this command.
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
If the command has no parents, this will be an empty list.
"""
cmd = self.parent
entries = []
while cmd is not None:
entries.append(cmd)
cmd = cmd.parent
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
async def do_conversion(self, ctx: "Context", converter, argument: str):
"""Convert an argument according to its type annotation.
Raises
------
ConversionFailure
If doing the conversion failed.
Returns
-------
Any
The converted argument.
"""
# Let's not worry about all of this junk if it's just a str converter
if converter is str:
return argument
try:
return await super().do_conversion(ctx, converter, argument)
except commands.BadArgument as exc:
raise ConversionFailure(converter, argument, *exc.args) from exc
except ValueError as exc:
# Some common converters need special treatment...
if converter in (int, float):
message = _('"{argument}" is not a number.').format(argument=argument)
raise ConversionFailure(converter, argument, message) from exc
# We should expose anything which might be a bug in the converter
raise exc
def command(self, cls=None, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.command` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
cls = cls or self.__class__
def decorator(func):
result = command(*args, **kwargs)(func)
self.add_command(result)
return result
return decorator
def group(self, cls=None, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
cls = None or Group
def decorator(func):
result = group(*args, **kwargs)(func)
self.add_command(result)
return result
return decorator
class Group(Command, commands.Group): class Group(Command, commands.Group):
"""Group command class for Red. """Group command class for Red.
@@ -55,7 +135,28 @@ class Group(Command, commands.Group):
This class inherits from `discord.ext.commands.Group`, with `Command` mixed This class inherits from `discord.ext.commands.Group`, with `Command` mixed
in. in.
""" """
pass
def __init__(self, *args, **kwargs):
self.autohelp = kwargs.pop("autohelp", False)
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 ctx.send_help()
await super().invoke(ctx)
# decorators # decorators

View 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)

View File

@@ -1,15 +1,13 @@
import logging import logging
import collections import collections
from copy import deepcopy from copy import deepcopy
from typing import Union, Tuple from typing import Union, Tuple, TYPE_CHECKING
import discord import discord
from .data_manager import cog_data_path, core_data_path from .data_manager import cog_data_path, core_data_path
from .drivers import get_driver from .drivers import get_driver
from .utils import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .drivers.red_base import BaseDriver from .drivers.red_base import BaseDriver
@@ -225,7 +223,7 @@ class Group(Value):
identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
) )
elif self.force_registration: elif self.force_registration:
raise AttributeError("'{}' is not a valid registered Group " "or value.".format(item)) raise AttributeError("'{}' is not a valid registered Group or value.".format(item))
else: else:
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver) return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
@@ -441,6 +439,7 @@ class Config:
attempting to access data. attempting to access data.
""" """
GLOBAL = "GLOBAL" GLOBAL = "GLOBAL"
GUILD = "GUILD" GUILD = "GUILD"
CHANNEL = "TEXTCHANNEL" CHANNEL = "TEXTCHANNEL"
@@ -624,9 +623,7 @@ class Config:
existing_is_dict = isinstance(_partial[k], dict) existing_is_dict = isinstance(_partial[k], dict)
if val_is_dict != existing_is_dict: if val_is_dict != existing_is_dict:
# != is XOR # != is XOR
raise KeyError( raise KeyError("You cannot register a Group and a Value under the same name.")
"You cannot register a Group and a Value under" " the same name."
)
if val_is_dict: if val_is_dict:
Config._update_defaults(v, _partial=_partial[k]) Config._update_defaults(v, _partial=_partial[k])
else: else:

View File

@@ -13,6 +13,7 @@ from pathlib import Path
from random import SystemRandom from random import SystemRandom
from string import ascii_letters, digits from string import ascii_letters, digits
from distutils.version import StrictVersion from distutils.version import StrictVersion
from typing import TYPE_CHECKING
import aiohttp import aiohttp
import discord import discord
@@ -21,9 +22,7 @@ import pkg_resources
from redbot.core import __version__ from redbot.core import __version__
from redbot.core import checks from redbot.core import checks
from redbot.core import i18n from redbot.core import i18n
from redbot.core import rpc
from redbot.core import commands from redbot.core import commands
from .utils import TYPE_CHECKING
from .utils.chat_formatting import pagify, box, inline from .utils.chat_formatting import pagify, box, inline
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -44,12 +43,201 @@ OWNER_DISCLAIMER = (
_ = i18n.Translator("Core", __file__) _ = i18n.Translator("Core", __file__)
class CoreLogic:
def __init__(self, bot: "Red"):
self.bot = bot
self.bot.register_rpc_handler(self._load)
self.bot.register_rpc_handler(self._unload)
self.bot.register_rpc_handler(self._reload)
self.bot.register_rpc_handler(self._name)
self.bot.register_rpc_handler(self._prefixes)
self.bot.register_rpc_handler(self._version_info)
self.bot.register_rpc_handler(self._invite_url)
async def _load(self, cog_names: list):
"""
Loads cogs by name.
Parameters
----------
cog_names : list of str
Returns
-------
tuple
3 element tuple of loaded, failed, and not found cogs.
"""
failed_packages = []
loaded_packages = []
notfound_packages = []
bot = self.bot
cogspecs = []
for name in cog_names:
try:
spec = await bot.cog_mgr.find_cog(name)
cogspecs.append((spec, name))
except RuntimeError:
notfound_packages.append(name)
for spec, name in cogspecs:
try:
self._cleanup_and_refresh_modules(spec.name)
await bot.load_extension(spec)
except Exception as e:
log.exception("Package loading failed", exc_info=e)
exception_log = "Exception during loading of cog\n"
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
bot._last_exception = exception_log
failed_packages.append(name)
else:
await bot.add_loaded_package(name)
loaded_packages.append(name)
return loaded_packages, failed_packages, notfound_packages
def _cleanup_and_refresh_modules(self, module_name: str):
"""Interally reloads modules so that changes are detected"""
splitted = module_name.split(".")
def maybe_reload(new_name):
try:
lib = sys.modules[new_name]
except KeyError:
pass
else:
importlib._bootstrap._exec(lib.__spec__, lib)
modules = itertools.accumulate(splitted, "{}.{}".format)
for m in modules:
maybe_reload(m)
children = {name: lib for name, lib in sys.modules.items() if name.startswith(module_name)}
for child_name, lib in children.items():
importlib._bootstrap._exec(lib.__spec__, lib)
def _get_package_strings(self, packages: list, fmt: str, other: tuple = None):
"""
Gets the strings needed for the load, unload and reload commands
"""
packages = [inline(name) for name in packages]
if other is None:
other = ("", "")
plural = "s" if len(packages) > 1 else ""
use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
form = {"plural": plural, "packs": packages_string, "other": other}
final_string = fmt.format(**form)
return final_string
async def _unload(self, cog_names: list):
"""
Unloads cogs with the given names.
Parameters
----------
cog_names : list of str
Returns
-------
tuple
2 element tuple of successful unloads and failed unloads.
"""
failed_packages = []
unloaded_packages = []
bot = self.bot
for name in cog_names:
if name in bot.extensions:
bot.unload_extension(name)
await bot.remove_loaded_package(name)
unloaded_packages.append(name)
else:
failed_packages.append(name)
return unloaded_packages, failed_packages
async def _reload(self, cog_names):
await self._unload(cog_names)
loaded, load_failed, not_found = await self._load(cog_names)
return loaded, load_failed, not_found
async def _name(self, name: str = None):
"""
Gets or sets the bot's username.
Parameters
----------
name : str
If passed, the bot will change it's username.
Returns
-------
str
The current (or new) username of the bot.
"""
if name is not None:
await self.bot.user.edit(username=name)
return self.bot.user.name
async def _prefixes(self, prefixes: list = None):
"""
Gets or sets the bot's global prefixes.
Parameters
----------
prefixes : list of str
If passed, the bot will set it's global prefixes.
Returns
-------
list of str
The current (or new) list of prefixes.
"""
if prefixes:
prefixes = sorted(prefixes, reverse=True)
await self.bot.db.prefix.set(prefixes)
return await self.bot.db.prefix()
async def _version_info(self):
"""
Version information for Red and discord.py
Returns
-------
dict
`redbot` and `discordpy` keys containing version information for both.
"""
return {"redbot": __version__, "discordpy": discord.__version__}
async def _invite_url(self):
"""
Generates the invite URL for the bot.
Returns
-------
str
Invite URL.
"""
if self.bot.user.bot:
app_info = await self.bot.application_info()
return discord.utils.oauth_url(app_info.id)
return "Not a bot account!"
@i18n.cog_i18n(_) @i18n.cog_i18n(_)
class Core: class Core(CoreLogic):
"""Commands related to core functions""" """Commands related to core functions"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot # type: Red super().__init__(bot)
@commands.command(hidden=True) @commands.command(hidden=True)
async def ping(self, ctx): async def ping(self, ctx):
@@ -99,7 +287,7 @@ class Core:
embed.add_field(name="About Red", value=about, inline=False) embed.add_field(name="About Red", value=about, inline=False)
embed.set_footer( embed.set_footer(
text="Bringing joy since 02 Jan 2016 (over " "{} days ago!)".format(days_since) text="Bringing joy since 02 Jan 2016 (over {} days ago!)".format(days_since)
) )
try: try:
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -133,7 +321,7 @@ class Core:
return fmt.format(d=days, h=hours, m=minutes, s=seconds) return fmt.format(d=days, h=hours, m=minutes, s=seconds)
@commands.group() @commands.group(autohelp=True)
async def embedset(self, ctx: commands.Context): async def embedset(self, ctx: commands.Context):
""" """
Commands for toggling embeds on or off. Commands for toggling embeds on or off.
@@ -153,7 +341,6 @@ class Core:
user_setting = await self.bot.db.user(ctx.author).embeds() user_setting = await self.bot.db.user(ctx.author).embeds()
text += "User setting: {}".format(user_setting) text += "User setting: {}".format(user_setting)
await ctx.send(box(text)) await ctx.send(box(text))
await ctx.send_help()
@embedset.command(name="global") @embedset.command(name="global")
@checks.is_owner() @checks.is_owner()
@@ -236,8 +423,7 @@ class Core:
async def invite(self, ctx): async def invite(self, ctx):
"""Show's Red's invite url""" """Show's Red's invite url"""
if self.bot.user.bot: if self.bot.user.bot:
app_info = await self.bot.application_info() await ctx.author.send(await self._invite_url())
await ctx.author.send(discord.utils.oauth_url(app_info.id))
else: else:
await ctx.send("I'm not a bot account. I have no invite URL.") await ctx.send("I'm not a bot account. I have no invite URL.")
@@ -249,7 +435,7 @@ class Core:
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
await ctx.send("Are you sure you want me to leave this server?" " Type yes to confirm.") await ctx.send("Are you sure you want me to leave this server? Type yes to confirm.")
def conf_check(m): def conf_check(m):
return m.author == author return m.author == author
@@ -319,149 +505,70 @@ class Core:
async def load(self, ctx, *, cog_name: str): async def load(self, ctx, *, cog_name: str):
"""Loads packages""" """Loads packages"""
failed_packages = [] cog_names = [c.strip() for c in cog_name.split(" ")]
loaded_packages = [] async with ctx.typing():
notfound_packages = [] loaded, failed, not_found = await self._load(cog_names)
cognames = [c.strip() for c in cog_name.split(" ")] if loaded:
cogspecs = []
for c in cognames:
try:
spec = await ctx.bot.cog_mgr.find_cog(c)
cogspecs.append((spec, c))
except RuntimeError:
notfound_packages.append(inline(c))
# await ctx.send(_("No module named '{}' was found in any"
# " cog path.").format(c))
if len(cogspecs) > 0:
for spec, name in cogspecs:
try:
await ctx.bot.load_extension(spec)
except Exception as e:
log.exception("Package loading failed", exc_info=e)
exception_log = "Exception in command '{}'\n" "".format(
ctx.command.qualified_name
)
exception_log += "".join(
traceback.format_exception(type(e), e, e.__traceback__)
)
self.bot._last_exception = exception_log
failed_packages.append(inline(name))
else:
await ctx.bot.add_loaded_package(name)
loaded_packages.append(inline(name))
if loaded_packages:
fmt = "Loaded {packs}" fmt = "Loaded {packs}"
formed = self.get_package_strings(loaded_packages, fmt) formed = self._get_package_strings(loaded, fmt)
await ctx.send(_(formed)) await ctx.send(formed)
if failed_packages: if failed:
fmt = ( fmt = (
"Failed to load package{plural} {packs}. Check your console or " "Failed to load package{plural} {packs}. Check your console or "
"logs for details." "logs for details."
) )
formed = self.get_package_strings(failed_packages, fmt) formed = self._get_package_strings(failed, fmt)
await ctx.send(_(formed)) await ctx.send(formed)
if notfound_packages: if not_found:
fmt = "The package{plural} {packs} {other} not found in any cog path." fmt = "The package{plural} {packs} {other} not found in any cog path."
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were")) formed = self._get_package_strings(not_found, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(formed)
@commands.group() @commands.command()
@checks.is_owner() @checks.is_owner()
async def unload(self, ctx, *, cog_name: str): async def unload(self, ctx, *, cog_name: str):
"""Unloads packages""" """Unloads packages"""
cognames = [c.strip() for c in cog_name.split(" ")]
failed_packages = []
unloaded_packages = []
for c in cognames: cog_names = [c.strip() for c in cog_name.split(" ")]
if c in ctx.bot.extensions:
ctx.bot.unload_extension(c)
await ctx.bot.remove_loaded_package(c)
unloaded_packages.append(inline(c))
else:
failed_packages.append(inline(c))
if unloaded_packages: unloaded, failed = await self._unload(cog_names)
if unloaded:
fmt = "Package{plural} {packs} {other} unloaded." fmt = "Package{plural} {packs} {other} unloaded."
formed = self.get_package_strings(unloaded_packages, fmt, ("was", "were")) formed = self._get_package_strings(unloaded, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(_(formed))
if failed_packages: if failed:
fmt = "The package{plural} {packs} {other} not loaded." fmt = "The package{plural} {packs} {other} not loaded."
formed = self.get_package_strings(failed_packages, fmt, ("is", "are")) formed = self._get_package_strings(failed, fmt, ("is", "are"))
await ctx.send(_(formed)) await ctx.send(formed)
@commands.command(name="reload") @commands.command(name="reload")
@checks.is_owner() @checks.is_owner()
async def _reload(self, ctx, *, cog_name: str): async def reload_(self, ctx, *, cog_name: str):
"""Reloads packages""" """Reloads packages"""
cognames = [c.strip() for c in cog_name.split(" ")] cog_names = [c.strip() for c in cog_name.split(" ")]
async with ctx.typing():
loaded, failed, not_found = await self._reload(cog_names)
for c in cognames: if loaded:
ctx.bot.unload_extension(c)
cogspecs = []
failed_packages = []
loaded_packages = []
notfound_packages = []
for c in cognames:
try:
spec = await ctx.bot.cog_mgr.find_cog(c)
cogspecs.append((spec, c))
except RuntimeError:
notfound_packages.append(inline(c))
for spec, name in cogspecs:
try:
self.cleanup_and_refresh_modules(spec.name)
await ctx.bot.load_extension(spec)
loaded_packages.append(inline(name))
except Exception as e:
log.exception("Package reloading failed", exc_info=e)
exception_log = "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
self.bot._last_exception = exception_log
failed_packages.append(inline(name))
if loaded_packages:
fmt = "Package{plural} {packs} {other} reloaded." fmt = "Package{plural} {packs} {other} reloaded."
formed = self.get_package_strings(loaded_packages, fmt, ("was", "were")) formed = self._get_package_strings(loaded, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(formed)
if failed_packages: if failed:
fmt = "Failed to reload package{plural} {packs}. Check your " "logs for details" fmt = "Failed to reload package{plural} {packs}. Check your logs for details"
formed = self.get_package_strings(failed_packages, fmt) formed = self._get_package_strings(failed, fmt)
await ctx.send(_(formed)) await ctx.send(formed)
if notfound_packages: if not_found:
fmt = "The package{plural} {packs} {other} not found in any cog path." fmt = "The package{plural} {packs} {other} not found in any cog path."
formed = self.get_package_strings(notfound_packages, fmt, ("was", "were")) formed = self._get_package_strings(not_found, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(formed)
def get_package_strings(self, packages: list, fmt: str, other: tuple = None):
"""
Gets the strings needed for the load, unload and reload commands
"""
if other is None:
other = ("", "")
plural = "s" if len(packages) > 1 else ""
use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
form = {"plural": plural, "packs": packages_string, "other": other}
final_string = fmt.format(**form)
return final_string
@commands.command(name="shutdown") @commands.command(name="shutdown")
@checks.is_owner() @checks.is_owner()
@@ -491,55 +598,32 @@ class Core:
pass pass
await ctx.bot.shutdown(restart=True) await ctx.bot.shutdown(restart=True)
def cleanup_and_refresh_modules(self, module_name: str): @commands.group(name="set", autohelp=True)
"""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): async def _set(self, ctx):
"""Changes Red's settings""" """Changes Red's settings"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role() if ctx.guild:
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id) admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role() admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id) or "Not set"
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id) mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
prefixes = await ctx.bot.db.guild(ctx.guild).prefix() mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id) or "Not set"
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
guild_settings = f"Admin role: {admin_role}\nMod role: {mod_role}\n"
else:
guild_settings = ""
prefixes = None # This is correct. The below can happen in a guild.
if not prefixes: if not prefixes:
prefixes = await ctx.bot.db.prefix() prefixes = await ctx.bot.db.prefix()
locale = await ctx.bot.db.locale() locale = await ctx.bot.db.locale()
prefix_string = " ".join(prefixes)
settings = ( settings = (
"{} Settings:\n\n" f"{ctx.bot.user.name} Settings:\n\n"
"Prefixes: {}\n" f"Prefixes: {prefix_string}\n"
"Admin role: {}\n" f"{guild_settings}"
"Mod role: {}\n" f"Locale: {locale}"
"Locale: {}"
"".format(
ctx.bot.user.name,
" ".join(prefixes),
admin_role.name if admin_role else "Not set",
mod_role.name if mod_role else "Not set",
locale,
)
) )
await ctx.send(box(settings)) await ctx.send(box(settings))
await ctx.send_help()
@_set.command() @_set.command()
@checks.guildowner() @checks.guildowner()
@@ -564,7 +648,7 @@ class Core:
""" """
Toggle whether to use the bot owner-configured colour for embeds. Toggle whether to use the bot owner-configured colour for embeds.
Default is to not use the bot's configured colour, in which case the Default is to not use the bot's configured colour, in which case the
colour used will be the colour of the bot's top role. colour used will be the colour of the bot's top role.
""" """
current_setting = await ctx.bot.db.guild(ctx.guild).use_bot_color() current_setting = await ctx.bot.db.guild(ctx.guild).use_bot_color()
@@ -575,6 +659,39 @@ class Core:
) )
) )
@_set.command()
@checks.guildowner()
@commands.guild_only()
async def serverfuzzy(self, ctx):
"""
Toggle whether to enable fuzzy command search for the server.
Default is for fuzzy command search to be disabled.
"""
current_setting = await ctx.bot.db.guild(ctx.guild).fuzzy()
await ctx.bot.db.guild(ctx.guild).fuzzy.set(not current_setting)
await ctx.send(
_("Fuzzy command search has been {} for this server.").format(
_("disabled") if current_setting else _("enabled")
)
)
@_set.command()
@checks.is_owner()
async def fuzzy(self, ctx):
"""
Toggle whether to enable fuzzy command search in DMs.
Default is for fuzzy command search to be disabled.
"""
current_setting = await ctx.bot.db.fuzzy()
await ctx.bot.db.fuzzy.set(not current_setting)
await ctx.send(
_("Fuzzy command search has been {} in DMs.").format(
_("disabled") if current_setting else _("enabled")
)
)
@_set.command(aliases=["color"]) @_set.command(aliases=["color"])
@checks.is_owner() @checks.is_owner()
async def colour(self, ctx, *, colour: discord.Colour = None): async def colour(self, ctx, *, colour: discord.Colour = None):
@@ -714,7 +831,7 @@ class Core:
async def _username(self, ctx, *, username: str): async def _username(self, ctx, *, username: str):
"""Sets Red's username""" """Sets Red's username"""
try: try:
await ctx.bot.user.edit(username=username) await self._name(name=username)
except discord.HTTPException: except discord.HTTPException:
await ctx.send( await ctx.send(
_( _(
@@ -735,7 +852,7 @@ class Core:
try: try:
await ctx.guild.me.edit(nick=nickname) await ctx.guild.me.edit(nick=nickname)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("I lack the permissions to change my own " "nickname.")) await ctx.send(_("I lack the permissions to change my own nickname."))
else: else:
await ctx.send("Done.") await ctx.send("Done.")
@@ -746,8 +863,7 @@ class Core:
if not prefixes: if not prefixes:
await ctx.send_help() await ctx.send_help()
return return
prefixes = sorted(prefixes, reverse=True) await self._prefixes(prefixes)
await ctx.bot.db.prefix.set(prefixes)
await ctx.send(_("Prefix set.")) await ctx.send(_("Prefix set."))
@_set.command(aliases=["serverprefixes"]) @_set.command(aliases=["serverprefixes"])
@@ -779,7 +895,7 @@ class Core:
for i in range(length): for i in range(length):
token += random.choice(chars) token += random.choice(chars)
log.info("{0} ({0.id}) requested to be set as owner." "".format(ctx.author)) log.info("{0} ({0.id}) requested to be set as owner.".format(ctx.author))
print(_("\nVerification token:")) print(_("\nVerification token:"))
print(token) print(token)
@@ -865,12 +981,11 @@ class Core:
ctx.bot.disable_sentry() ctx.bot.disable_sentry()
await ctx.send(_("Done. Sentry logging is now disabled.")) await ctx.send(_("Done. Sentry logging is now disabled."))
@commands.group() @commands.group(autohelp=True)
@checks.is_owner() @checks.is_owner()
async def helpset(self, ctx: commands.Context): async def helpset(self, ctx: commands.Context):
"""Manage settings for the help command.""" """Manage settings for the help command."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@helpset.command(name="pagecharlimit") @helpset.command(name="pagecharlimit")
async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int): async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int):
@@ -915,7 +1030,7 @@ class Core:
""" """
Set the tagline to be used. Set the tagline to be used.
This setting only applies to embedded help. If no tagline is This setting only applies to embedded help. If no tagline is
specified, the default will be used instead. specified, the default will be used instead.
""" """
if tagline is None: if tagline is None:
@@ -998,10 +1113,8 @@ class Core:
if downloader_cog and hasattr(downloader_cog, "_repo_manager"): if downloader_cog and hasattr(downloader_cog, "_repo_manager"):
repo_output = [] repo_output = []
repo_mgr = downloader_cog._repo_manager repo_mgr = downloader_cog._repo_manager
for n, repo in repo_mgr._repos: for repo in repo_mgr._repos.values():
repo_output.append( repo_output.append({"url": repo.url, "name": repo.name, "branch": repo.branch})
{{"url": repo.url, "name": repo.name, "branch": repo.branch}}
)
repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json" repo_filename = data_dir / "cogs" / "RepoManager" / "repos.json"
with open(str(repo_filename), "w") as f: with open(str(repo_filename), "w") as f:
f.write(json.dumps(repo_output, indent=4)) f.write(json.dumps(repo_output, indent=4))
@@ -1058,7 +1171,7 @@ class Core:
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None)) prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
prefix = prefixes[0] prefix = prefixes[0]
content = _("Use `{}dm {} <text>` to reply to this user" "").format(prefix, author.id) content = _("Use `{}dm {} <text>` to reply to this user").format(prefix, author.id)
description = _("Sent by {} {}").format(author, source) description = _("Sent by {} {}").format(author, source)
@@ -1079,7 +1192,7 @@ class Core:
await owner.send(content, embed=e) await owner.send(content, embed=e)
except discord.InvalidArgument: except discord.InvalidArgument:
await ctx.send( await ctx.send(
_("I cannot send your message, I'm unable to find " "my owner... *sigh*") _("I cannot send your message, I'm unable to find my owner... *sigh*")
) )
except: except:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
@@ -1091,7 +1204,7 @@ class Core:
await owner.send("{}\n{}".format(content, box(msg_text))) await owner.send("{}\n{}".format(content, box(msg_text)))
except discord.InvalidArgument: except discord.InvalidArgument:
await ctx.send( await ctx.send(
_("I cannot send your message, I'm unable to find " "my owner... *sigh*") _("I cannot send your message, I'm unable to find my owner... *sigh*")
) )
except: except:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
@@ -1136,7 +1249,7 @@ class Core:
await destination.send(embed=e) await destination.send(embed=e)
except: except:
await ctx.send( await ctx.send(
_("Sorry, I couldn't deliver your message " "to {}").format(destination) _("Sorry, I couldn't deliver your message to {}").format(destination)
) )
else: else:
await ctx.send(_("Message delivered to {}").format(destination)) await ctx.send(_("Message delivered to {}").format(destination))
@@ -1146,19 +1259,18 @@ class Core:
await destination.send("{}\n{}".format(box(response), content)) await destination.send("{}\n{}".format(box(response), content))
except: except:
await ctx.send( await ctx.send(
_("Sorry, I couldn't deliver your message " "to {}").format(destination) _("Sorry, I couldn't deliver your message to {}").format(destination)
) )
else: else:
await ctx.send(_("Message delivered to {}").format(destination)) await ctx.send(_("Message delivered to {}").format(destination))
@commands.group() @commands.group(autohelp=True)
@checks.is_owner() @checks.is_owner()
async def whitelist(self, ctx): async def whitelist(self, ctx):
""" """
Whitelist management commands. Whitelist management commands.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@whitelist.command(name="add") @whitelist.command(name="add")
async def whitelist_add(self, ctx, user: discord.User): async def whitelist_add(self, ctx, user: discord.User):
@@ -1180,7 +1292,7 @@ class Core:
msg = _("Whitelisted Users:") msg = _("Whitelisted Users:")
for user in curr_list: for user in curr_list:
msg.append("\n\t- {}".format(user)) msg += "\n\t- {}".format(user)
for page in pagify(msg): for page in pagify(msg):
await ctx.send(box(page)) await ctx.send(box(page))
@@ -1210,14 +1322,13 @@ class Core:
await ctx.bot.db.whitelist.set([]) await ctx.bot.db.whitelist.set([])
await ctx.send(_("Whitelist has been cleared.")) await ctx.send(_("Whitelist has been cleared."))
@commands.group() @commands.group(autohelp=True)
@checks.is_owner() @checks.is_owner()
async def blacklist(self, ctx): async def blacklist(self, ctx):
""" """
blacklist management commands. blacklist management commands.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@blacklist.command(name="add") @blacklist.command(name="add")
async def blacklist_add(self, ctx, user: discord.User): async def blacklist_add(self, ctx, user: discord.User):
@@ -1243,7 +1354,7 @@ class Core:
msg = _("blacklisted Users:") msg = _("blacklisted Users:")
for user in curr_list: for user in curr_list:
msg.append("\n\t- {}".format(user)) msg += "\n\t- {}".format(user)
for page in pagify(msg): for page in pagify(msg):
await ctx.send(box(page)) await ctx.send(box(page))
@@ -1273,6 +1384,177 @@ class Core:
await ctx.bot.db.blacklist.set([]) await ctx.bot.db.blacklist.set([])
await ctx.send(_("blacklist has been cleared.")) await ctx.send(_("blacklist has been cleared."))
@commands.group()
@commands.guild_only()
@checks.admin_or_permissions(administrator=True)
async def localwhitelist(self, ctx):
"""
Whitelist management commands.
"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
@localwhitelist.command(name="add")
async def localwhitelist_add(self, ctx, *, user_or_role: str):
"""
Adds a user or role to the whitelist.
"""
try:
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
if obj.id not in curr_list:
curr_list.append(obj.id)
if user:
await ctx.send(_("User added to whitelist."))
else:
await ctx.send(_("Role added to whitelist."))
@localwhitelist.command(name="list")
async def localwhitelist_list(self, ctx):
"""
Lists whitelisted users and roles.
"""
curr_list = await ctx.bot.db.guild(ctx.guild).whitelist()
msg = _("Whitelisted Users and roles:")
for obj in curr_list:
msg += "\n\t- {}".format(obj)
for page in pagify(msg):
await ctx.send(box(page))
@localwhitelist.command(name="remove")
async def localwhitelist_remove(self, ctx, *, user_or_role: str):
"""
Removes user or role from whitelist.
"""
try:
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
removed = False
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
if obj.id in curr_list:
removed = True
curr_list.remove(obj.id)
if removed:
if user:
await ctx.send(_("User has been removed from whitelist."))
else:
await ctx.send(_("Role has been removed from whitelist."))
else:
if user:
await ctx.send(_("User was not in the whitelist."))
else:
await ctx.send(_("Role was not in the whitelist."))
@localwhitelist.command(name="clear")
async def localwhitelist_clear(self, ctx):
"""
Clears the whitelist.
"""
await ctx.bot.db.guild(ctx.guild).whitelist.set([])
await ctx.send(_("Whitelist has been cleared."))
@commands.group()
@commands.guild_only()
@checks.admin_or_permissions(administrator=True)
async def localblacklist(self, ctx):
"""
blacklist management commands.
"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
@localblacklist.command(name="add")
async def localblacklist_add(self, ctx, *, user_or_role: str):
"""
Adds a user or role to the blacklist.
"""
try:
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
if user and await ctx.bot.is_owner(obj):
ctx.send(_("You cannot blacklist an owner!"))
return
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
if obj.id not in curr_list:
curr_list.append(obj.id)
if user:
await ctx.send(_("User added to blacklist."))
else:
await ctx.send(_("Role added to blacklist."))
@localblacklist.command(name="list")
async def localblacklist_list(self, ctx):
"""
Lists blacklisted users and roles.
"""
curr_list = await ctx.bot.db.guild(ctx.guild).blacklist()
msg = _("blacklisted Users and Roles:")
for obj in curr_list:
msg += "\n\t- {}".format(obj)
for page in pagify(msg):
await ctx.send(box(page))
@localblacklist.command(name="remove")
async def localblacklist_remove(self, ctx, *, user_or_role: str):
"""
Removes user or role from blacklist.
"""
removed = False
try:
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
if obj.id in curr_list:
removed = True
curr_list.remove(obj.id)
if removed:
if user:
await ctx.send(_("User has been removed from blacklist."))
else:
await ctx.send(_("Role has been removed from blacklist."))
else:
if user:
await ctx.send(_("User was not in the blacklist."))
else:
await ctx.send(_("Role was not in the blacklist."))
@localblacklist.command(name="clear")
async def localblacklist_clear(self, ctx):
"""
Clears the blacklist.
"""
await ctx.bot.db.guild(ctx.guild).blacklist.set([])
await ctx.send(_("blacklist has been cleared."))
# RPC handlers # RPC handlers
async def rpc_load(self, request): async def rpc_load(self, request):
cog_name = request.params[0] cog_name = request.params[0]
@@ -1281,7 +1563,7 @@ class Core:
if spec is None: if spec is None:
raise LookupError("No such cog found.") raise LookupError("No such cog found.")
self.cleanup_and_refresh_modules(spec.name) self._cleanup_and_refresh_modules(spec.name)
self.bot.load_extension(spec) self.bot.load_extension(spec)

View File

@@ -9,10 +9,6 @@ import logging
import appdirs import appdirs
from .json_io import JsonIO from .json_io import JsonIO
from .utils import TYPE_CHECKING
if TYPE_CHECKING:
from . import Config
__all__ = [ __all__ = [
"load_basic_configuration", "load_basic_configuration",
@@ -78,9 +74,7 @@ def load_basic_configuration(instance_name_: str):
def _base_data_path() -> Path: def _base_data_path() -> Path:
if basic_config is None: if basic_config is None:
raise RuntimeError( raise RuntimeError("You must load the basic config before you can get the base data path.")
"You must load the basic config before you" " can get the base data path."
)
path = basic_config["DATA_PATH"] path = basic_config["DATA_PATH"]
return Path(path).resolve() return Path(path).resolve()
@@ -110,7 +104,7 @@ def cog_data_path(cog_instance=None, raw_name: str = None) -> Path:
base_data_path = Path(_base_data_path()) base_data_path = Path(_base_data_path())
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError( raise RuntimeError(
"You must load the basic config before you" " can get the cog data path." "You must load the basic config before you can get the cog data path."
) from e ) from e
cog_path = base_data_path / basic_config["COG_PATH_APPEND"] cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
@@ -128,7 +122,7 @@ def core_data_path() -> Path:
base_data_path = Path(_base_data_path()) base_data_path = Path(_base_data_path())
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError( raise RuntimeError(
"You must load the basic config before you" " can get the core data path." "You must load the basic config before you can get the core data path."
) from e ) from e
core_path = base_data_path / basic_config["CORE_PATH_APPEND"] core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
core_path.mkdir(exist_ok=True, parents=True) core_path.mkdir(exist_ok=True, parents=True)

View File

@@ -47,9 +47,7 @@ class Dev:
""" """
if e.text is None: if e.text is None:
return box("{0.__class__.__name__}: {0}".format(e), lang="py") return box("{0.__class__.__name__}: {0}".format(e), lang="py")
return box( return box("{0.text}{1:>{0.offset}}\n{2}: {0}".format(e, "^", type(e).__name__), lang="py")
"{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
)
@staticmethod @staticmethod
def get_pages(msg: str): def get_pages(msg: str):
@@ -209,12 +207,12 @@ class Dev:
if ctx.channel.id in self.sessions: if ctx.channel.id in self.sessions:
await ctx.send( await ctx.send(
_("Already running a REPL session in this channel. " "Exit it with `quit`.") _("Already running a REPL session in this channel. Exit it with `quit`.")
) )
return return
self.sessions.add(ctx.channel.id) self.sessions.add(ctx.channel.id)
await ctx.send(_("Enter code to execute or evaluate." " `exit()` or `quit` to exit.")) await ctx.send(_("Enter code to execute or evaluate. `exit()` or `quit` to exit."))
msg_check = lambda m: ( msg_check = lambda m: (
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`") m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")

View File

@@ -2,7 +2,6 @@ __all__ = ["BaseDriver"]
class BaseDriver: class BaseDriver:
def __init__(self, cog_name, identifier): def __init__(self, cog_name, identifier):
self.cog_name = cog_name self.cog_name = cog_name
self.unique_cog_identifier = identifier self.unique_cog_identifier = identifier

View File

@@ -78,7 +78,7 @@ class Mongo(BaseDriver):
) )
if partial is None: if partial is None:
raise KeyError("No matching document was found and Config expects" " a KeyError.") raise KeyError("No matching document was found and Config expects a KeyError.")
for i in identifiers: for i in identifiers:
partial = partial[i] partial = partial[i]

View File

@@ -11,13 +11,13 @@ from pkg_resources import DistributionNotFound
import discord import discord
from discord.ext import commands
from . import __version__ from . import __version__, commands
from .data_manager import storage_type from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered, pagify, box from .utils.chat_formatting import inline, bordered, pagify, box
from .utils import fuzzy_command_search from .utils import fuzzy_command_search
from colorama import Fore, Style, init from colorama import Fore, Style, init
from . import rpc
log = logging.getLogger("red") log = logging.getLogger("red")
sentry_log = logging.getLogger("red.sentry") sentry_log = logging.getLogger("red.sentry")
@@ -49,7 +49,6 @@ def should_log_sentry(exception) -> bool:
def init_events(bot, cli_flags): def init_events(bot, cli_flags):
@bot.event @bot.event
async def on_connect(): async def on_connect():
if bot.uptime is None: if bot.uptime is None:
@@ -85,6 +84,9 @@ def init_events(bot, cli_flags):
if packages: if packages:
print("Loaded packages: " + ", ".join(packages)) print("Loaded packages: " + ", ".join(packages))
if bot.rpc_enabled:
await bot.rpc.initialize()
guilds = len(bot.guilds) guilds = len(bot.guilds)
users = len(set([m for m in bot.get_all_members()])) users = len(set([m for m in bot.get_all_members()]))
@@ -97,7 +99,7 @@ def init_events(bot, cli_flags):
else: else:
invite_url = None invite_url = None
prefixes = await bot.db.prefix() prefixes = cli_flags.prefix or (await bot.db.prefix())
lang = await bot.db.locale() lang = await bot.db.locale()
red_version = __version__ red_version = __version__
red_pkg = pkg_resources.get_distribution("Red-DiscordBot") red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
@@ -119,24 +121,24 @@ def init_events(bot, cli_flags):
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands))) INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
async with aiohttp.ClientSession() as session: try:
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r: async with aiohttp.ClientSession() as session:
data = await r.json() async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version): data = await r.json()
INFO.append( if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
"Outdated version! {} is available " INFO.append(
"but you're using {}".format(data["info"]["version"], red_version) "Outdated version! {} is available "
) "but you're using {}".format(data["info"]["version"], red_version)
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id) )
try: owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id)
await owner.send( await owner.send(
"Your Red instance is out of date! {} is the current " "Your Red instance is out of date! {} is the current "
"version, however you are using {}!".format( "version, however you are using {}!".format(
data["info"]["version"], red_version data["info"]["version"], red_version
) )
) )
except: except:
pass pass
INFO2 = [] INFO2 = []
sentry = await bot.db.enable_sentry() sentry = await bot.db.enable_sentry()
@@ -173,8 +175,6 @@ def init_events(bot, cli_flags):
print("\nInvite URL: {}\n".format(invite_url)) print("\nInvite URL: {}\n".format(invite_url))
bot.color = discord.Colour(await bot.db.color()) bot.color = discord.Colour(await bot.db.color())
if bot.rpc_enabled:
await bot.rpc.initialize()
@bot.event @bot.event
async def on_error(event_method, *args, **kwargs): async def on_error(event_method, *args, **kwargs):
@@ -184,6 +184,11 @@ def init_events(bot, cli_flags):
async def on_command_error(ctx, error): async def on_command_error(ctx, error):
if isinstance(error, commands.MissingRequiredArgument): if isinstance(error, commands.MissingRequiredArgument):
await ctx.send_help() await ctx.send_help()
elif isinstance(error, commands.ConversionFailure):
if error.args:
await ctx.send(error.args[0])
else:
await ctx.send_help()
elif isinstance(error, commands.BadArgument): elif isinstance(error, commands.BadArgument):
await ctx.send_help() await ctx.send_help()
elif isinstance(error, commands.DisabledCommand): elif isinstance(error, commands.DisabledCommand):
@@ -226,7 +231,9 @@ def init_events(bot, cli_flags):
term = ctx.invoked_with + " " term = ctx.invoked_with + " "
if len(ctx.args) > 1: if len(ctx.args) > 1:
term += " ".join(ctx.args[1:]) term += " ".join(ctx.args[1:])
await ctx.maybe_send_embed(fuzzy_command_search(ctx, ctx.invoked_with)) fuzzy_result = await fuzzy_command_search(ctx, ctx.invoked_with)
if fuzzy_result is not None:
await ctx.maybe_send_embed(fuzzy_result)
elif isinstance(error, commands.CheckFailure): elif isinstance(error, commands.CheckFailure):
pass pass
elif isinstance(error, commands.NoPrivateMessage): elif isinstance(error, commands.NoPrivateMessage):

View File

@@ -3,7 +3,6 @@ from . import commands
def init_global_checks(bot): def init_global_checks(bot):
@bot.check @bot.check
async def global_perms(ctx): async def global_perms(ctx):
"""Check the user is/isn't globally whitelisted/blacklisted.""" """Check the user is/isn't globally whitelisted/blacklisted."""
@@ -27,10 +26,12 @@ def init_global_checks(bot):
local_blacklist = await guild_settings.blacklist() local_blacklist = await guild_settings.blacklist()
local_whitelist = await guild_settings.whitelist() local_whitelist = await guild_settings.whitelist()
_ids = [r.id for r in ctx.author.roles if not r.is_default]
_ids.append(ctx.author.id)
if local_whitelist: if local_whitelist:
return ctx.author.id in local_whitelist return any(i in local_whitelist for i in _ids)
return ctx.author.id not in local_blacklist return not any(i in local_blacklist for i in _ids)
@bot.check @bot.check
async def bots(ctx): async def bots(ctx):

View File

@@ -40,7 +40,7 @@ from redbot.core.utils.chat_formatting import pagify, box
from redbot.core.utils import fuzzy_command_search from redbot.core.utils import fuzzy_command_search
EMPTY_STRING = u"\u200b" EMPTY_STRING = "\u200b"
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"} _mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
@@ -60,6 +60,12 @@ class Help(formatter.HelpFormatter):
def pm_check(self, ctx): def pm_check(self, ctx):
return isinstance(ctx.channel, discord.DMChannel) return isinstance(ctx.channel, discord.DMChannel)
@property
def clean_prefix(self):
maybe_member = self.context.guild.me if self.context.guild else self.context.bot.user
pretty = f"@{maybe_member.display_name}"
return self.context.prefix.replace(maybe_member.mention, pretty)
@property @property
def me(self): def me(self):
return self.context.me return self.context.me
@@ -184,15 +190,19 @@ class Help(formatter.HelpFormatter):
for category, commands_ in itertools.groupby(data, key=category): for category, commands_ in itertools.groupby(data, key=category):
commands_ = sorted(commands_) commands_ = sorted(commands_)
if len(commands_) > 0: if len(commands_) > 0:
field = EmbedField(category, self._add_subcommands(commands_), False) for i, page in enumerate(
emb["fields"].append(field) pagify(self._add_subcommands(commands_), page_length=1000)
):
title = category if i < 1 else f"{category} (continued)"
field = EmbedField(title, page, False)
emb["fields"].append(field)
else: else:
# Get list of commands for category # Get list of commands for category
filtered = sorted(filtered) filtered = sorted(filtered)
if filtered: if filtered:
for i, page in enumerate( for i, page in enumerate(
pagify(self._add_subcommands(filtered), page_length=1020) pagify(self._add_subcommands(filtered), page_length=1000)
): ):
title = ( title = (
"**__Commands:__**" "**__Commands:__**"
@@ -202,7 +212,6 @@ class Help(formatter.HelpFormatter):
if i > 0: if i > 0:
title += " (continued)" title += " (continued)"
field = EmbedField(title, page, False) field = EmbedField(title, page, False)
# This will still break at 6k total chars, hope that isnt an issue later
emb["fields"].append(field) emb["fields"].append(field)
return emb return emb
@@ -281,11 +290,10 @@ class Help(formatter.HelpFormatter):
embed.set_author(**self.author) embed.set_author(**self.author)
return embed return embed
async def cmd_not_found(self, ctx, cmd, color=None): async def cmd_not_found(self, ctx, cmd, description=None, color=None):
# Shortcut for a shortcut. Sue me # Shortcut for a shortcut. Sue me
out = fuzzy_command_search(ctx, " ".join(ctx.args[1:]))
embed = await self.simple_embed( embed = await self.simple_embed(
ctx, title="Command {} not found.".format(cmd), description=out, color=color ctx, title="Command {} not found.".format(cmd), description=description, color=color
) )
return embed return embed
@@ -296,7 +304,7 @@ class Help(formatter.HelpFormatter):
return embed return embed
@commands.command() @commands.command(hidden=True)
async def help(ctx, *cmds: str): async def help(ctx, *cmds: str):
"""Shows help documentation. """Shows help documentation.
@@ -326,11 +334,19 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send(
embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
)
)
else: else:
await destination.send( fuzzy_result = await fuzzy_command_search(ctx, name)
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) if fuzzy_result is not None:
) await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_result)
)
return return
if use_embeds: if use_embeds:
embeds = await ctx.bot.formatter.format_help_for(ctx, command) embeds = await ctx.bot.formatter.format_help_for(ctx, command)
@@ -341,11 +357,17 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send(
embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
)
)
else: else:
await destination.send( fuzzy_result = await fuzzy_command_search(ctx, name)
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) if fuzzy_result is not None:
) await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
return return
for key in cmds[1:]: for key in cmds[1:]:
@@ -354,13 +376,19 @@ async def help(ctx, *cmds: str):
command = command.all_commands.get(key) command = command.all_commands.get(key)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send( fuzzy_result = await fuzzy_command_search(ctx, name)
embed=await ctx.bot.formatter.cmd_not_found(ctx, key) if fuzzy_result is not None:
) await destination.send(
embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
)
)
else: else:
await destination.send( fuzzy_result = await fuzzy_command_search(ctx, name)
ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name)) if fuzzy_result is not None:
) await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_result)
)
return return
except AttributeError: except AttributeError:
if use_embeds: if use_embeds:

View File

@@ -461,6 +461,9 @@ async def create_case(
if not await case_type.is_enabled(): if not await case_type.is_enabled():
return None return None
if user == bot.user:
return None
next_case_number = int(await get_next_case_number(guild)) next_case_number = int(await get_next_case_number(guild))
case = Case( case = Case(

View File

@@ -1,117 +1,74 @@
import weakref import asyncio
from aiohttp import web from aiohttp import web
import jsonrpcserver.aio from aiohttp_json_rpc import JsonRpc
from aiohttp_json_rpc.rpc import unpack_request_args
import inspect
import logging import logging
__all__ = ["methods", "RPC", "Methods"]
log = logging.getLogger("red.rpc") log = logging.getLogger("red.rpc")
__all__ = ["RPC", "RPCMixin", "get_name"]
class Methods(jsonrpcserver.aio.AsyncMethods):
"""
Container class for all registered RPC methods, please use the existing `methods`
attribute rather than creating a new instance of this class.
.. warning::
**NEVER** create a new instance of this class!
"""
def __init__(self):
super().__init__()
self._items = weakref.WeakValueDictionary()
def add(self, method, name: str = None):
"""
Registers a method to the internal RPC server making it available for
RPC users to call.
.. important::
Any method added here must take ONLY JSON serializable parameters and
MUST return a JSON serializable object.
Parameters
----------
method : function
A reference to the function to register.
name : str
Name of the function as seen by the RPC clients.
"""
if not inspect.iscoroutinefunction(method):
raise TypeError("Method must be a coroutine.")
if name is None:
name = method.__qualname__
self._items[str(name)] = method
def remove(self, *, name: str = None, method=None):
"""
Unregisters an RPC method. Either a name or reference to the method must
be provided and name will take priority.
Parameters
----------
name : str
method : function
"""
if name and name in self._items:
del self._items[name]
elif method and method in self._items.values():
to_remove = []
for name, val in self._items.items():
if method == val:
to_remove.append(name)
for name in to_remove:
del self._items[name]
def all_methods(self):
"""
Lists all available method names.
Returns
-------
list of str
"""
return self._items.keys()
methods = Methods() def get_name(func, prefix=None):
class_name = prefix or func.__self__.__class__.__name__.lower()
func_name = func.__name__.strip("_")
if class_name == "redrpc":
return func_name.upper()
return f"{class_name}__{func_name}".upper()
class BaseRPCMethodMixin: class RedRpc(JsonRpc):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_methods(("", self.get_method_info))
def __init__(self): def _add_method(self, method, prefix=""):
methods.add(self.all_methods, name="all_methods") if not asyncio.iscoroutinefunction(method):
return
async def all_methods(self): name = get_name(method, prefix)
return list(methods.all_methods())
self.methods[name] = method
def remove_method(self, method):
meth_name = get_name(method)
new_methods = {}
for name, meth in self.methods.items():
if name != meth_name:
new_methods[name] = meth
self.methods = new_methods
def remove_methods(self, prefix: str):
new_methods = {}
for name, meth in self.methods.items():
splitted = name.split("__")
if len(splitted) < 2 or splitted[0] != prefix:
new_methods[name] = meth
self.methods = new_methods
async def get_method_info(self, request):
method_name = request.params[0]
if method_name in self.methods:
return self.methods[method_name].__doc__
return "No docstring available."
class RPC(BaseRPCMethodMixin): class RPC:
""" """
RPC server manager. RPC server manager.
""" """
def __init__(self, bot): def __init__(self):
self.app = web.Application(loop=bot.loop) self.app = web.Application()
self.app.router.add_post("/rpc", self.handle) self._rpc = RedRpc()
self.app.router.add_route("*", "/", self._rpc)
self.app_handler = self.app.make_handler() self.app_handler = self.app.make_handler()
self.server = None self.server = None
super().__init__()
async def initialize(self): async def initialize(self):
""" """
Finalizes the initialization of the RPC server and allows it to begin Finalizes the initialization of the RPC server and allows it to begin
@@ -126,10 +83,79 @@ class RPC(BaseRPCMethodMixin):
""" """
self.server.close() self.server.close()
async def handle(self, request): def add_method(self, method, prefix: str = None):
request = await request.text() if prefix is None:
response = await methods.dispatch(request) prefix = method.__self__.__class__.__name__.lower()
if response.is_notification:
return web.Response() if not asyncio.iscoroutinefunction(method):
else: raise TypeError("RPC methods must be coroutines.")
return web.json_response(response, status=response.http_status)
self._rpc.add_methods((prefix, unpack_request_args(method)))
def add_multi_method(self, *methods, prefix: str = None):
if not all(asyncio.iscoroutinefunction(m) for m in methods):
raise TypeError("RPC methods must be coroutines.")
for method in methods:
self.add_method(method, prefix=prefix)
def remove_method(self, method):
self._rpc.remove_method(method)
def remove_methods(self, prefix: str):
self._rpc.remove_methods(prefix)
class RPCMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.rpc = RPC()
self.rpc_handlers = {} # Lowered cog name to method
def register_rpc_handler(self, method):
"""
Registers a method to act as an RPC handler if the internal RPC server is active.
When calling this method through the RPC server, use the naming scheme "cogname__methodname".
.. important::
All parameters to RPC handler methods must be JSON serializable objects.
The return value of handler methods must also be JSON serializable.
Parameters
----------
method : coroutine
The method to register with the internal RPC server.
"""
self.rpc.add_method(method)
cog_name = method.__self__.__class__.__name__.upper()
if cog_name not in self.rpc_handlers:
self.rpc_handlers[cog_name] = []
self.rpc_handlers[cog_name].append(method)
def unregister_rpc_handler(self, method):
"""
Unregisters an RPC method handler.
This will be called automatically for you on cog unload and will pass silently if the
method is not previously registered.
Parameters
----------
method : coroutine
The method to unregister from the internal RPC server.
"""
self.rpc.remove_method(method)
name = get_name(method)
cog_name = name.split("__")[0]
if cog_name in self.rpc_handlers:
try:
self.rpc_handlers[cog_name].remove(method)
except ValueError:
pass

View File

@@ -1,23 +1,19 @@
__all__ = ["TYPE_CHECKING", "NewType", "safe_delete", "fuzzy_command_search"] __all__ = ["safe_delete", "fuzzy_command_search"]
from pathlib import Path from pathlib import Path
import os import os
import shutil import shutil
import logging
from redbot.core import commands from redbot.core import commands
from fuzzywuzzy import process from fuzzywuzzy import process
from .chat_formatting import box from .chat_formatting import box
try:
from typing import TYPE_CHECKING
except ImportError:
TYPE_CHECKING = False
try: def fuzzy_filter(record):
from typing import NewType return record.funcName != "extractWithoutOrder"
except ImportError:
def NewType(name, tp):
return type(name, (tp,), {}) logging.getLogger().addFilter(fuzzy_filter)
def safe_delete(pth: Path): def safe_delete(pth: Path):
@@ -31,9 +27,49 @@ def safe_delete(pth: Path):
shutil.rmtree(str(pth), ignore_errors=True) shutil.rmtree(str(pth), ignore_errors=True)
def fuzzy_command_search(ctx: commands.Context, term: str): async def filter_commands(ctx: commands.Context, extracted: list):
return [
i
for i in extracted
if i[1] >= 90
and not i[0].hidden
and await i[0].can_run(ctx)
and all([await p.can_run(ctx) for p in i[0].parents])
and not any([p.hidden for p in i[0].parents])
]
async def fuzzy_command_search(ctx: commands.Context, term: str):
out = "" out = ""
for pos, extracted in enumerate(process.extract(term, ctx.bot.walk_commands(), limit=5), 1): if ctx.guild is not None:
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy()
else:
enabled = await ctx.bot.db.fuzzy()
if not enabled:
return None
alias_cog = ctx.bot.get_cog("Alias")
if alias_cog is not None:
is_alias, alias = await alias_cog.is_alias(ctx.guild, term)
if is_alias:
return None
customcom_cog = ctx.bot.get_cog("CustomCommands")
if customcom_cog is not None:
cmd_obj = customcom_cog.commandobj
try:
ccinfo = await cmd_obj.get(ctx.message, term)
except:
pass
else:
return None
extracted_cmds = await filter_commands(
ctx, process.extract(term, ctx.bot.walk_commands(), limit=5)
)
if not extracted_cmds:
return None
for pos, extracted in enumerate(extracted_cmds, 1):
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format( out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format(
pos, pos,
ctx, ctx,

View File

@@ -18,6 +18,7 @@ class AntiSpam:
Where quantity represents the maximum amount of times Where quantity represents the maximum amount of times
something should be allowed in an interval. something should be allowed in an interval.
""" """
# TODO : Decorator interface for command check using `spammy` # TODO : Decorator interface for command check using `spammy`
# with insertion of the antispam element into context # with insertion of the antispam element into context
# for manual stamping on succesful command completion # for manual stamping on succesful command completion

View File

@@ -5,10 +5,14 @@ https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5) Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
""" """
import asyncio import asyncio
import contextlib
from typing import Union, Iterable
import discord import discord
from redbot.core import commands from redbot.core import commands
_ReactableEmoji = Union[str, discord.Emoji]
async def menu( async def menu(
ctx: commands.Context, ctx: commands.Context,
@@ -66,8 +70,8 @@ async def menu(
message = await ctx.send(embed=current_page) message = await ctx.send(embed=current_page)
else: else:
message = await ctx.send(current_page) message = await ctx.send(current_page)
for key in controls.keys(): # Don't wait for reactions to be added (GH-1797)
await message.add_reaction(key) ctx.bot.loop.create_task(_add_menu_reactions(message, controls.keys()))
else: else:
if isinstance(current_page, discord.Embed): if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page) await message.edit(embed=current_page)
@@ -148,4 +152,12 @@ async def close_menu(
return None return None
async def _add_menu_reactions(message: discord.Message, emojis: Iterable[_ReactableEmoji]):
"""Add the reactions"""
# The task should exit silently if the message is deleted
with contextlib.suppress(discord.NotFound):
for emoji in emojis:
await message.add_reaction(emoji)
DEFAULT_CONTROLS = {"": prev_page, "": close_menu, "": next_page} DEFAULT_CONTROLS = {"": prev_page, "": close_menu, "": next_page}

View File

@@ -25,9 +25,7 @@ if sys.platform == "linux":
PYTHON_OK = sys.version_info >= (3, 5) PYTHON_OK = sys.version_info >= (3, 5)
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
INTRO = ( INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
"==========================\n" "Red Discord Bot - Launcher\n" "==========================\n"
)
IS_WINDOWS = os.name == "nt" IS_WINDOWS = os.name == "nt"
IS_MAC = sys.platform == "darwin" IS_MAC = sys.platform == "darwin"
@@ -383,7 +381,7 @@ def debug_info():
+ "User: {}\n".format(user_who_ran) + "User: {}\n".format(user_who_ran)
) )
print(info) print(info)
exit(0) sys.exit(0)
def main_menu(): def main_menu():
@@ -457,7 +455,7 @@ def main_menu():
def main(): def main():
if not PYTHON_OK: if not PYTHON_OK:
raise RuntimeError( raise RuntimeError(
"Red requires Python 3.5 or greater. " "Please install the correct version!" "Red requires Python 3.5 or greater. Please install the correct version!"
) )
if args.debuginfo: # Check first since the function triggers an exit if args.debuginfo: # Check first since the function triggers an exit
debug_info() debug_info()

5
redbot/meta.py Normal file
View File

@@ -0,0 +1,5 @@
"""
This module will contain various attributes useful for testing and cog development.
"""
testing = False

View File

@@ -32,7 +32,7 @@ if not config_dir:
try: try:
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
except PermissionError: except PermissionError:
print("You don't have permission to write to " "'{}'\nExiting...".format(config_dir)) print("You don't have permission to write to '{}'\nExiting...".format(config_dir))
sys.exit(1) sys.exit(1)
config_file = config_dir / "config.json" config_file = config_dir / "config.json"
@@ -101,7 +101,7 @@ def get_data_dir():
) )
sys.exit(1) sys.exit(1)
print("You have chosen {} to be your data directory." "".format(default_data_dir)) print("You have chosen {} to be your data directory.".format(default_data_dir))
if not confirm("Please confirm (y/n):"): if not confirm("Please confirm (y/n):"):
print("Please start the process over.") print("Please start the process over.")
sys.exit(0) sys.exit(0)

View File

@@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
appdirs==1.4.3 appdirs==1.4.3
raven==6.5.0 raven==6.5.0
colorama==0.3.9 colorama==0.3.9
jsonrpcserver aiohttp-json-rpc==0.8.7
pyyaml==3.12 pyyaml==3.12
fuzzywuzzy[speedup]<=0.16.0 fuzzywuzzy[speedup]<=0.16.0
Red-Trivia>=1.1.1 Red-Trivia>=1.1.1

View File

@@ -1,6 +1,9 @@
from distutils.core import setup from distutils.core import setup
from distutils import ccompiler
from distutils.errors import CCompilerError, DistutilsPlatformError
from pathlib import Path from pathlib import Path
import re import re
import tempfile
import os import os
import sys import sys
@@ -11,7 +14,9 @@ IS_TRAVIS = "TRAVIS" in os.environ
IS_DEPLOYING = "DEPLOYING" in os.environ IS_DEPLOYING = "DEPLOYING" in os.environ
IS_RTD = "READTHEDOCS" in os.environ IS_RTD = "READTHEDOCS" in os.environ
dep_links = ["https://github.com/Rapptz/discord.py/tarball/rewrite#egg=discord.py-1.0"] dep_links = [
"https://github.com/Rapptz/discord.py/tarball/7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0"
]
if IS_TRAVIS: if IS_TRAVIS:
dep_links = [] dep_links = []
@@ -21,6 +26,20 @@ def get_package_list():
return core return core
def check_compiler_available():
m = ccompiler.new_compiler()
with tempfile.TemporaryDirectory() as tdir:
with tempfile.NamedTemporaryFile(prefix="dummy", suffix=".c", dir=tdir) as tfile:
tfile.write(b"int main(int argc, char** argv) {return 0;}")
tfile.seek(0)
try:
m.compile([tfile.name])
except (CCompilerError, DistutilsPlatformError):
return False
return True
def get_requirements(): def get_requirements():
with open("requirements.txt") as f: with open("requirements.txt") as f:
requirements = f.read().splitlines() requirements = f.read().splitlines()
@@ -31,6 +50,9 @@ def get_requirements():
except ValueError: except ValueError:
pass pass
if not check_compiler_available(): # Can't compile python-Levensthein, so drop extra
requirements.remove("fuzzywuzzy[speedup]<=0.16.0")
requirements.append("fuzzywuzzy<=0.16.0")
if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD): if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD):
requirements.append("discord.py>=1.0.0a0") requirements.append("discord.py>=1.0.0a0")
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
@@ -104,7 +126,7 @@ setup(
"redbot-launcher=redbot.launcher:main", "redbot-launcher=redbot.launcher:main",
] ]
}, },
python_requires=">=3.5,<3.7", python_requires=">=3.6,<3.7",
setup_requires=get_requirements(), setup_requires=get_requirements(),
install_requires=get_requirements(), install_requires=get_requirements(),
dependency_links=dep_links, dependency_links=dep_links,
@@ -113,6 +135,6 @@ setup(
"mongo": ["motor"], "mongo": ["motor"],
"docs": ["sphinx>=1.7", "sphinxcontrib-asyncio", "sphinx_rtd_theme"], "docs": ["sphinx>=1.7", "sphinxcontrib-asyncio", "sphinx_rtd_theme"],
"voice": ["red-lavalink>=0.0.4"], "voice": ["red-lavalink>=0.0.4"],
"style": ["black"], "style": ["black==18.5b1"],
}, },
) )

View File

View File

@@ -0,0 +1,26 @@
{
"1" : {
"1" : [
"Test",
"Test2",
"TEST3"
],
"2" : [
"Test4",
"Test5",
"TEST6"
]
},
"2" : {
"1" : [
"Test",
"Test2",
"TEST3"
],
"2" : [
"Test4",
"Test5",
"TEST6"
]
}
}

View File

@@ -0,0 +1,39 @@
import pytest
from pathlib import Path
from collections import namedtuple
from redbot.cogs.dataconverter import core_specs
from redbot.core.utils.data_converter import DataConverter
def mock_dpy_object(id_):
return namedtuple("DPYObject", "id")(int(id_))
def mock_dpy_member(guildid, userid):
return namedtuple("Member", "id guild")(int(userid), mock_dpy_object(guildid))
@pytest.fixture()
def specresolver():
here = Path(__file__)
resolver = core_specs.SpecResolver(here.parent)
return resolver
@pytest.mark.asyncio
async def test_mod_nicknames(red, specresolver: core_specs.SpecResolver):
filepath, converter, cogname, attr, _id = specresolver.get_conversion_info("Past Nicknames")
conf = specresolver.get_config_object(red, cogname, attr, _id)
v2data = DataConverter.json_load(filepath)
await specresolver.convert(red, "Past Nicknames", config=conf)
for guildid, guild_data in v2data.items():
guild = mock_dpy_object(guildid)
for userid, user_data in guild_data.items():
member = mock_dpy_member(guildid, userid)
assert await conf.member(member).past_nicks() == user_data

View File

@@ -25,7 +25,6 @@ async def fake_run_noprint(*args, **kwargs):
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="module", autouse=True)
def patch_relative_to(monkeysession): def patch_relative_to(monkeysession):
def fake_relative_to(self, some_path: Path): def fake_relative_to(self, some_path: Path):
return self return self
@@ -111,6 +110,18 @@ async def test_add_repo(monkeypatch, repo_manager):
assert squid.available_modules == [] assert squid.available_modules == []
@pytest.mark.asyncio
async def test_remove_repo(monkeypatch, repo_manager):
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
await repo_manager.add_repo(
url="https://github.com/tekulvw/Squid-Plugins", name="squid", branch="rewrite_cogs"
)
assert repo_manager.get_repo("squid") is not None
await repo_manager.delete_repo("squid")
assert repo_manager.get_repo("squid") is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_current_branch(bot_repo): async def test_current_branch(bot_repo):
branch = await bot_repo.current_branch() branch = await bot_repo.current_branch()

View File

@@ -1,15 +1,14 @@
import pytest import pytest
from redbot.cogs.alias import Alias from redbot.cogs.alias import Alias
from redbot.core import Config
@pytest.fixture() @pytest.fixture()
def alias(config): def alias(config, monkeypatch):
import redbot.cogs.alias.alias with monkeypatch.context() as m:
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
redbot.cogs.alias.alias.Config.get_conf = lambda *args, **kwargs: config return Alias(None)
return Alias(None)
def test_is_valid_alias_name(alias): def test_is_valid_alias_name(alias):

View File

@@ -2,15 +2,15 @@ import pytest
@pytest.fixture() @pytest.fixture()
def bank(config): def bank(config, monkeypatch):
from redbot.core import Config from redbot.core import Config
Config.get_conf = lambda *args, **kwargs: config with monkeypatch.context() as m:
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
from redbot.core import bank
from redbot.core import bank bank._register_defaults()
return bank
bank._register_defaults()
return bank
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -56,3 +56,27 @@ async def test_bank_can_spend(bank, member_factory):
acc = await bank.get_account(mbr) acc = await bank.get_account(mbr)
canspendnow = await bank.can_spend(mbr, 100) canspendnow = await bank.can_spend(mbr, 100)
assert canspendnow assert canspendnow
@pytest.mark.asyncio
async def test_set_bank_name(bank, guild_factory):
guild = guild_factory.get()
await bank.set_bank_name("Test Bank", guild)
name = await bank.get_bank_name(guild)
assert name == "Test Bank"
@pytest.mark.asyncio
async def test_set_currency_name(bank, guild_factory):
guild = guild_factory.get()
await bank.set_currency_name("Coins", guild)
name = await bank.get_currency_name(guild)
assert name == "Coins"
@pytest.mark.asyncio
async def test_set_default_balance(bank, guild_factory):
guild = guild_factory.get()
await bank.set_default_balance(500, guild)
default_bal = await bank.get_default_balance(guild)
assert default_bal == 500

View File

@@ -2,15 +2,15 @@ import pytest
@pytest.fixture @pytest.fixture
def mod(config): def mod(config, monkeypatch):
from redbot.core import Config from redbot.core import Config
Config.get_conf = lambda *args, **kwargs: config with monkeypatch.context() as m:
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
from redbot.core import modlog
from redbot.core import modlog modlog._register_defaults()
return modlog
modlog._register_defaults()
return modlog
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@@ -27,7 +27,6 @@ def override_data_path(tmpdir):
@pytest.fixture() @pytest.fixture()
def coroutine(): def coroutine():
async def some_coro(*args, **kwargs): async def some_coro(*args, **kwargs):
return args, kwargs return args, kwargs
@@ -74,7 +73,6 @@ def guild_factory():
mock_guild = namedtuple("Guild", "id members") mock_guild = namedtuple("Guild", "id members")
class GuildFactory: class GuildFactory:
def get(self): def get(self):
return mock_guild(random.randint(1, 999999999), []) return mock_guild(random.randint(1, 999999999), [])
@@ -103,7 +101,6 @@ def member_factory(guild_factory):
mock_member = namedtuple("Member", "id guild display_name") mock_member = namedtuple("Member", "id guild display_name")
class MemberFactory: class MemberFactory:
def get(self): def get(self):
return mock_member(random.randint(1, 999999999), guild_factory.get(), "Testing_Name") return mock_member(random.randint(1, 999999999), guild_factory.get(), "Testing_Name")
@@ -120,7 +117,6 @@ def user_factory():
mock_user = namedtuple("User", "id") mock_user = namedtuple("User", "id")
class UserFactory: class UserFactory:
def get(self): def get(self):
return mock_user(random.randint(1, 999999999)) return mock_user(random.randint(1, 999999999))
@@ -158,7 +154,7 @@ def red(config_fr):
Config.get_core_conf = lambda *args, **kwargs: config_fr Config.get_core_conf = lambda *args, **kwargs: config_fr
red = Red(cli_flags, description=description, pm_help=None) red = Red(cli_flags=cli_flags, description=description, pm_help=None)
yield red yield red

145
tests/core/test_rpc.py Normal file
View File

@@ -0,0 +1,145 @@
import pytest
from redbot.core.rpc import RPC, RPCMixin, get_name
from unittest.mock import MagicMock
@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
def test_get_name(cog):
assert get_name(cog.cofunc) == "COG__COFUNC"
assert get_name(cog.cofunc2) == "COG__COFUNC2"
assert get_name(cog.func) == "COG__FUNC"
def test_internal_methods_exist(rpc):
assert "GET_METHODS" in rpc._rpc.methods
def test_add_method(rpc, cog):
rpc.add_method(cog.cofunc)
assert get_name(cog.cofunc) in rpc._rpc.methods
def test_double_add(rpc, cog):
rpc.add_method(cog.cofunc)
count = len(rpc._rpc.methods)
rpc.add_method(cog.cofunc)
assert count == len(rpc._rpc.methods)
def test_add_notcoro_method(rpc, cog):
with pytest.raises(TypeError):
rpc.add_method(cog.func)
def test_add_multi(rpc, cog):
funcs = [cog.cofunc, cog.cofunc2, cog.cofunc3]
rpc.add_multi_method(*funcs)
names = [get_name(f) for f in funcs]
assert all(n in rpc._rpc.methods for n in names)
def test_add_multi_bad(rpc, cog):
funcs = [cog.cofunc, cog.cofunc2, cog.cofunc3, cog.func]
with pytest.raises(TypeError):
rpc.add_multi_method(*funcs)
names = [get_name(f) for f in funcs]
assert not any(n in rpc._rpc.methods for n in names)
def test_remove_method(rpc, existing_func):
before_count = len(rpc._rpc.methods)
rpc.remove_method(existing_func)
assert get_name(existing_func) not in rpc._rpc.methods
assert before_count - 1 == len(rpc._rpc.methods)
def test_remove_multi_method(rpc, existing_multi_func):
before_count = len(rpc._rpc.methods)
name = get_name(existing_multi_func[0])
prefix = name.split("__")[0]
rpc.remove_methods(prefix)
assert before_count - len(existing_multi_func) == len(rpc._rpc.methods)
names = [get_name(f) for f in existing_multi_func]
assert not any(n in rpc._rpc.methods for n in names)
def test_rpcmixin_register(rpcmixin, cog):
rpcmixin.register_rpc_handler(cog.cofunc)
assert rpcmixin.rpc.add_method.called_once_with(cog.cofunc)
name = get_name(cog.cofunc)
cogname = name.split("__")[0]
assert cogname in rpcmixin.rpc_handlers
def test_rpcmixin_unregister(rpcmixin, cog):
rpcmixin.register_rpc_handler(cog.cofunc)
rpcmixin.unregister_rpc_handler(cog.cofunc)
assert rpcmixin.rpc.remove_method.called_once_with(cog.cofunc)
name = get_name(cog.cofunc)
cogname = name.split("__")[0]
if cogname in rpcmixin.rpc_handlers:
assert cog.cofunc not in rpcmixin.rpc_handlers[cogname]

21
tests/rpc_test.html Normal file
View File

@@ -0,0 +1,21 @@
<script src="https://code.jquery.com/jquery-2.2.1.js"></script>
<script>
var ws = new WebSocket("ws://localhost:6133");
var message_id = 0;
ws.onmessage = function(event) {
console.log(JSON.parse(event.data));
}
function ws_call_method(method, params) {
var request = {
jsonrpc: "2.0",
id: message_id,
method: method,
params: params
}
ws.send(JSON.stringify(request));
message_id++;
}
</script>

View File

@@ -5,7 +5,6 @@
[tox] [tox]
envlist = envlist =
py35
py36 py36
docs docs
style style