Compare commits

..

67 Commits

Author SHA1 Message Date
Toby Harradine
9752a9c719 Bump version to 3.0.0rc3 (#2367)
Also updated some dependencies, including discord.py.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 11:10:01 +11:00
Toby Harradine
7973babe4b Catch exceptions in [p]backup (#2363)
Resolves #2354.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 09:07:37 +11:00
Michael H
78e4b578e2 [Utils] Tunnel minor fixes (#2366)
- Tunnel uses a safe max size (Max size is related to maximum payload, not maximum file size)
  - Checks attachment sizes prior to download
2019-01-10 19:46:49 +11:00
Toby Harradine
8eb8848898 [Mod] Context-based voice checks (#2351)
- Removed `redbot.cogs.mod.checks` module
- Moved logic for formatting a user-friendly list of permissions to `redbot.core.utils.chat_formatting`
- `[p]voice(un)ban` and `[p](un)mute voice` now check permissions in the user's voice channel

Resolves #2296.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-10 11:35:37 +11:00
Toby Harradine
aac1460240 [Utils] Exit menu silently when message is deleted (#2344)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-10 11:33:38 +11:00
Toby Harradine
dde5582669 [CogManager] Removal of implicit paths and general cleanup (#2345)
- Removed memory-sided `CogManager._paths` attribute, as it has no practical use.
- `[p]removepath` now removes the actual path displayed with the index specified in `[p]paths`.
- New method for retreiving a deduplicated list of user-defined paths as `Path` objects
- General cleanup so we don't have to do so much head-scratching next time an issue arises here

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-06 11:02:58 +11:00
Toby Harradine
aa854cf1f9 [CustomCom] Insert space before newline separating CC previews (#2350)
Resolves #2295.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-29 16:54:32 +01:00
Toby Harradine
2bd05a5a04 Fix JSON to Mongo migration (#2346)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-29 10:51:31 +11:00
Toby Harradine
3a8da1f82b [Cleanup] Fix cleanup after
Resolves #2343.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-22 09:19:25 +11:00
Toby Harradine
811634a2b0 [Permissions] Help menu respects rules (#2339)
Resolves #2249.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-21 13:30:13 +11:00
Toby Harradine
2512320b30 [DataManager] Don't copy bundled data to cog data folder (#2342)
- `load_bundled_data` is now deprecated and obselete
- Bundled data is now no longer copied to the cog's data folder, greatly simplifying its operations under the hood. Instead, `bundled_data_path` will now return the path to the folder from which the files would have previously been copied.

Resolves #2329.

Resolves #2280.
2018-12-21 13:26:00 +11:00
Michael H
db03faf042 Make webhooks automod immune (#2337)
Resolves #2336.
2018-12-21 11:43:46 +11:00
Toby Harradine
701259158f [Permissions] Always respect default rules (#2341)
Resolves #2340.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-21 10:35:17 +11:00
Kowlin
a5efdc6492 [Mod] Handle invalid nicknames in [p]rename (#2311) 2018-12-16 11:21:36 +11:00
Kowlin
38b15ded87 [Mod] Fixed loud RuntimeError on modlog cases (#2331) 2018-12-16 10:26:48 +11:00
Michael H
351749dff6 [Permissions] Find things uniquely for models (#2258)
This is a safety measure to prevent accidentally passing a model which has the same name as another model, potentially modifying rules for the unwanted one.
2018-12-16 10:09:18 +11:00
Michael H
2d9912cea7 prevent locking out owner (#2317) 2018-12-13 18:38:03 +01:00
NNTin
c4ab34a049 V3: regex extension on java -version (#2316)
* regex extension on java -version

* make it a non capturing group

* alphanumeric matching

* Match specification: Style, line length

* Update manager.py
2018-12-13 18:28:00 +01:00
Michael H
985e7b3c6d swap unsafe yaml.load usage for yaml.safe_load (#2324)
Related to #2323

Recommend additionally adding a step in CI
ensuring use of `yaml.load` is prevented from existing in the code base.
2018-12-13 18:19:27 +01:00
palmtree5
7546c50226 [Streams] Toggle mentions on for roles being alerted (#2252)
Resolves #1831.
2018-11-24 10:46:38 +11:00
aikaterna
6435f6b882 [Audio] Match openJDK 11 on Ubuntu (#2270)
Using the OpenJDK 11 from java.net on Ubuntu 18 reports "11" as the version, which failed the Java version check on loading audio on a new instance. This change will return "11 0" as the version, passing the check, instead of just "11".
2018-11-24 10:45:37 +11:00
aikaterna
bbccb671b8 [Audio] Disallow seek during active vote (#2290)
Users were able to use seek to skip songs while voteskip was active. This change prevents users from doing so.
2018-11-24 10:43:23 +11:00
Twentysix
8abb24bc01 [Core] [p]leave: Fix incorrect response handling (#2302) 2018-11-24 10:42:05 +11:00
aikaterna
419008f644 [Downloader] Findcog fix with no repo installed (#2304)
The findcog command errored out if a repo was removed. This change informs the user that the repo is not installed instead of erroring out.
2018-11-24 10:39:54 +11:00
odinair
d17c2430d7 [Streams] Fix alerts with no configured mentions (#2305) 2018-11-24 10:14:43 +11:00
Michael H
ca533f8937 [JsonIO] race condition fix (#2308)
* race condition fix

* style fix
2018-11-24 10:11:59 +11:00
Michael H
9d22d5b7b5 [Cleanup] Correct handling of command message (#2310)
Resolves #2307
2018-11-24 10:04:08 +11:00
Kowlin
2846dce6ea [Mod] [p]modset: Fix KeyError (#2279)
Co-authored-by: aikaterna <20862007+aikaterna@users.noreply.github.com>
2018-11-14 23:35:56 +01:00
aikaterna
9973b2e3b8 [General] [p]urban: Handle no embeds (#2285) 2018-11-14 23:21:11 +01:00
aikaterna
d008a2559a [General] Clearer error message for [p]rps (#2284) 2018-11-14 22:56:08 +01:00
aikaterna
7f3a0b8a88 [Streams] Add help for streamset (#2287) 2018-11-06 10:26:55 +11:00
aikaterna
d0fca373ba [Audio] Local track verify on playlist start (#2271) 2018-11-06 09:44:26 +11:00
FixedThink
451c4c9d54 [Trivia] Fix text formatting issue (#2269)
The "Question number {}" message was being emboldened twice from the left side, making the bot send messages with two redundant asterisks.
2018-11-06 08:58:10 +11:00
zephyrkul
a59002275d [Economy] Fix TypeError in [p]payouts (#2263) 2018-11-06 08:55:29 +11:00
Toby Harradine
99bbde7be9 Reject bad perm kwargs in check decorators (#2289)
Also fixed a misspelled kwarg in reports.

Also now raising TypeError for an empty `@checks.has_permissions()` decorator.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-11-05 15:07:56 +11:00
Kowlin
92dbd14006 Fixes typos in permissions (#2288)
causing the checks to be thrown out of the window.
2018-11-05 04:19:54 +01:00
palmtree5
6e9243f6e9 Drop unneeded .format in [p]triviaset override (#2268) 2018-10-23 17:40:51 -08:00
Toby Harradine
8bba860f85 Bump version to 3.0.0rc2
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:37:23 +11:00
Toby Harradine
d2d26835c3 [Economy] Detect max balance and prevent OverflowError (#2211)
Resolves #2091.

This doesn't fix every OverflowError with MongoDB; but at least the seemingly easiest one to achieve with core cogs.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:30:53 +11:00
Toby Harradine
aff62a8006 [Downloader] Unload extensions on uninstall (#2243)
Resolves #2216.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:19:32 +11:00
Toby Harradine
b5fd28ef7c [CustomCom] Better display for [p]cc list (#2215)
Uses a menu, optionally embedded with respect to the embed settings, for scrolling through the custom command list, each cc with a ~50 character preview. Format is purposefully similar to the help menu.

Resolves #2104.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 08:39:44 +11:00
Michael H
c510ebe5e5 [Downloader] Only prompt reload of loaded cogs (#2233) 2018-10-15 23:29:56 +11:00
Toby Harradine
5ba95090d9 [Streams] Suppress HTTPExceptions on load (#2228)
Resolves #2227.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-15 22:31:14 +11:00
Toby Harradine
ad51fa830b [Cleanup] [p]cleanup bot includes aliases and CCs (#2213)
Resolves #1920.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-15 22:29:07 +11:00
Christopher Rice
1ba922eba2 [Downloader] Add missing prefix format kwarg (#2238)
Fixes #2237.
2018-10-14 17:11:16 +11:00
Christopher Rice
9588a5740c [Downloader] Define Translator in converters module (#2239)
Fixes #2236
2018-10-14 17:09:54 +11:00
Toby Harradine
7cd765d548 Fix permissions hook removal (#2234)
Some in-progress work slipped through #2149, and I figure it should be fixed before RC2.

I've also just decided to allow discovery of permissions hooks from superclasses as well. We should try to be more aware of the possibility of cog superclasses moving forward.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-14 11:52:39 +11:00
zephyrkul
6022c0f7d7 [Mod] Mute/unmute bugfixes (#2230)
- Helper methods mute_user and unmute_user now take GuildChannel instead of solely TextChannel.
- The bot will not attempt to mute a member with the Administrator permission, as that permission bypasses channel overwrites anyway. The bot will still unmute members with the Administrator permission (see #2076).
- Audit reasons are now specified for mass mutes / unmutes.
- A few typos and missing keyword specifiers were corrected.
- Streamlined some logic and used some already-existing functions.
2018-10-12 15:47:39 +11:00
palmtree5
0548744e94 [V3 Cleanup] fix error in cleanup user (#2225) 2018-10-11 14:36:45 +02:00
Toby Harradine
8b2d115335 [Audio] Rename current_build to current_version in Config (#2219)
Renames the `current_build` key to `current_version`. This means the `current_version` key will always be a dict and never a list, and `current_build` having no defaults means it won't mess with `[p]audioset settings`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 12:02:02 +11:00
Toby Harradine
094735566d [Warnings] Fix actions not being taken (#2218)
When multiple warning actions were registered, and the user didn't exceed the points for the highest action on the list, no action was taken.

Resolves #2106.

Also commented out the casetype registration for warnings, since it's not actually using modlog yet.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:54:11 +11:00
Toby Harradine
f7b1f9f0dc [MongoDB] Escape special characters in keys (#2212)
Essentially resolves #2038, although this is escaping and not rejecting keys as that issue implies.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:20:42 +11:00
Toby Harradine
ce25011f0d [Config] Cast keys to str on get/set/clear (#2217)
This is a step towards a more consistent front-end behaviour of Config, where errors are either circumvented or raised in the same way regardless of the driver being used.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:18:57 +11:00
bobloy
f85034eb27 [Trivia] Add On/Off as alternatives for YAML bools (#2177) 2018-10-09 22:05:37 +11:00
Toby Harradine
849755ecd2 [Core] Fix errors with [p]backup on MongoDB (#2210)
Resolves #2094.

This command needs some more fixing and cleaning up than this, this is just a simple bugfix which gets it mostly working for now.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-09 15:30:27 +11:00
Toby Harradine
9217275908 [Permissions] Fix ValueError for "default" rule in config/ACL (#2200)
This was thrown when the "default" key existed and Permissions tried to iterate over the list mapping keys as ints.

Also fixed some issues with saving config with keys as `int` instead of `str`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-09 15:14:59 +11:00
zephyrkul
9e13ca45e6 [Mod] Fix KeyError in modset (#2208) 2018-10-09 09:04:51 +11:00
aikaterna
46c38a28eb [Alias] Fix alias help (#2194)
Alias help would only return the first character of the invoked command previously. This change shows help for basic commands that are aliased (i.e. just `ping`) or aliased commands that have an argument included (i.e. `audioset role beep` with `beep` being a role name)
2018-10-08 18:23:32 +11:00
El Laggron
76bbcf2f8c [Core] Support already loaded packages in [p]load (#2116) 2018-10-08 08:18:28 +11:00
ASSASSIN0831
ee7e8aa782 [Economy] Revert change to payday message (#2203)
in the updates were for the i18n translation strings the payday command message was accidentally changed to +(amount) (new balance). This changes it back to its original message +(amount) (currency name)
2018-10-08 07:39:30 +11:00
Toby Harradine
fd0abc250d [Audio] Fix type mismatch between config defaults and set value (#2201)
current_build is now set as a dict, but its default was a list.

This resolves the error an `[p]audioset settings`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 19:33:04 +11:00
Toby Harradine
847f9fc8d1 [CustomCommands] Find default converters properly (#2199)
The new `redbot.core.commands.converter` module was causing default converters to never be found.

Also cleaned up some of the other code (made some methods static, fixed some typing violations)

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 12:58:08 +11:00
zephyrkul
046e98565e [Cleanup] use message_filter() over check() param (#2180)
Cleanup's helper method to collect messages to delete was incorrectly filtering by check rather than message_filter, causing delete_after to be ignored.
2018-10-07 10:19:56 +11:00
Toby Harradine
71eddc89ea [Mod] Fix unresolved reference to Member.permissions in reinvite logic
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 09:42:25 +11:00
aikaterna
9730a424ec [Admin] Selfrole list formatting (#2193)
Selfrole list needed a return in between the header and the list.
2018-10-07 08:46:32 +11:00
aikaterna
7b260cdafc [Audio] Playlist list, local queue, DJ Role fix (#2191)
Fix for `playlist list`: added forgotten variable plus return for formatting (fixes #2190)

i18n addditions: DJ Role toggle, Thumbnail display toggle

`audioset settings`: added missed end of line return

`queue` fix: added indentation to not have the currently playing song info repeated in the queue display when playing local songs

`local play` and `local folder` now display the appropriate menu when DJ role is on.
2018-10-07 08:42:13 +11:00
aikaterna
4369095a51 [Economy] Fix for bank set (#2192)
i18n variable was wrong.
2018-10-07 08:26:33 +11:00
49 changed files with 1627 additions and 1081 deletions

View File

@@ -54,7 +54,7 @@ jobs:
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
- sudo apt-get update -qq
- sudo apt-get install -y crowdin
- pip install redgettext==2.1
- pip install redgettext==2.2
deploy:
- provider: script
script: make gettext

453
Pipfile.lock generated
View File

@@ -57,11 +57,10 @@
},
"async-timeout": {
"hashes": [
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_version >= '3.5.3'",
"version": "==3.0.0"
"version": "==3.0.1"
},
"attrs": {
"hashes": [
@@ -79,15 +78,20 @@
},
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.3.9"
"version": "==0.4.1"
},
"discord-py": {
"editable": true,
"git": "git://github.com/Rapptz/discord.py",
"ref": "7f4c57dd5ad20b7fa10aea485f674a4bc24b9547"
},
"discord.py": {
"editable": true,
"git": "git://github.com/Rapptz/discord.py",
"ref": "836ae730401ea370aa10127bb9c86854c8b516ac"
"ref": "rewrite"
},
"distro": {
"hashes": [
@@ -98,10 +102,10 @@
},
"dnspython": {
"hashes": [
"sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c",
"sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31"
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
],
"version": "==1.15.0"
"version": "==1.16.0"
},
"e1839a8": {
"editable": true,
@@ -120,10 +124,10 @@
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.7"
"version": "==2.8"
},
"idna-ssl": {
"hashes": [
@@ -140,72 +144,76 @@
},
"multidict": {
"hashes": [
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
],
"version": "==4.4.2"
"version": "==4.5.2"
},
"pymongo": {
"hashes": [
"sha256:08dea6dbff33363419af7af3bf2e9a373ff71eb22833dd7063f9b953f09a0bdf",
"sha256:0949110db76eb1b54cecfc0c0f8468a8b9a7fd42ba23fd0d4a37d97e0b4ca203",
"sha256:0c31a39f440801cc8603547ccaacf4cb1f02b81af6ba656621c13677b27f4426",
"sha256:1e10b3fda5677d360440ebd12a1185944dc81d9ea9acf0c6b0681013b3fb9bc2",
"sha256:1f59440b993666a417ba1954cfb1b7fb11cb4dea1a1d2777897009f688d000ee",
"sha256:2b5a3806d9f656c14e9d9b693a344fc5684fdd045155594be0c505c6e9410a94",
"sha256:4a14e2d7c2c0e07b5affcfbfc5c395d767f94bb1a822934a41a3b5371cde1458",
"sha256:4cb50541225208b37786fdb0de632e475c4f00ec4792579df551ef48d6999d69",
"sha256:52999666ad01de885653e1f74a86c2a6520d1004afec475180bebf3d7393a8fc",
"sha256:562c353079e8ce7e2ad611fd7436a72f5df97be72bca59ae9ebf789a724afd5c",
"sha256:5ce2a71f473f4703daa8d6c61a00b35ce625a7f5015b4371e3af728dafca296a",
"sha256:6613e633676168a4500e5e6bb6e3e64d3fdb96d2dc472eb4b99235fb4141adb1",
"sha256:8330406f294df118399c721f80979f2516447bcc73e4262826687872c864751e",
"sha256:8e939dfa7d16609b99eb4d1fd2fc74f7a90f4fd0aaf31d611822daaff456236f",
"sha256:8fa4303e1f50d9f0c8f2f7833b5a370a94d19d41449def62b34ae072126b4dfd",
"sha256:966d987975aa3b4cfcdf1495930ff6ecb152fafe8e544e40633e41b24ca3e1c5",
"sha256:aec4ea43a1b8e9782246a259410f66692f2d3aa0f03c54477e506193b0781cb6",
"sha256:b73f889f032fbef05863f5056b46468a8262ae83628898e20b10bbbb79a3617e",
"sha256:b752088a2f819f163d11dfdbbe627b27eef9d8478c7e57d42c5e7c600fee434e",
"sha256:c8669f96277f140797e0ff99f80bd706271674942672a38ed694e2bfa66f3900",
"sha256:ccf00549efaf6f8d5b35b654beb9aed2b788a5b33b05606eb818ddaa4e924ea3",
"sha256:ce7c91463ad21ac72fc795188292b01c8366cf625e2d1e5ed473ce127b844f60",
"sha256:d776d8d47884e6ad39ff8a301f1ae6b7d2186f209218cf024f43334dbba79c64",
"sha256:dab0f63841aebb2b421fadb31f3c7eef27898f21274a8e5b45c4f2bccb40f9ed",
"sha256:daedcfbf3b24b2b687e35b33252a9315425c2dd06a085a36906d516135bdd60e",
"sha256:e7ad1ec621db2c5ad47924f63561f75abfd4fff669c62c8cc99c169c90432f59",
"sha256:f14fb6c4058772a0d74d82874d3b89d7264d89b4ed7fa0413ea0ef8112b268b9",
"sha256:f16c7b6b98bc400d180f05e65e2236ef4ee9d71f3815280558582670e1e67536",
"sha256:f2d9eb92b26600ae6e8092f66da4bcede1b61a647c9080d6b44c148aff3a8ea4",
"sha256:ffe94f9d17800610dda5282d7f6facfc216d79a93dd728a03d2f21cff3af7cc6"
"sha256:025f94fc1e1364f00e50badc88c47f98af20012f23317234e51a11333ef986e6",
"sha256:02aa7fb282606331aefbc0586e2cf540e9dbe5e343493295e7f390936ad2738e",
"sha256:057210e831573e932702cf332012ed39da78edf0f02d24a3f0b213264a87a397",
"sha256:0d946b79c56187fe139276d4c8ed612a27a616966c8b9779d6b79e2053587c8b",
"sha256:104790893b928d310aae8a955e0bdbaa442fb0ac0a33d1bbb0741c791a407778",
"sha256:15527ef218d95a8717486106553b0d54ff2641e795b65668754e17ab9ca6e381",
"sha256:1826527a0b032f6e20e7ac7f72d7c26dd476a5e5aa82c04aa1c7088a59fded7d",
"sha256:22e3aa4ce1c3eebc7f70f9ca7fd4ce1ea33e8bdb7b61996806cd312f08f84a3a",
"sha256:244e1101e9a48615b9a16cbd194f73c115fdfefc96894803158608115f703b26",
"sha256:24b8c04fdb633a84829d03909752c385faef249c06114cc8d8e1700b95aae5c8",
"sha256:2c276696350785d3104412cbe3ac70ab1e3a10c408e7b20599ee41403a3ed630",
"sha256:2d8474dc833b1182b651b184ace997a7bd83de0f51244de988d3c30e49f07de3",
"sha256:3119b57fe1d964781e91a53e81532c85ed1701baaddec592e22f6b77a9fdf3df",
"sha256:3bee8e7e0709b0fcdaa498a3e513bde9ffc7cd09dbceb11e425bd91c89dbd5b6",
"sha256:436c071e01a464753d30dbfc8768dd93aecf2a8e378e5314d130b95e77b4d612",
"sha256:46635e3f19ad04d5a7d7cf23d232388ddbfccf46d9a3b7436b6abadda4e84813",
"sha256:4772e0b679717e7ac4608d996f57b6f380748a919b457cb05bb941467b888b22",
"sha256:4e2cd80e16f481a62c3175b607373200e714ed29025f21559ebf7524f295689f",
"sha256:52732960efa0e003ca1c092dc0a3c65276e897681287a788a01ca78dda3b41f0",
"sha256:55a7de51ec7d1731b2431886d0349146645f2816e5b8eb982d7c49f89472c9f3",
"sha256:5f8ed5934197a2d4b2087646e98de3e099a237099dcf498b9e38dd3465f74ef4",
"sha256:64b064124fcbc8eb04a155117dc4d9a336e3cda3f069958fbc44fe70c3c3d1e9",
"sha256:65958b8e4319f992e85dad59d8081888b97fcdbde5f0d14bc28f2848b92d3ef1",
"sha256:7683428862e20c6a790c19e64f8ccf487f613fbc83d47e3d532df9c81668d451",
"sha256:78566d5570c75a127c2491e343dc006798a384f06be588fe9b0cbe5595711559",
"sha256:7d1cb00c093dbf1d0b16ccf123e79dee3b82608e4a2a88947695f0460eef13ff",
"sha256:8c74e2a9b594f7962c62cef7680a4cb92a96b4e6e3c2f970790da67cc0213a7e",
"sha256:8e60aa7699170f55f4b0f56ee6f8415229777ac7e4b4b1aa41fc61eec08c1f1d",
"sha256:9447b561529576d89d3bf973e5241a88cf76e45bd101963f5236888713dea774",
"sha256:970055bfeb0be373f2f5299a3db8432444bad3bc2f198753ee6c2a3a781e0959",
"sha256:a6344b8542e584e140dc3c651d68bde51270e79490aa9320f9e708f9b2c39bd5",
"sha256:ce309ca470d747b02ba6069d286a17b7df8e9c94d10d727d9cf3a64e51d85184",
"sha256:cfbd86ed4c2b2ac71bbdbcea6669bf295def7152e3722ddd9dda94ac7981f33d",
"sha256:d7929c513732dff093481f4a0954ed5ff16816365842136b17caa0b4992e49d3"
],
"version": "==3.7.1"
"version": "==3.7.2"
},
"python-levenshtein": {
"hashes": [
@@ -231,10 +239,10 @@
},
"raven": {
"hashes": [
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
],
"version": "==6.9.0"
"version": "==6.10.0"
},
"raven-aiohttp": {
"hashes": [
@@ -280,23 +288,23 @@
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"markers": "python_version >= '3.4'",
"version": "==6.0"
},
"yarl": {
"hashes": [
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
],
"markers": "python_version >= '3.4.1'",
"version": "==1.2.6"
"version": "==1.3.0"
}
},
"develop": {
@@ -336,10 +344,10 @@
},
"alabaster": {
"hashes": [
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.11"
"version": "==0.7.12"
},
"appdirs": {
"hashes": [
@@ -350,18 +358,16 @@
},
"async-timeout": {
"hashes": [
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_version >= '3.5.3'",
"version": "==3.0.0"
"version": "==3.0.1"
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
],
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.2.1"
},
"attrs": {
@@ -387,10 +393,10 @@
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.8.24"
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
@@ -408,10 +414,10 @@
},
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.3.9"
"version": "==0.4.1"
},
"distro": {
"hashes": [
@@ -437,6 +443,13 @@
],
"path": "."
},
"filelock": {
"hashes": [
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
"sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"
],
"version": "==3.0.10"
},
"fuzzywuzzy": {
"hashes": [
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
@@ -446,10 +459,10 @@
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.7"
"version": "==2.8"
},
"idna-ssl": {
"hashes": [
@@ -462,7 +475,6 @@
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.1.0"
},
"jinja2": {
@@ -474,51 +486,78 @@
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.0"
"version": "==1.1.0"
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
],
"version": "==4.3.0"
"version": "==5.0.0"
},
"multidict": {
"hashes": [
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
],
"version": "==4.4.2"
"version": "==4.5.2"
},
"packaging": {
"hashes": [
@@ -529,48 +568,45 @@
},
"pluggy": {
"hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
],
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==0.7.1"
"version": "==0.8.1"
},
"py": {
"hashes": [
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
],
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.6.0"
"version": "==1.7.0"
},
"pygments": {
"hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
],
"version": "==2.2.0"
"version": "==2.3.1"
},
"pyparsing": {
"hashes": [
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
],
"version": "==2.2.2"
"version": "==2.3.0"
},
"pytest": {
"hashes": [
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02",
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"
],
"version": "==3.8.2"
"version": "==4.1.0"
},
"pytest-asyncio": {
"hashes": [
"sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
"sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
],
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==0.9.0"
"version": "==0.10.0"
},
"python-levenshtein": {
"hashes": [
@@ -580,10 +616,10 @@
},
"pytz": {
"hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.5"
"version": "==2018.9"
},
"pyyaml": {
"hashes": [
@@ -603,10 +639,10 @@
},
"raven": {
"hashes": [
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
],
"version": "==6.9.0"
"version": "==6.10.0"
},
"raven-aiohttp": {
"hashes": [
@@ -617,10 +653,10 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.19.1"
"version": "==2.21.0"
},
"schema": {
"hashes": [
@@ -631,10 +667,10 @@
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.11.0"
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
@@ -645,17 +681,17 @@
},
"sphinx": {
"hashes": [
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5",
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"
],
"version": "==1.7.9"
"version": "==1.8.3"
},
"sphinx-rtd-theme": {
"hashes": [
"sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
"sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
"sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09",
"sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96"
],
"version": "==0.4.1"
"version": "==0.4.2"
},
"sphinxcontrib-asyncio": {
"hashes": [
@@ -668,39 +704,36 @@
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42",
"sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957"
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.9.6"
"version": "==0.10.0"
},
"tox": {
"hashes": [
"sha256:7f802b37fffd3b5ef2aab104943fa5dad24bf9564bb7e732e54b8d0cfec2fca0",
"sha256:cc97859bd7f38aa5b3b8ba55ffe7ee9952e7050faad1aedc0829cd3db2fb61d6"
"sha256:2a8d8a63660563e41e64e3b5b677e81ce1ffa5e2a93c2c565d3768c287445800",
"sha256:edfca7809925f49bdc110d0a2d9966bbf35a0c25637216d9586e7a5c5de17bfb"
],
"index": "pypi",
"version": "==3.4.0"
"version": "==3.6.1"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.23"
"version": "==1.24.1"
},
"virtualenv": {
"hashes": [
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
"sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c",
"sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd"
],
"markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==16.0.0"
"version": "==16.2.0"
},
"websockets": {
"hashes": [
@@ -726,23 +759,23 @@
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"markers": "python_version >= '3.4'",
"version": "==6.0"
},
"yarl": {
"hashes": [
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
],
"markers": "python_version >= '3.4.1'",
"version": "==1.2.6"
"version": "==1.3.0"
}
}
}

View File

@@ -1 +1 @@
https://github.com/Rapptz/discord.py/tarball/836ae730401ea370aa10127bb9c86854c8b516ac#egg=discord.py-1.0.0a0
https://github.com/Rapptz/discord.py/tarball/7f4c57dd5ad20b7fa10aea485f674a4bc24b9547#egg=discord.py-1.0.0a0

View File

@@ -374,6 +374,21 @@ API Reference
inside the bot itself! Simply take a peek inside of the :code:`tests/core/test_config.py` file for examples of using
Config in all kinds of ways.
.. important::
When getting, setting or clearing values in Config, all keys are casted to `str` for you. This
includes keys within a `dict` when one is being set, as well as keys in nested dictionaries
within that `dict`. For example::
>>> conf = Config.get_conf(self, identifier=999)
>>> conf.register_global(foo={})
>>> await conf.foo.set_raw(123, value=True)
>>> await conf.foo()
{'123': True}
>>> await conf.foo.set({123: True, 456: {789: False}}
>>> await conf.foo()
{'123': True, '456': {'789': False}}
.. automodule:: redbot.core.config
Config

View File

@@ -359,7 +359,7 @@ class Admin(commands.Cog):
selfroles = await self._valid_selfroles(ctx.guild)
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles)
msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles)
await ctx.send(box(msg, "diff"))
async def _serverlock_check(self, guild: discord.Guild) -> bool:

View File

@@ -288,7 +288,10 @@ class Alias(commands.Cog):
"""Try to execute help for the base command of the alias."""
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
if is_alias:
base_cmd = alias.command[0]
if self.is_command(alias.command):
base_cmd = alias.command
else:
base_cmd = alias.command.rsplit(" ", 1)[0]
new_msg = copy(ctx.message)
new_msg.content = _("{prefix}help {command}").format(

View File

@@ -34,14 +34,14 @@ async def download_lavalink(session):
async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_build())
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_version())
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)
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_version.set(redbot.core.version_info.to_json())
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))

View File

@@ -48,7 +48,7 @@ class Audio(commands.Cog):
"ws_port": "2332",
"password": "youshallnotpass",
"status": False,
"current_build": [3, 0, 0, "alpha", 0],
"current_version": redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
"use_external_lavalink": False,
}
@@ -253,7 +253,9 @@ class Audio(commands.Cog):
dj_enabled = await self.config.guild(ctx.guild).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: {true_or_false}.".format(true_or_false=not dj_enabled))
)
@audioset.command()
@checks.mod_or_permissions(administrator=True)
@@ -332,7 +334,7 @@ class Audio(commands.Cog):
jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"]
msg = "----" + _("Server Settings") + "----"
msg = "----" + _("Server Settings") + "----\n"
if emptydc_enabled:
msg += _("Disconnect timer: [{num_seconds}]\n").format(
num_seconds=self._dynamic_time(emptydc_timer)
@@ -370,7 +372,9 @@ class Audio(commands.Cog):
"""Toggle displaying a thumbnail on audio messages."""
thumbnail = await self.config.guild(ctx.guild).thumbnail()
await self.config.guild(ctx.guild).thumbnail.set(not thumbnail)
await self._embed_msg(ctx, _("Thumbnail display: {}.").format(not thumbnail))
await self._embed_msg(
ctx, _("Thumbnail display: {true_or_false}.").format(true_or_false=not thumbnail)
)
@audioset.command()
@checks.mod_or_permissions(administrator=True)
@@ -565,6 +569,8 @@ class Audio(commands.Cog):
if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author):
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
@@ -1097,7 +1103,7 @@ class Audio(commands.Cog):
(
bold(playlist_name),
_("Tracks: {num}").format(num=len(tracks)),
_("Author: {name}").format(self.bot.get_user(author)),
_("Author: {name}\n").format(name=self.bot.get_user(author)),
)
)
)
@@ -1254,6 +1260,9 @@ class Audio(commands.Cog):
try:
player = lavalink.get_player(ctx.guild.id)
for track in playlists[playlist_name]["tracks"]:
if track["info"]["uri"].startswith("localtracks/"):
if not os.path.isfile(track["info"]["uri"]):
continue
player.add(author_obj, lavalink.rest_api.Track(data=track))
track_count = track_count + 1
embed = discord.Embed(
@@ -1533,9 +1542,9 @@ class Audio(commands.Cog):
)
else:
queue_list += _("Playing: ")
queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
queue_list += _("Requested by: **{user}**").format(user=player.current.requester)
queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n"
queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
queue_list += _("Requested by: **{user}**").format(user=player.current.requester)
queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n"
for i, track in enumerate(
player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start
@@ -1974,6 +1983,7 @@ class Audio(commands.Cog):
async def seek(self, ctx, seconds: int = 30):
"""Seek ahead or behind on a track by seconds."""
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
if not self._player_check(ctx):
return await self._embed_msg(ctx, _("Nothing playing."))
player = lavalink.get_player(ctx.guild.id)
@@ -1986,6 +1996,13 @@ class Audio(commands.Cog):
ctx, ctx.author
):
return await self._embed_msg(ctx, _("You need the DJ role to use seek."))
if vote_enabled:
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
ctx, ctx.author
):
return await self._embed_msg(
ctx, _("There are other people listening - vote to skip instead.")
)
if player.current:
if player.current.is_stream:
return await self._embed_msg(ctx, _("Can't seek on a stream."))
@@ -2360,7 +2377,7 @@ class Audio(commands.Cog):
if await self._check_external():
embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Websocket port set to {}.").format(ws_port),
title=_("Websocket port set to {port}.").format(port=ws_port),
)
embed.set_footer(text=_("External lavalink server set to True."))
await ctx.send(embed=embed)

View File

@@ -71,13 +71,19 @@ async def get_java_version(loop) -> _JavaVersion:
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
# ...
# We only care about the major and minor parts though.
version_line_re = re.compile(r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?"')
version_line_re = re.compile(
r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?(?:-[A-Za-z0-9]+)?"'
)
short_version_re = re.compile(r'version "(?P<major>\d+)"')
lines = version_info.splitlines()
for line in lines:
match = version_line_re.search(line)
short_match = short_version_re.search(line)
if match:
return int(match["major"]), int(match["minor"])
elif short_match:
return int(short_match["major"]), 0
raise RuntimeError(
"The output of `java -version` was unexpected. Please report this issue on Red's "

View File

@@ -1,6 +1,6 @@
import re
from datetime import datetime, timedelta
from typing import Union, List, Callable
from typing import Union, List, Callable, Set
import discord
@@ -94,7 +94,7 @@ class Cleanup(commands.Cog):
):
if message.created_at < two_weeks_ago:
break
if check(message):
if message_filter(message):
collected.append(message)
if number and number <= len(collected):
break
@@ -133,8 +133,6 @@ class Cleanup(commands.Cog):
def check(m):
if text in m.content:
return True
elif m == ctx.message:
return True
else:
return False
@@ -145,6 +143,7 @@ class Cleanup(commands.Cog):
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
author.name, author.id, len(to_delete), text, channel.id
@@ -169,7 +168,7 @@ class Cleanup(commands.Cog):
member = None
try:
member = await commands.converter.MemberConverter().convert(ctx, user)
member = await commands.MemberConverter().convert(ctx, user)
except commands.BadArgument:
try:
_id = int(user)
@@ -188,8 +187,6 @@ class Cleanup(commands.Cog):
def check(m):
if m.author.id == _id:
return True
elif m == ctx.message:
return True
else:
return False
@@ -200,6 +197,8 @@ class Cleanup(commands.Cog):
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = (
"{}({}) deleted {} messages "
" made by {}({}) in channel {}."
@@ -263,6 +262,7 @@ class Cleanup(commands.Cog):
to_delete = await self.get_messages_for_deletion(
channel=channel, number=number, before=before, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name
@@ -323,15 +323,35 @@ class Cleanup(commands.Cog):
if "" in prefixes:
prefixes.remove("")
cc_cog = self.bot.get_cog("CustomCommands")
if cc_cog is not None:
command_names: Set[str] = await cc_cog.get_command_names(ctx.guild)
is_cc = lambda name: name in command_names
else:
is_cc = lambda name: False
alias_cog = self.bot.get_cog("Alias")
if alias_cog is not None:
alias_names: Set[str] = (
set((a.name for a in await alias_cog.unloaded_global_aliases()))
| set(a.name for a in await alias_cog.unloaded_aliases(ctx.guild))
)
is_alias = lambda name: name in alias_names
else:
is_alias = lambda name: False
bot_id = self.bot.user.id
def check(m):
if m.author.id == self.bot.user.id:
if m.author.id == bot_id:
return True
elif m == ctx.message:
return True
p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0:
cmd_name = m.content[len(p) :].split(" ")[0]
return bool(self.bot.get_command(cmd_name))
return (
bool(self.bot.get_command(cmd_name)) or is_alias(cmd_name) or is_cc(cmd_name)
)
return False
to_delete = await self.get_messages_for_deletion(

View File

@@ -3,13 +3,14 @@ import random
from datetime import datetime, timedelta
from inspect import Parameter
from collections import OrderedDict
from typing import Mapping, Tuple, Dict
from typing import Mapping, Tuple, Dict, Set
import discord
from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import menus
from redbot.core.utils.chat_formatting import box, pagify, escape
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("CustomCommands", __file__)
@@ -43,11 +44,8 @@ class CommandObj:
@staticmethod
async def get_commands(config) -> dict:
commands = await config.commands()
customcommands = {k: v for k, v in commands.items() if commands[k]}
if len(customcommands) == 0:
return None
return customcommands
_commands = await config.commands()
return {k: v for k, v in _commands.items() if _commands[k]}
async def get_responses(self, ctx):
intro = _(
@@ -79,7 +77,8 @@ class CommandObj:
responses.append(msg.content)
return responses
def get_now(self) -> str:
@staticmethod
def get_now() -> str:
# Get current time as a string, for 'created_at' and 'edited_at' fields
# in the ccinfo dict
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
@@ -116,7 +115,7 @@ class CommandObj:
*,
response=None,
cooldowns: Mapping[str, int] = None,
ask_for: bool = True
ask_for: bool = True,
):
"""Edit an already existing custom command"""
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
@@ -312,8 +311,6 @@ class CustomCommands(commands.Cog):
Example:
- `[p]customcom edit yourcommand Text you want`
"""
command = command.lower()
try:
await self.commandobj.edit(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully edited."))
@@ -327,12 +324,16 @@ class CustomCommands(commands.Cog):
await ctx.send(e.args[0])
@customcom.command(name="list")
async def cc_list(self, ctx):
"""List all available custom commands."""
@checks.bot_has_permissions(add_reactions=True)
async def cc_list(self, ctx: commands.Context):
"""List all available custom commands.
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
The list displays a preview of each command's response, with
markdown escaped and newlines replaced with spaces.
"""
cc_dict = await CommandObj.get_commands(self.config.guild(ctx.guild))
if not response:
if not cc_dict:
await ctx.send(
_(
"There are no custom commands in this server."
@@ -342,8 +343,7 @@ class CustomCommands(commands.Cog):
return
results = []
for command, body in response.items():
for command, body in sorted(cc_dict.items(), key=lambda t: t[0]):
responses = body["response"]
if isinstance(responses, list):
result = ", ".join(responses)
@@ -351,15 +351,33 @@ class CustomCommands(commands.Cog):
result = responses
else:
continue
results.append("{command:<15} : {result}".format(command=command, result=result))
# Cut preview to 52 characters max
if len(result) > 52:
result = result[:49] + "..."
# Replace newlines with spaces
result = result.replace("\n", " ")
# Escape markdown and mass mentions
result = escape(result, formatting=True, mass_mentions=True)
results.append((f"{ctx.clean_prefix}{command}", result))
commands = "\n".join(results)
if len(commands) < 1500:
await ctx.send(box(commands))
if await ctx.embed_requested():
# We need a space before the newline incase the CC preview ends in link (GH-2295)
content = " \n".join(map("**{0[0]}** {0[1]}".format, results))
pages = list(pagify(content, page_length=1024))
embed_pages = []
for idx, page in enumerate(pages, start=1):
embed = discord.Embed(
title=_("Custom Command List"),
description=page,
colour=await ctx.embed_colour(),
)
embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages)))
embed_pages.append(embed)
await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS)
else:
for page in pagify(commands, delims=[" ", "\n"]):
await ctx.author.send(box(page))
content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results))
pages = list(map(box, pagify(content, page_length=2000, shorten_by=10)))
await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS)
async def on_message(self, message):
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
@@ -411,11 +429,11 @@ class CustomCommands(commands.Cog):
async def cc_command(self, ctx, *cc_args, raw_response, **cc_kwargs) -> None:
cc_args = (*cc_args, *cc_kwargs.values())
results = re.findall(r"\{([^}]+)\}", raw_response)
results = re.findall(r"{([^}]+)\}", raw_response)
for result in results:
param = self.transform_parameter(result, ctx.message)
raw_response = raw_response.replace("{" + result + "}", param)
results = re.findall(r"\{((\d+)[^\.}]*(\.[^:}]+)?[^}]*)\}", raw_response)
results = re.findall(r"{((\d+)[^.}]*(\.[^:}]+)?[^}]*)\}", raw_response)
if results:
low = min(int(result[1]) for result in results)
for result in results:
@@ -424,9 +442,10 @@ class CustomCommands(commands.Cog):
raw_response = raw_response.replace("{" + result[0] + "}", arg)
await ctx.send(raw_response)
def prepare_args(self, raw_response) -> Mapping[str, Parameter]:
args = re.findall(r"\{(\d+)[^:}]*(:[^\.}]*)?[^}]*\}", raw_response)
default = [["ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD)]]
@staticmethod
def prepare_args(raw_response) -> Mapping[str, Parameter]:
args = re.findall(r"{(\d+)[^:}]*(:[^.}]*)?[^}]*\}", raw_response)
default = [("ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD))]
if not args:
return OrderedDict(default)
allowed_builtins = {
@@ -466,7 +485,7 @@ class CustomCommands(commands.Cog):
try:
anno = getattr(discord, anno)
# force an AttributeError if there's no discord.py converter
getattr(commands.converter, anno.__name__ + "Converter")
getattr(commands, anno.__name__ + "Converter")
except AttributeError:
anno = allowed_builtins.get(anno.lower(), Parameter.empty)
if (
@@ -520,7 +539,8 @@ class CustomCommands(commands.Cog):
# only update cooldowns if the command isn't on cooldown
self.cooldowns.update(new_cooldowns)
def transform_arg(self, result, attr, obj) -> str:
@staticmethod
def transform_arg(result, attr, obj) -> str:
attr = attr[1:] # strip initial dot
if not attr:
return str(obj)
@@ -530,7 +550,8 @@ class CustomCommands(commands.Cog):
return raw_result
return str(getattr(obj, attr, raw_result))
def transform_parameter(self, result, message) -> str:
@staticmethod
def transform_parameter(result, message) -> str:
"""
For security reasons only specific objects are allowed
Internals are ignored
@@ -554,3 +575,14 @@ class CustomCommands(commands.Cog):
else:
return raw_result
return str(getattr(first, second, raw_result))
async def get_command_names(self, guild: discord.Guild) -> Set[str]:
"""Get all custom command names in a guild.
Returns
--------
Set[str]
A set of all custom command names.
"""
return set(await CommandObj.get_commands(self.config.guild(guild)))

View File

@@ -1,7 +1,10 @@
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
from .installable import Installable
_ = Translator("Koala", __file__)
class InstalledCog(Installable):
@classmethod

View File

@@ -325,13 +325,12 @@ class Downloader(commands.Cog):
You may only uninstall cogs which were previously installed
by Downloader.
"""
# noinspection PyUnresolvedReferences,PyProtectedMember
real_name = cog.name
poss_installed_path = (await self.cog_install_path()) / real_name
if poss_installed_path.exists():
ctx.bot.unload_extension(real_name)
await self._delete_cog(poss_installed_path)
# noinspection PyTypeChecker
await self._remove_from_installed(cog)
await ctx.send(
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
@@ -344,7 +343,7 @@ class Downloader(commands.Cog):
" files manually if it is still usable."
" Also make sure you've unloaded the cog"
" with `{prefix}unload {cog_name}`."
).format(cog_name=real_name)
).format(prefix=ctx.prefix, cog_name=real_name)
)
@cog.command(name="update")
@@ -372,13 +371,18 @@ class Downloader(commands.Cog):
await self._reinstall_libraries(installed_and_updated)
message = _("Cog update completed successfully.")
cognames = [c.name for c in installed_and_updated]
cognames = {c.name for c in installed_and_updated}
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames)))
else:
await ctx.send(_("All installed cogs are already up to date."))
return
await ctx.send(message)
cognames &= set(ctx.bot.extensions.keys()) # only reload loaded cogs
if not cognames:
return await ctx.send(
_("None of the updated cogs were previously loaded. Update complete.")
)
message = _("Would you like to reload the updated cogs?")
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
if not can_react:
@@ -402,7 +406,6 @@ class Downloader(commands.Cog):
if can_react:
with contextlib.suppress(discord.Forbidden):
await query.clear_reactions()
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
else:
if can_react:
@@ -499,7 +502,7 @@ class Downloader(commands.Cog):
if isinstance(cog_installable, Installable):
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
repo = self._repo_manager.get_repo(cog_installable.repo_name)
repo_url = repo.url
repo_url = _("Missing from installed repos") if repo is None else repo.url
cog_name = cog_installable.name
else:
made_by = "26 & co."

View File

@@ -8,7 +8,7 @@ from typing import cast, Iterable
import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands
from redbot.core import Config, bank, commands, errors
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@@ -171,7 +171,7 @@ class Economy(commands.Cog):
try:
await bank.transfer_credits(from_, to, amount)
except ValueError as e:
except (ValueError, errors.BalanceTooHigh) as e:
return await ctx.send(str(e))
await ctx.send(
@@ -195,36 +195,35 @@ class Economy(commands.Cog):
author = ctx.author
currency = await bank.get_currency_name(ctx.guild)
if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum)
await ctx.send(
_("{author} added {num} {currency} to {user}'s account.").format(
try:
if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum)
msg = _("{author} added {num} {currency} to {user}'s account.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
)
elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum)
await ctx.send(
_("{author} removed {num} {currency} from {user}'s account.").format(
elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum)
msg = _("{author} removed {num} {currency} from {user}'s account.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
)
else:
await bank.set_balance(to, creds.sum)
msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
except (ValueError, errors.BalanceTooHigh) as e:
await ctx.send(str(e))
else:
await bank.set_balance(to, creds.sum)
await ctx.send(
_("{author} set {users}'s account balance to {num} {currency}.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
)
await ctx.send(msg)
@_bank.command()
@check_global_setting_guildowner()
@@ -260,7 +259,18 @@ class Economy(commands.Cog):
if await bank.is_global(): # Role payouts will not be used
next_payday = await self.config.user(author).next_payday()
if cur_time >= next_payday:
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
try:
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}! (**{balance:,}**) "
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(currency=credits_name, new_balance=exc.max_balance)
)
return
next_payday = cur_time + await self.config.PAYDAY_TIME()
await self.config.user(author).next_payday.set(next_payday)
@@ -268,7 +278,7 @@ class Economy(commands.Cog):
await ctx.send(
_(
"{author.mention} Here, take some {currency}. "
"Enjoy! (+{amount} {new_balance}!)\n\n"
"Enjoy! (+{amount} {currency}!)\n\n"
"You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format(
@@ -297,14 +307,25 @@ class Economy(commands.Cog):
).PAYDAY_CREDITS() # Nice variable name
if role_credits > credit_amount:
credit_amount = role_credits
await bank.deposit_credits(author, credit_amount)
try:
await bank.deposit_credits(author, credit_amount)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(currency=credits_name, new_balance=exc.max_balance)
)
return
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author)
await ctx.send(
_(
"{author.mention} Here, take some {currency}. "
"Enjoy! (+{amount} {new_balance}!)\n\n"
"Enjoy! (+{amount} {currency}!)\n\n"
"You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format(
@@ -367,7 +388,7 @@ class Economy(commands.Cog):
@guild_only_check()
async def payouts(self, ctx: commands.Context):
"""Show the payouts for the slot machine."""
await ctx.author.send(SLOT_PAYOUTS_MSG())
await ctx.author.send(SLOT_PAYOUTS_MSG)
@commands.command()
@guild_only_check()
@@ -444,7 +465,21 @@ class Economy(commands.Cog):
then = await bank.get_balance(author)
pay = payout["payout"](bid)
now = then - bid + pay
await bank.set_balance(author, now)
try:
await bank.set_balance(author, now)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await channel.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
).format(
currency=await bank.get_currency_name(getattr(channel, "guild", None)),
old_balance=then,
new_balance=exc.max_balance,
)
)
return
phrase = T_(payout["phrase"])
else:
then = await bank.get_balance(author)
@@ -561,10 +596,10 @@ class Economy(commands.Cog):
async def paydayamount(self, ctx: commands.Context, creds: int):
"""Set the amount earned each payday."""
guild = ctx.guild
credits_name = await bank.get_currency_name(guild)
if creds <= 0:
if creds <= 0 or creds > bank.MAX_BALANCE:
await ctx.send(_("Har har so funny."))
return
credits_name = await bank.get_currency_name(guild)
if await bank.is_global():
await self.config.PAYDAY_CREDITS.set(creds)
else:
@@ -579,6 +614,9 @@ class Economy(commands.Cog):
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
"""Set the amount earned each payday for a role."""
guild = ctx.guild
if creds <= 0 or creds > bank.MAX_BALANCE:
await ctx.send(_("Har har so funny."))
return
credits_name = await bank.get_currency_name(guild)
if await bank.is_global():
await ctx.send(_("The bank must be per-server for per-role paydays to work."))

View File

@@ -28,7 +28,7 @@ class RPSParser:
elif argument == "scissors":
self.choice = RPS.scissors
else:
raise ValueError
self.choice = None
@cog_i18n(_)
@@ -121,6 +121,8 @@ class General(commands.Cog):
"""Play Rock Paper Scissors."""
author = ctx.author
player_choice = your_choice.choice
if not player_choice:
return await ctx.send("This isn't a valid option. Try rock, paper, or scissors.")
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
cond = {
(RPS.rock, RPS.paper): False,
@@ -263,12 +265,13 @@ class General(commands.Cog):
except aiohttp.ClientError:
await ctx.send(
_("No Urban dictionary entries were found, or there was an error in the process")
_("No Urban Dictionary entries were found, or there was an error in the process.")
)
return
if data.get("error") != 404:
if not data["list"]:
return await ctx.send(_("No Urban Dictionary entries were found."))
if await ctx.embed_requested():
# a list of embeds
embeds = []
@@ -303,14 +306,14 @@ class General(commands.Cog):
else:
messages = []
for ud in data["list"]:
ud.set_default("example", "N/A")
ud.setdefault("example", "N/A")
description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048:
description = "{}...".format(description[:2045])
message = _(
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
).format(word=ud.pop("word").capitalize(), description=description, **ud)
messages.append(message)
@@ -325,6 +328,5 @@ class General(commands.Cog):
)
else:
await ctx.send(
_("No Urban dictionary entries were found, or there was an error in the process.")
_("No Urban Dictionary entries were found, or there was an error in the process.")
)
return

View File

@@ -1,66 +0,0 @@
from redbot.core import commands
def mod_or_voice_permissions(**perms):
async def pred(ctx: commands.Context):
author = ctx.author
guild = ctx.guild
if await ctx.bot.is_owner(author) or guild.owner == author:
# Author is bot owner or guild owner
return True
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
mod_role = guild.get_role(await ctx.bot.db.guild(guild).mod_role())
if admin_role in author.roles or mod_role in author.roles:
return True
for vc in guild.voice_channels:
resolved = vc.permissions_for(author)
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.permissions_check(pred)
def admin_or_voice_permissions(**perms):
async def pred(ctx: commands.Context):
author = ctx.author
guild = ctx.guild
if await ctx.bot.is_owner(author) or guild.owner == author:
return True
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
if admin_role in author.roles:
return True
for vc in guild.voice_channels:
resolved = vc.permissions_for(author)
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.permissions_check(pred)
def bot_has_voice_permissions(**perms):
async def pred(ctx: commands.Context):
guild = ctx.guild
for vc in guild.voice_channels:
resolved = vc.permissions_for(guild.me)
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.check(pred)

View File

@@ -2,19 +2,18 @@ import asyncio
import contextlib
from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple
from typing import cast
from typing import cast, Optional
import discord
from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, escape
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions
from redbot.core.utils.chat_formatting import box, escape, format_perms_list
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
_ = T_ = Translator("Mod", __file__)
@@ -193,7 +192,7 @@ class Mod(commands.Cog):
yes_or_no=_("Yes") if respect_hierarchy else _("No")
)
msg += _("Delete delay: {num_seconds}\n").format(
num_seconds=_("{num} seconds").format(delete_delay)
num_seconds=_("{num} seconds").format(num=delete_delay)
if delete_delay != -1
else _("None")
)
@@ -311,13 +310,15 @@ class Mod(commands.Cog):
if not cur_setting:
await self.settings.guild(guild).reinvite_on_unban.set(True)
await ctx.send(
_("Users unbanned with {command} will be reinvited.").format(f"{ctx.prefix}unban")
_("Users unbanned with {command} will be reinvited.").format(
command=f"{ctx.prefix}unban"
)
)
else:
await self.settings.guild(guild).reinvite_on_unban.set(False)
await ctx.send(
_("Users unbanned with {command} will not be reinvited.").format(
f"{ctx.prefix}unban"
command=f"{ctx.prefix}unban"
)
)
@@ -748,7 +749,8 @@ class Mod(commands.Cog):
to send the newly unbanned user
:returns: :class:`Invite`"""
guild = ctx.guild
if guild.me.permissions.manage_guild:
my_perms: discord.Permissions = guild.me.guild_permissions
if my_perms.manage_guild or my_perms.administrator:
if "VANITY_URL" in guild.features:
# guild has a vanity url so use it as the one to send
return await guild.vanity_invite()
@@ -778,15 +780,60 @@ class Mod(commands.Cog):
except discord.HTTPException:
return
@staticmethod
async def _voice_perm_check(
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
) -> bool:
"""Check if the bot and user have sufficient permissions for voicebans.
This also verifies that the user's voice state and connected
channel are not ``None``.
Returns
-------
bool
``True`` if the permissions are sufficient and the user has
a valid voice state.
"""
if user_voice_state is None or user_voice_state.channel is None:
await ctx.send(_("That user is not in a voice channel."))
return False
voice_channel: discord.VoiceChannel = user_voice_state.channel
required_perms = discord.Permissions()
required_perms.update(**perms)
if not voice_channel.permissions_for(ctx.me) >= required_perms:
await ctx.send(
_("I require the {perms} permission(s) in that user's channel to do that.").format(
perms=format_perms_list(required_perms)
)
)
return False
if (
ctx.permission_state is commands.PermState.NORMAL
and not voice_channel.permissions_for(ctx.author) >= required_perms
):
await ctx.send(
_(
"You must have the {perms} permission(s) in that user's channel to use this "
"command."
).format(perms=format_perms_list(required_perms))
)
return False
return True
@commands.command()
@commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
@checks.admin_or_permissions(mute_members=True, deafen_members=True)
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Ban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice
if user_voice_state is None:
await ctx.send(_("No voice state for that user!"))
user_voice_state: discord.VoiceState = user.voice
if (
await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return
needs_mute = True if user_voice_state.mute is False else False
needs_deafen = True if user_voice_state.deaf is False else False
@@ -821,13 +868,15 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Unban a the user from speaking and listening in the server's voice channels."""
"""Unban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice
if user_voice_state is None:
await ctx.send(_("No voice state for that user!"))
if (
await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return
needs_unmute = True if user_voice_state.mute else False
needs_undeafen = True if user_voice_state.deaf else False
@@ -863,68 +912,89 @@ class Mod(commands.Cog):
@commands.guild_only()
@commands.bot_has_permissions(manage_nicknames=True)
@checks.admin_or_permissions(manage_nicknames=True)
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname: str = ""):
"""Change a user's nickname.
Leaving the nickname empty will remove it.
"""
nickname = nickname.strip()
if nickname == "":
me = cast(discord.Member, ctx.me)
if not nickname:
nickname = None
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.")
elif not 2 <= len(nickname) <= 32:
await ctx.send(_("Nicknames must be between 2 and 32 characters long."))
return
if not (
(me.guild_permissions.manage_nicknames or me.guild_permissions.administrator)
and me.top_role > user.top_role
and user != ctx.guild.owner
):
await ctx.send(
_(
"I do not have permission to rename that member. They may be higher than or "
"equal to me in the role hierarchy."
)
)
else:
try:
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
except discord.Forbidden:
# Just in case we missed something in the permissions check above
await ctx.send(_("I do not have permission to rename that member."))
except discord.HTTPException as exc:
if exc.status == 400: # BAD REQUEST
await ctx.send(_("That nickname is invalid."))
else:
await ctx.send(_("An unexpected error has occured."))
else:
await ctx.send(_("Done."))
@commands.group()
@commands.guild_only()
@checks.mod_or_permissions(manage_channel=True)
@checks.mod_or_permissions(manage_channels=True)
async def mute(self, ctx: commands.Context):
"""Mute users."""
pass
@mute.command(name="voice")
@commands.guild_only()
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mute a user in their current voice channel."""
user_voice_state = user.voice
if (
await self._voice_perm_check(
ctx, user_voice_state, mute_members=True, manage_channels=True
)
is False
):
return
guild = ctx.guild
author = ctx.author
if user_voice_state:
channel = user_voice_state.channel
if channel and channel.permissions_for(user).speak:
overwrites = channel.overwrites_for(user)
overwrites.speak = False
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
await ctx.send(
_("Muted {user} in channel {channel.name}").format(user, channel=channel)
channel = user_voice_state.channel
audit_reason = get_audit_reason(author, reason)
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
if success:
await ctx.send(
_("Muted {user} in channel {channel.name}").format(user=user, channel=channel)
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"vmute",
user,
author,
reason,
until=None,
channel=channel,
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"boicemute",
user,
author,
reason,
until=None,
channel=channel,
)
except RuntimeError as e:
await ctx.send(e)
return
elif channel.permissions_for(user).speak is False:
await ctx.send(
_("That user is already muted in {channel}!").format(channel=channel.name)
)
return
else:
await ctx.send(_("That user is not in a voice channel right now!"))
except RuntimeError as e:
await ctx.send(e)
else:
await ctx.send(_("No voice state for the target!"))
return
await ctx.send(issue)
@mute.command(name="channel")
@commands.guild_only()
@@ -937,13 +1007,7 @@ class Mod(commands.Cog):
author = ctx.message.author
channel = ctx.message.channel
guild = ctx.guild
if reason is None:
audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author)
else:
audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format(
a=author, r=reason
)
audit_reason = get_audit_reason(author, reason)
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
@@ -974,26 +1038,12 @@ class Mod(commands.Cog):
"""Mutes user in the server"""
author = ctx.message.author
guild = ctx.guild
if reason is None:
audit_reason = "server mute requested by {author} (ID {author.id})".format(
author=author
)
else:
audit_reason = (
"server mute requested by {author} (ID {author.id}). Reason: {reason}"
).format(author=author, reason=reason)
audit_reason = get_audit_reason(author, reason)
mute_success = []
for channel in guild.channels:
if not isinstance(channel, discord.TextChannel):
if channel.permissions_for(user).speak:
overwrites = channel.overwrites_for(user)
overwrites.speak = False
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
else:
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
mute_success.append((success, issue))
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
mute_success.append((success, issue))
await asyncio.sleep(0.1)
await ctx.send(_("User has been muted in this server."))
try:
@@ -1014,7 +1064,7 @@ class Mod(commands.Cog):
async def mute_user(
self,
guild: discord.Guild,
channel: discord.TextChannel,
channel: discord.abc.GuildChannel,
author: discord.Member,
user: discord.Member,
reason: str,
@@ -1022,80 +1072,83 @@ class Mod(commands.Cog):
"""Mutes the specified user in the specified channel"""
overwrites = channel.overwrites_for(user)
permissions = channel.permissions_for(user)
perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages is False or permissions.send_messages is False:
if permissions.administrator:
return False, T_(mute_unmute_issues["is_admin"])
new_overs = {}
if not isinstance(channel, discord.TextChannel):
new_overs.update(speak=False)
if not isinstance(channel, discord.VoiceChannel):
new_overs.update(send_messages=False, add_reactions=False)
if all(getattr(permissions, p) is False for p in new_overs.keys()):
return False, T_(mute_unmute_issues["already_muted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"])
perms_cache[str(channel.id)] = {
"send_messages": overwrites.send_messages,
"add_reactions": overwrites.add_reactions,
}
overwrites.update(send_messages=False, add_reactions=False)
old_overs = {k: getattr(overwrites, k) for k in new_overs}
overwrites.update(**new_overs)
try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"])
else:
await self.settings.member(user).perms_cache.set(perms_cache)
await self.settings.member(user).set_raw(
"perms_cache", str(channel.id), value=old_overs
)
return True, None
@commands.group()
@commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(manage_channel=True)
@checks.mod_or_permissions(manage_channels=True)
async def unmute(self, ctx: commands.Context):
"""Unmute users."""
pass
@unmute.command(name="voice")
@commands.guild_only()
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def unmute_voice(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmute a user in their current voice channel."""
user_voice_state = user.voice
if user_voice_state:
channel = user_voice_state.channel
if channel and channel.permissions_for(user).speak is False:
overwrites = channel.overwrites_for(user)
overwrites.speak = None
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
author = ctx.author
guild = ctx.guild
await ctx.send(
_("Unmuted {}#{} in channel {}").format(
user.name, user.discriminator, channel.name
)
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"voiceunmute",
user,
author,
reason,
until=None,
channel=channel,
)
except RuntimeError as e:
await ctx.send(e)
elif channel.permissions_for(user).speak:
await ctx.send(_("That user is already unmuted in {}!").format(channel.name))
return
else:
await ctx.send(_("That user is not in a voice channel right now!"))
else:
await ctx.send(_("No voice state for the target!"))
if (
await self._voice_perm_check(
ctx, user_voice_state, mute_members=True, manage_channels=True
)
is False
):
return
guild = ctx.guild
author = ctx.author
channel = user_voice_state.channel
audit_reason = get_audit_reason(author, reason)
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
if success:
await ctx.send(
_("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel)
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"vunmute",
user,
author,
reason,
until=None,
channel=channel,
)
except RuntimeError as e:
await ctx.send(e)
else:
await ctx.send(_("Unmute failed. Reason: {}").format(message))
@checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel")
@@ -1108,8 +1161,9 @@ class Mod(commands.Cog):
channel = ctx.channel
author = ctx.author
guild = ctx.guild
audit_reason = get_audit_reason(author, reason)
success, message = await self.unmute_user(guild, channel, author, user)
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
if success:
await ctx.send(_("User unmuted in this channel."))
@@ -1140,16 +1194,11 @@ class Mod(commands.Cog):
"""Unmute a user in this server."""
guild = ctx.guild
author = ctx.author
audit_reason = get_audit_reason(author, reason)
unmute_success = []
for channel in guild.channels:
if not isinstance(channel, discord.TextChannel):
if channel.permissions_for(user).speak is False:
overwrites = channel.overwrites_for(user)
overwrites.speak = None
audit_reason = get_audit_reason(author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
success, message = await self.unmute_user(guild, channel, author, user)
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
unmute_success.append((success, message))
await asyncio.sleep(0.1)
await ctx.send(_("User has been unmuted in this server."))
@@ -1170,45 +1219,37 @@ class Mod(commands.Cog):
async def unmute_user(
self,
guild: discord.Guild,
channel: discord.TextChannel,
channel: discord.abc.GuildChannel,
author: discord.Member,
user: discord.Member,
reason: str,
) -> (bool, str):
overwrites = channel.overwrites_for(user)
permissions = channel.permissions_for(user)
perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages or permissions.send_messages:
if channel.id in perms_cache:
old_values = perms_cache[channel.id]
else:
old_values = {"send_messages": None, "add_reactions": None, "speak": None}
if all(getattr(overwrites, k) == v for k, v in old_values.items()):
return False, T_(mute_unmute_issues["already_unmuted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"])
if channel.id in perms_cache:
old_values = perms_cache[channel.id]
else:
old_values = {"send_messages": None, "add_reactions": None}
overwrites.update(
send_messages=old_values["send_messages"], add_reactions=old_values["add_reactions"]
)
is_empty = self.are_overwrites_empty(overwrites)
overwrites.update(**old_values)
try:
if not is_empty:
await channel.set_permissions(user, overwrite=overwrites)
else:
if overwrites.is_empty():
await channel.set_permissions(
user, overwrite=cast(discord.PermissionOverwrite, None)
user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason
)
else:
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"])
else:
try:
del perms_cache[channel.id]
except KeyError:
pass
else:
await self.settings.member(user).perms_cache.set(perms_cache)
await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
return True, None
@commands.group()
@@ -1591,8 +1632,9 @@ class Mod(commands.Cog):
"""
An event for modlog case creation
"""
mod_channel = await modlog.get_modlog_channel(case.guild)
if mod_channel is None:
try:
mod_channel = await modlog.get_modlog_channel(case.guild)
except RuntimeError:
return
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
case_content = await case.message_content(use_embeds)
@@ -1694,20 +1736,15 @@ class Mod(commands.Cog):
while len(nick_list) > 20:
nick_list.pop(0)
@staticmethod
def are_overwrites_empty(overwrites):
"""There is currently no cleaner way to check if a
PermissionOverwrite object is empty"""
return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
_ = lambda s: s
mute_unmute_issues = {
"already_muted": _("That user can't send messages in this channel."),
"already_unmuted": _("That user isn't muted in this channel!"),
"already_unmuted": _("That user isn't muted in this channel."),
"hierarchy_problem": _(
"I cannot let you do that. You are not higher than " "the user in the role hierarchy."
"I cannot let you do that. You are not higher than the user in the role hierarchy."
),
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
"permissions_issue": _(
"Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be "

View File

@@ -1,10 +1,133 @@
from typing import NamedTuple, Union, Optional, cast, Type
import itertools
import re
from typing import NamedTuple, Union, Optional
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("PermissionsConverters", __file__)
MENTION_RE = re.compile(r"^<?(?:(?:@[!&]?)?|#)(\d{15,21})>?$")
def _match_id(arg: str) -> Optional[int]:
m = MENTION_RE.match(arg)
if m:
return int(m.group(1))
class GlobalUniqueObjectFinder(commands.Converter):
async def convert(
self, ctx: commands.Context, arg: str
) -> Union[discord.Guild, discord.abc.GuildChannel, discord.abc.User, discord.Role]:
bot: commands.Bot = ctx.bot
_id = _match_id(arg)
if _id is not None:
guild: discord.Guild = bot.get_guild(_id)
if guild is not None:
return guild
channel: discord.abc.GuildChannel = bot.get_channel(_id)
if channel is not None:
return channel
user: discord.User = bot.get_user(_id)
if user is not None:
return user
for guild in bot.guilds:
role: discord.Role = guild.get_role(_id)
if role is not None:
return role
objects = itertools.chain(
bot.get_all_channels(),
bot.users,
bot.guilds,
*(filter(lambda r: not r.is_default(), guild.roles) for guild in bot.guilds),
)
maybe_matches = []
for obj in objects:
if obj.name == arg or str(obj) == arg:
maybe_matches.append(obj)
if ctx.guild is not None:
for member in ctx.guild.members:
if member.nick == arg and not any(obj.id == member.id for obj in maybe_matches):
maybe_matches.append(member)
if not maybe_matches:
raise commands.BadArgument(
_(
'"{arg}" was not found. It must be the ID, mention, or name of a server, '
"channel, user or role which the bot can see."
).format(arg=arg)
)
elif len(maybe_matches) == 1:
return maybe_matches[0]
else:
raise commands.BadArgument(
_(
'"{arg}" does not refer to a unique server, channel, user or role. Please use '
"the ID for whatever/whoever you're trying to specify, or mention it/them."
).format(arg=arg)
)
class GuildUniqueObjectFinder(commands.Converter):
async def convert(
self, ctx: commands.Context, arg: str
) -> Union[discord.abc.GuildChannel, discord.Member, discord.Role]:
guild: discord.Guild = ctx.guild
_id = _match_id(arg)
if _id is not None:
channel: discord.abc.GuildChannel = guild.get_channel(_id)
if channel is not None:
return channel
member: discord.Member = guild.get_member(_id)
if member is not None:
return member
role: discord.Role = guild.get_role(_id)
if role is not None and not role.is_default():
return role
objects = itertools.chain(
guild.channels, guild.members, filter(lambda r: not r.is_default(), guild.roles)
)
maybe_matches = []
for obj in objects:
if obj.name == arg or str(obj) == arg:
maybe_matches.append(obj)
try:
if obj.nick == arg:
maybe_matches.append(obj)
except AttributeError:
pass
if not maybe_matches:
raise commands.BadArgument(
_(
'"{arg}" was not found. It must be the ID, mention, or name of a channel, '
"user or role in this server."
).format(arg=arg)
)
elif len(maybe_matches) == 1:
return maybe_matches[0]
else:
raise commands.BadArgument(
_(
'"{arg}" does not refer to a unique channel, user or role. Please use the ID '
"for whatever/whoever you're trying to specify, or mention it/them."
).format(arg=arg)
)
class CogOrCommand(NamedTuple):
type: str

View File

@@ -14,7 +14,13 @@ from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
from .converters import CogOrCommand, RuleType, ClearableRuleType
from .converters import (
CogOrCommand,
RuleType,
ClearableRuleType,
GuildUniqueObjectFinder,
GlobalUniqueObjectFinder,
)
_ = Translator("Permissions", __file__)
@@ -142,23 +148,20 @@ class Permissions(commands.Cog):
if not command:
return await ctx.send_help()
message = copy(ctx.message)
message.author = user
message.content = "{}{}".format(ctx.prefix, command)
fake_message = copy(ctx.message)
fake_message.author = user
fake_message.content = "{}{}".format(ctx.prefix, command)
com = ctx.bot.get_command(command)
if com is None:
out = _("No such command")
else:
fake_context = await ctx.bot.get_context(fake_message)
try:
testcontext = await ctx.bot.get_context(message, cls=commands.Context)
to_check = [*reversed(com.parents)] + [com]
can = False
for cmd in to_check:
can = await cmd.can_run(testcontext)
if can is False:
break
except commands.CheckFailure:
can = await com.can_run(
fake_context, check_all_parents=True, change_permission_state=False
)
except commands.CommandError:
can = False
out = (
@@ -275,7 +278,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel,
who_or_what: GlobalUniqueObjectFinder,
):
"""Add a global rule to a command.
@@ -303,7 +306,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: commands.GuildPermissionModel,
who_or_what: GuildUniqueObjectFinder,
):
"""Add a rule to a command in this server.
@@ -328,7 +331,7 @@ class Permissions(commands.Cog):
self,
ctx: commands.Context,
cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel,
who_or_what: GlobalUniqueObjectFinder,
):
"""Remove a global rule from a command.
@@ -351,7 +354,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
cog_or_command: CogOrCommand,
*,
who_or_what: commands.GuildPermissionModel,
who_or_what: GuildUniqueObjectFinder,
):
"""Remove a server rule from a command.
@@ -542,7 +545,8 @@ class Permissions(commands.Cog):
continue
conf = self.config.custom(category)
for cmd_name, cmd_rules in rules_dict.items():
await conf.set_raw(cmd_name, guild_id, value=cmd_rules)
cmd_rules = {str(model_id): rule for model_id, rule in cmd_rules.items()}
await conf.set_raw(cmd_name, str(guild_id), value=cmd_rules)
cmd_obj = getter(cmd_name)
if cmd_obj is not None:
self._load_rules_for(cmd_obj, {guild_id: cmd_rules})
@@ -651,14 +655,14 @@ class Permissions(commands.Cog):
if category in old_rules:
for name, rules in old_rules[category].items():
these_rules = new_rules.setdefault(name, {})
guild_rules = these_rules.setdefault(guild_id, {})
guild_rules = these_rules.setdefault(str(guild_id), {})
# Since allow rules would take precedence if the same model ID
# sat in both the allow and deny list, we add the deny entries
# first and let any conflicting allow entries overwrite.
for model_id in rules.get("deny", []):
guild_rules[model_id] = False
guild_rules[str(model_id)] = False
for model_id in rules.get("allow", []):
guild_rules[model_id] = True
guild_rules[str(model_id)] = True
if "default" in rules:
default = rules["default"]
if default == "allow":
@@ -689,7 +693,9 @@ class Permissions(commands.Cog):
"""
for guild_id, guild_dict in _int_key_map(rule_dict.items()):
for model_id, rule in _int_key_map(guild_dict.items()):
if rule is True:
if model_id == "default":
cog_or_command.set_default_rule(rule, guild_id=guild_id)
elif rule is True:
cog_or_command.allow_for(model_id, guild_id=guild_id)
elif rule is False:
cog_or_command.deny_to(model_id, guild_id=guild_id)
@@ -724,9 +730,16 @@ class Permissions(commands.Cog):
rules.
"""
for guild_id, guild_dict in _int_key_map(rule_dict.items()):
for model_id in map(int, guild_dict.keys()):
cog_or_command.clear_rule_for(model_id, guild_id)
for model_id in guild_dict.keys():
if model_id == "default":
cog_or_command.set_default_rule(None, guild_id=guild_id)
else:
cog_or_command.clear_rule_for(int(model_id), guild_id=guild_id)
def _int_key_map(items_view: ItemsView[str, Any]) -> Iterator[Tuple[int, Any]]:
return map(lambda tup: (int(tup[0]), tup[1]), items_view)
def _int_key_map(items_view: ItemsView[str, Any]) -> Iterator[Tuple[Union[str, int], Any]]:
for k, v in items_view:
if k == "default":
yield k, v
else:
yield int(k), v

View File

@@ -316,7 +316,7 @@ class Reports(commands.Cog):
self.tunnel_store[k]["msgs"] = msgs
@commands.guild_only()
@checks.mod_or_permissions(manage_members=True)
@checks.mod_or_permissions(manage_roles=True)
@report.command(name="interact")
async def response(self, ctx, ticket_number: int):
"""Open a message tunnel.

View File

@@ -28,7 +28,7 @@ from . import streamtypes as _streamtypes
from collections import defaultdict
import asyncio
import re
from typing import Optional, List
from typing import Optional, List, Tuple
CHECK_DELAY = 60
@@ -320,6 +320,7 @@ class Streams(commands.Cog):
@commands.group()
@checks.mod()
async def streamset(self, ctx: commands.Context):
"""Set tokens for accessing streams."""
pass
@streamset.command()
@@ -396,9 +397,6 @@ class Streams(commands.Cog):
async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggle a role mention."""
current_setting = await self.db.role(role).mention()
if not role.mentionable:
await ctx.send("That role is not mentionable!")
return
if current_setting:
await self.db.role(role).mention.set(False)
await ctx.send(
@@ -408,11 +406,17 @@ class Streams(commands.Cog):
)
else:
await self.db.role(role).mention.set(True)
await ctx.send(
_(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
)
msg = _(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
if not role.mentionable:
msg += " " + _(
"Since the role is not mentionable, it will be momentarily made mentionable "
"when announcing a streamalert. Please make sure I have the correct "
"permissions to manage this role, or else members of this role won't receive "
"a notification."
)
await ctx.send(msg)
@streamset.command()
@commands.guild_only()
@@ -535,30 +539,46 @@ class Streams(commands.Cog):
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
mention_str, edited_roles = await self._get_mention_str(channel.guild)
if mention_str:
content = _("{mention}, {stream.name} is live!").format(
mention=mention_str, stream=stream
)
else:
content = _("{stream.name} is live!").format(stream=stream.name)
content = _("{stream.name} is live!").format(stream=stream)
m = await channel.send(content, embed=embed)
stream._messages_cache.append(m)
if edited_roles:
for role in edited_roles:
await role.edit(mentionable=False)
await self.save_streams()
async def _get_mention_str(self, guild: discord.Guild):
async def _get_mention_str(self, guild: discord.Guild) -> Tuple[str, List[discord.Role]]:
"""Returns a 2-tuple with the string containing the mentions, and a list of
all roles which need to have their `mentionable` property set back to False.
"""
settings = self.db.guild(guild)
mentions = []
edited_roles = []
if await settings.mention_everyone():
mentions.append("@everyone")
if await settings.mention_here():
mentions.append("@here")
can_manage_roles = guild.me.guild_permissions.manage_roles
for role in guild.roles:
if await self.db.role(role).mention():
if can_manage_roles and not role.mentionable:
try:
await role.edit(mentionable=True)
except discord.Forbidden:
# Might still be unable to edit role based on hierarchy
pass
else:
edited_roles.append(role)
mentions.append(role.mention)
return " ".join(mentions)
return " ".join(mentions), edited_roles
async def check_communities(self):
for community in self.communities:
@@ -589,12 +609,15 @@ class Streams(commands.Cog):
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
mentions, roles = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
if roles:
for role in roles:
await role.edit(mentionable=False)
await self.save_communities()
else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
@@ -626,8 +649,13 @@ class Streams(commands.Cog):
raw_stream["_messages_cache"] = []
for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"])
msg = await chn.get_message(raw_msg["message"])
raw_stream["_messages_cache"].append(msg)
if chn is not None:
try:
msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_stream["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None)
if token is not None:
raw_stream["token"] = token
@@ -646,8 +674,13 @@ class Streams(commands.Cog):
raw_community["_messages_cache"] = []
for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"])
msg = await chn.get_message(raw_msg["message"])
raw_community["_messages_cache"].append(msg)
if chn is not None:
try:
msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_community["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None)
communities.append(_class(token=token, **raw_community))

View File

@@ -114,7 +114,7 @@ class TriviaSession:
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
msg = bold(_("Question number {num}!").format(num=self.count)) + "\n\n" + question
await self.ctx.send(msg)
continue_ = await self.wait_for_answer(answers, delay, timeout)
if continue_ is False:
@@ -322,9 +322,9 @@ def _parse_answers(answers):
for answer in answers:
if isinstance(answer, bool):
if answer is True:
ret.extend(["True", "Yes", _("Yes")])
ret.extend(["True", "Yes", "On"])
else:
ret.extend(["False", "No", _("No")])
ret.extend(["False", "No", "Off"])
else:
ret.append(str(answer))
# Uniquify list

View File

@@ -111,16 +111,14 @@ class Trivia(commands.Cog):
await settings.allow_override.set(enabled)
if enabled:
await ctx.send(
_(
"Done. Trivia lists can now override the trivia settings for this server."
).format(now=enabled)
_("Done. Trivia lists can now override the trivia settings for this server.")
)
else:
await ctx.send(
_(
"Done. Trivia lists can no longer override the trivia settings for this "
"server."
).format(now=enabled)
)
)
@triviaset.command(name="botplays", usage="<true_or_false>")
@@ -506,7 +504,7 @@ class Trivia(commands.Cog):
with path.open(encoding="utf-8") as file:
try:
dict_ = yaml.load(file)
dict_ = yaml.safe_load(file)
except yaml.error.YAMLError as exc:
raise InvalidListError("YAML parsing failed.") from exc
else:

View File

@@ -19,9 +19,11 @@ async def warning_points_add_check(
act = {}
async with guild_settings.actions() as registered_actions:
for a in registered_actions:
# Actions are sorted in decreasing order of points.
# The first action we find where the user is above the threshold will be the
# highest action we can take.
if points >= a["points"]:
act = a
else:
break
if act and act["exceed_command"] is not None: # some action needs to be taken
await create_and_invoke_context(ctx, act["exceed_command"], user)

View File

@@ -9,7 +9,7 @@ from redbot.cogs.warnings.helpers import (
get_command_for_dropping_points,
warning_points_remove_check,
)
from redbot.core import Config, modlog, checks, commands
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.mod import is_admin_or_superior
@@ -34,15 +34,14 @@ class Warnings(commands.Cog):
self.config.register_guild(**self.default_guild)
self.config.register_member(**self.default_member)
self.bot = bot
loop = asyncio.get_event_loop()
loop.create_task(self.register_warningtype())
@staticmethod
async def register_warningtype():
try:
await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
except RuntimeError:
pass
# We're not utilising modlog yet - no need to register a casetype
# @staticmethod
# async def register_warningtype():
# try:
# await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
# except RuntimeError:
# pass
@commands.group()
@commands.guild_only()

View File

@@ -148,5 +148,5 @@ class VersionInfo:
)
__version__ = "3.0.0rc1.post1"
__version__ = "3.0.0rc3"
version_info = VersionInfo.from_str(__version__)

View File

@@ -4,9 +4,10 @@ from typing import Union, List, Optional
import discord
from redbot.core import Config
from . import Config, errors
__all__ = [
"MAX_BALANCE",
"Account",
"get_balance",
"set_balance",
@@ -26,6 +27,8 @@ __all__ = [
"set_default_balance",
]
MAX_BALANCE = 2 ** 63 - 1
_DEFAULT_GLOBAL = {
"is_global": False,
"bank_name": "Twentysix bank",
@@ -170,10 +173,22 @@ async def set_balance(member: discord.Member, amount: int) -> int:
------
ValueError
If attempting to set the balance to a negative number.
BalanceTooHigh
If attempting to set the balance to a value greater than
``bank.MAX_BALANCE``
"""
if amount < 0:
raise ValueError("Not allowed to have negative balance.")
if amount > MAX_BALANCE:
currency = (
await get_currency_name()
if await is_global()
else await get_currency_name(member.guild)
)
raise errors.BalanceTooHigh(
user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency
)
if await is_global():
group = _conf.user(member)
else:

View File

@@ -1,4 +1,5 @@
import asyncio
import inspect
import os
import logging
from collections import Counter
@@ -11,10 +12,10 @@ import discord
import sys
from discord.ext.commands import when_mentioned_or
from . import Config, i18n, commands, errors
from .cog_manager import CogManager
from . import Config, i18n, commands
from .rpc import RPCMixin
from .help_formatter import Help, help as help_
from .rpc import RPCMixin
from .sentry import SentryManager
from .utils import common_filters
@@ -110,7 +111,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
self.main_dir = bot_dir
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
self.cog_mgr = CogManager()
super().__init__(*args, formatter=Help(), **kwargs)
@@ -185,13 +186,23 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
async def is_admin(self, member: discord.Member):
"""Checks if a member is an admin of their guild."""
admin_role = await self.db.guild(member.guild).admin_role()
return any(role.id == admin_role for role in member.roles)
try:
if any(role.id == admin_role for role in member.roles):
return True
except AttributeError: # someone passed a webhook to this
pass
return False
async def is_mod(self, member: discord.Member):
"""Checks if a member is a mod or admin of their guild."""
mod_role = await self.db.guild(member.guild).mod_role()
admin_role = await self.db.guild(member.guild).admin_role()
return any(role.id in (mod_role, admin_role) for role in member.roles)
try:
if any(role.id in (mod_role, admin_role) for role in member.roles):
return True
except AttributeError: # someone passed a webhook to this
pass
return False
async def get_context(self, message, *, cls=commands.Context):
return await super().get_context(message, cls=cls)
@@ -217,7 +228,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
async def load_extension(self, spec: ModuleSpec):
name = spec.name.split(".")[-1]
if name in self.extensions:
raise discord.ClientException(f"there is already a package named {name} loaded")
raise errors.PackageAlreadyLoaded(spec)
lib = spec.loader.load_module()
if not hasattr(lib, "setup"):
@@ -236,20 +247,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
if cog is None:
return
for when in ("before", "after"):
for cls in inspect.getmro(cog.__class__):
try:
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}")
hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
except AttributeError:
pass
else:
self.remove_permissions_hook(hook, when)
try:
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before")
except AttributeError:
pass
else:
self.remove_permissions_hook(hook)
self.remove_permissions_hook(hook)
super().remove_cog(cogname)
@@ -340,8 +344,14 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
ids_to_check = [to_check.id]
else:
author = getattr(to_check, "author", to_check)
ids_to_check = [r.id for r in author.roles]
ids_to_check.append(author.id)
try:
ids_to_check = [r.id for r in author.roles]
except AttributeError:
# webhook messages are a user not member,
# cheaper than isinstance
return True # webhooks require significant permissions to enable.
else:
ids_to_check.append(author.id)
immune_ids = await self.db.guild(guild).autoimmune_ids()
@@ -390,10 +400,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
)
if not hasattr(cog, "requires"):
commands.Cog.__init__(cog)
for cls in inspect.getmro(cog.__class__):
try:
hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
except AttributeError:
pass
else:
self.add_permissions_hook(hook)
for attr in dir(cog):
_attr = getattr(cog, attr)
if attr == f"_{cog.__class__.__name__}__permissions_hook":
self.add_permissions_hook(_attr)
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
_attr, commands.Command
):

View File

@@ -3,7 +3,7 @@ import pkgutil
from importlib import import_module, invalidate_caches
from importlib.machinery import ModuleSpec
from pathlib import Path
from typing import Tuple, Union, List, Optional
from typing import Union, List, Optional
import redbot.cogs
from redbot.core.utils import deduplicate_iterables
@@ -25,8 +25,6 @@ class NoSuchCog(ImportError):
Different from ImportError because some ImportErrors can happen inside cogs.
"""
pass
class CogManager:
"""Directory manager for Red's cogs.
@@ -39,30 +37,27 @@ class CogManager:
CORE_PATH = Path(redbot.cogs.__path__[0])
def __init__(self, paths: Tuple[str] = ()):
def __init__(self):
self.conf = Config.get_conf(self, 2938473984732, True)
tmp_cog_install_path = cog_data_path(self) / "cogs"
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
self.conf.register_global(paths=[], install_path=str(tmp_cog_install_path))
self._paths = [Path(p) for p in paths]
async def paths(self) -> Tuple[Path, ...]:
"""Get all currently valid path directories.
async def paths(self) -> List[Path]:
"""Get all currently valid path directories, in order of priority
Returns
-------
`tuple` of `pathlib.Path`
All valid cog paths.
List[pathlib.Path]
A list of paths where cog packages can be found. The
install path is highest priority, followed by the
user-defined paths, and the core path has the lowest
priority.
"""
conf_paths = [Path(p) for p in await self.conf.paths()]
other_paths = self._paths
all_paths = deduplicate_iterables(conf_paths, other_paths, [self.CORE_PATH])
if self.install_path not in all_paths:
all_paths.insert(0, await self.install_path())
return tuple(p.resolve() for p in all_paths if p.is_dir())
return deduplicate_iterables(
[await self.install_path()], await self.user_defined_paths(), [self.CORE_PATH]
)
async def install_path(self) -> Path:
"""Get the install path for 3rd party cogs.
@@ -73,8 +68,20 @@ class CogManager:
The path to the directory where 3rd party cogs are stored.
"""
p = Path(await self.conf.install_path())
return p.resolve()
return Path(await self.conf.install_path()).resolve()
async def user_defined_paths(self) -> List[Path]:
"""Get a list of user-defined cog paths.
All paths will be absolute and unique, in order of priority.
Returns
-------
List[pathlib.Path]
A list of user-defined paths.
"""
return list(map(Path, deduplicate_iterables(await self.conf.paths())))
async def set_install_path(self, path: Path) -> Path:
"""Set the install path for 3rd party cogs.
@@ -125,11 +132,10 @@ class CogManager:
path = Path(path)
return path
async def add_path(self, path: Union[Path, str]):
async def add_path(self, path: Union[Path, str]) -> None:
"""Add a cog path to current list.
This will ignore duplicates. Does have a side effect of removing all
invalid paths from the saved path list.
This will ignore duplicates.
Parameters
----------
@@ -156,11 +162,12 @@ class CogManager:
if path == self.CORE_PATH:
raise ValueError("Cannot add the core path as an additional path.")
async with self.conf.paths() as paths:
if not any(Path(p) == path for p in paths):
paths.append(str(path))
current_paths = await self.user_defined_paths()
if path not in current_paths:
current_paths.append(path)
await self.set_paths(current_paths)
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
async def remove_path(self, path: Union[Path, str]) -> None:
"""Remove a path from the current paths list.
Parameters
@@ -168,20 +175,12 @@ class CogManager:
path : `pathlib.Path` or `str`
Path to remove.
Returns
-------
`tuple` of `pathlib.Path`
Tuple of new valid paths.
"""
path = self._ensure_path_obj(path).resolve()
paths = await self.user_defined_paths()
paths = [Path(p) for p in await self.conf.paths()]
if path in paths:
paths.remove(path)
await self.set_paths(paths)
return tuple(paths)
paths.remove(path)
await self.set_paths(paths)
async def set_paths(self, paths_: List[Path]):
"""Set the current paths list.
@@ -192,7 +191,7 @@ class CogManager:
List of paths to set.
"""
str_paths = [str(p) for p in paths_]
str_paths = list(map(str, paths_))
await self.conf.paths.set(str_paths)
async def _find_ext_cog(self, name: str) -> ModuleSpec:
@@ -213,9 +212,9 @@ class CogManager:
------
NoSuchCog
When no cog with the requested name was found.
"""
resolved_paths = await self.paths()
real_paths = [str(p) for p in resolved_paths if p != self.CORE_PATH]
real_paths = list(map(str, [await self.install_path()] + await self.user_defined_paths()))
for finder, module_name, _ in pkgutil.iter_modules(real_paths):
if name == module_name:
@@ -287,10 +286,8 @@ class CogManager:
return await self._find_core_cog(name)
async def available_modules(self) -> List[str]:
"""Finds the names of all available modules to load.
"""
paths = (await self.install_path(),) + await self.paths()
paths = [str(p) for p in paths]
"""Finds the names of all available modules to load."""
paths = list(map(str, await self.paths()))
ret = []
for finder, module_name, _ in pkgutil.iter_modules(paths):
@@ -314,13 +311,6 @@ _ = Translator("CogManagerUI", __file__)
class CogManagerUI(commands.Cog):
"""Commands to interface with Red's cog manager."""
@staticmethod
async def visible_paths(ctx):
install_path = await ctx.bot.cog_mgr.install_path()
cog_paths = await ctx.bot.cog_mgr.paths()
cog_paths = [p for p in cog_paths if p != install_path]
return cog_paths
@commands.command()
@checks.is_owner()
async def paths(self, ctx: commands.Context):
@@ -330,8 +320,7 @@ class CogManagerUI(commands.Cog):
cog_mgr = ctx.bot.cog_mgr
install_path = await cog_mgr.install_path()
core_path = cog_mgr.CORE_PATH
cog_paths = await cog_mgr.paths()
cog_paths = [p for p in cog_paths if p not in (install_path, core_path)]
cog_paths = await cog_mgr.user_defined_paths()
msg = _("Install Path: {install_path}\nCore Path: {core_path}\n\n").format(
install_path=install_path, core_path=core_path
@@ -369,7 +358,11 @@ class CogManagerUI(commands.Cog):
from !paths
"""
path_number -= 1
cog_paths = await self.visible_paths(ctx)
if path_number < 0:
await ctx.send(_("Path numbers must be positive."))
return
cog_paths = await ctx.bot.cog_mgr.user_defined_paths()
try:
to_remove = cog_paths.pop(path_number)
except IndexError:
@@ -388,8 +381,11 @@ class CogManagerUI(commands.Cog):
# Doing this because in the paths command they're 1 indexed
from_ -= 1
to -= 1
if from_ < 0 or to < 0:
await ctx.send(_("Path numbers must be positive."))
return
all_paths = await self.visible_paths(ctx)
all_paths = await ctx.bot.cog_mgr.user_defined_paths()
try:
to_move = all_paths.pop(from_)
except IndexError:

View File

@@ -145,7 +145,7 @@ class Command(CogCommandMixin, commands.Command):
@property
def parents(self) -> List["Group"]:
"""List[Group] : Returns all parent commands of this command.
"""List[commands.Group] : Returns all parent commands of this command.
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
If the command has no parents, this will be an empty list.
@@ -157,12 +157,31 @@ class Command(CogCommandMixin, commands.Command):
cmd = cmd.parent
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
async def can_run(self, ctx: "Context") -> bool:
# noinspection PyMethodOverriding
async def can_run(
self,
ctx: "Context",
*,
check_all_parents: bool = False,
change_permission_state: bool = False,
) -> bool:
"""Check if this command can be run in the given context.
This function first checks if the command can be run using
discord.py's method `discord.ext.commands.Command.can_run`,
then will return the result of `Requires.verify`.
Keyword Arguments
-----------------
check_all_parents : bool
If ``True``, this will check permissions for all of this
command's parents and its cog as well as the command
itself. Defaults to ``False``.
change_permission_state : bool
Whether or not the permission state should be changed as
a result of this call. For most cases this should be
``False``. Defaults to ``False``.
"""
ret = await super().can_run(ctx)
if ret is False:
@@ -171,8 +190,21 @@ class Command(CogCommandMixin, commands.Command):
# This is so contexts invoking other commands can be checked with
# this command as well
original_command = ctx.command
original_state = ctx.permission_state
ctx.command = self
if check_all_parents is True:
# Since we're starting from the beginning, we should reset the state to normal
ctx.permission_state = PermState.NORMAL
for parent in reversed(self.parents):
try:
result = await parent.can_run(ctx, change_permission_state=True)
except commands.CommandError:
result = False
if result is False:
return False
if self.parent is None and self.instance is not None:
# For top-level commands, we need to check the cog's requires too
ret = await self.instance.requires.verify(ctx)
@@ -183,6 +215,17 @@ class Command(CogCommandMixin, commands.Command):
return await self.requires.verify(ctx)
finally:
ctx.command = original_command
if not change_permission_state:
ctx.permission_state = original_state
async def _verify_checks(self, ctx):
if not self.enabled:
raise commands.DisabledCommand(f"{self.name} command is disabled")
if not (await self.can_run(ctx, change_permission_state=True)):
raise commands.CheckFailure(
f"The check functions for command {self.qualified_name} failed."
)
async def do_conversion(
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
@@ -238,7 +281,9 @@ class Command(CogCommandMixin, commands.Command):
if cmd.hidden:
return False
try:
can_run = await self.can_run(ctx)
can_run = await self.can_run(
ctx, check_all_parents=True, change_permission_state=False
)
except commands.CheckFailure:
return False
else:

View File

@@ -281,12 +281,14 @@ class Requires:
if isinstance(user_perms, dict):
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
_validate_perms_dict(user_perms)
self.user_perms.update(**user_perms)
else:
self.user_perms = user_perms
if isinstance(bot_perms, dict):
self.bot_perms: discord.Permissions = discord.Permissions.none()
_validate_perms_dict(bot_perms)
self.bot_perms.update(**bot_perms)
else:
self.bot_perms = bot_perms
@@ -311,6 +313,7 @@ class Requires:
if user_perms is None:
func.requires.user_perms = None
else:
_validate_perms_dict(user_perms)
func.requires.user_perms.update(**user_perms)
return func
@@ -417,9 +420,13 @@ class Requires:
"""
await self._verify_bot(ctx)
# Owner-only commands are non-overrideable
# Owner should never be locked out of commands for user permissions.
if await ctx.bot.is_owner(ctx.author):
return True
# Owner-only commands are non-overrideable, and we already checked for owner.
if self.privilege_level is PrivilegeLevel.BOT_OWNER:
return await ctx.bot.is_owner(ctx.author)
return False
hook_result = await ctx.bot.verify_permissions_hooks(ctx)
if hook_result is not None:
@@ -445,7 +452,20 @@ class Requires:
should_invoke = await self._verify_user(ctx)
elif isinstance(next_state, dict):
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
next_state = next_state[await self._verify_user(ctx)]
# We must check what would happen normally, if no explicit rules were set.
default_rule = PermState.NORMAL
if ctx.guild is not None:
default_rule = self.get_default_guild_rule(guild_id=ctx.guild.id)
if default_rule is PermState.NORMAL:
default_rule = self.default_global_rule
if default_rule == PermState.ACTIVE_DENY:
would_invoke = False
elif default_rule == PermState.ACTIVE_ALLOW:
would_invoke = True
else:
would_invoke = await self._verify_user(ctx)
next_state = next_state[would_invoke]
ctx.permission_state = next_state
return should_invoke
@@ -584,6 +604,7 @@ def bot_has_permissions(**perms: bool):
if asyncio.iscoroutinefunction(func):
func.__requires_bot_perms__ = perms
else:
_validate_perms_dict(perms)
func.requires.bot_perms.update(**perms)
return func
@@ -595,6 +616,8 @@ def has_permissions(**perms: bool):
This check can be overridden by rules.
"""
if perms is None:
raise TypeError("Must provide at least one keyword argument to has_permissions")
return Requires.get_decorator(None, perms)
@@ -666,3 +689,20 @@ class _IntKeyDict(Dict[int, _T]):
if not isinstance(key, int):
raise TypeError("Keys must be of type `int`")
return super().__setitem__(key, value)
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
for perm, value in perms.items():
try:
attr = getattr(discord.Permissions, perm)
except AttributeError:
attr = None
if attr is None or not isinstance(attr, property):
# We reject invalid permissions
raise TypeError(f"Unknown permission name '{perm}'")
if value is not True:
# We reject any permission not specified as 'True', since this is the only value which
# makes practical sense.
raise TypeError(f"Permission {perm} may only be specified as 'True', not {value}")

View File

@@ -39,17 +39,21 @@ class _ValueCtxManager(Awaitable[_T], AsyncContextManager[_T]):
async def __aenter__(self):
self.raw_value = await self
self.__original_value = deepcopy(self.raw_value)
if not isinstance(self.raw_value, (list, dict)):
raise TypeError(
"Type of retrieved value must be mutable (i.e. "
"list or dict) in order to use a config value as "
"a context manager."
)
self.__original_value = deepcopy(self.raw_value)
return self.raw_value
async def __aexit__(self, exc_type, exc, tb):
if self.raw_value != self.__original_value:
if isinstance(self.raw_value, dict):
raw_value = _str_key_dict(self.raw_value)
else:
raw_value = self.raw_value
if raw_value != self.__original_value:
await self.value_obj.set(self.raw_value)
@@ -58,7 +62,7 @@ class Value:
Attributes
----------
identifiers : `tuple` of `str`
identifiers : Tuple[str]
This attribute provides all the keys necessary to get a specific data
element from a json document.
default
@@ -69,15 +73,10 @@ class Value:
"""
def __init__(self, identifiers: Tuple[str], default_value, driver):
self._identifiers = identifiers
self.identifiers = identifiers
self.default = default_value
self.driver = driver
@property
def identifiers(self):
return tuple(str(i) for i in self._identifiers)
async def _get(self, default=...):
try:
ret = await self.driver.get(*self.identifiers)
@@ -149,6 +148,8 @@ class Value:
The new literal value of this attribute.
"""
if isinstance(value, dict):
value = _str_key_dict(value)
await self.driver.set(*self.identifiers, value=value)
async def clear(self):
@@ -192,7 +193,10 @@ class Group(Value):
async def _get(self, default: Dict[str, Any] = ...) -> Dict[str, Any]:
default = default if default is not ... else self.defaults
raw = await super()._get(default)
return self.nested_update(raw, default)
if isinstance(raw, dict):
return self.nested_update(raw, default)
else:
return raw
# noinspection PyTypeChecker
def __getattr__(self, item: str) -> Union["Group", Value]:
@@ -238,7 +242,7 @@ class Group(Value):
else:
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
async def clear_raw(self, *nested_path: str):
async def clear_raw(self, *nested_path: Any):
"""
Allows a developer to clear data as if it was stored in a standard
Python dictionary.
@@ -254,44 +258,44 @@ class Group(Value):
Parameters
----------
nested_path : str
nested_path : Any
Multiple arguments that mirror the arguments passed in for nested
dict access.
dict access. These are casted to `str` for you.
"""
path = [str(p) for p in nested_path]
await self.driver.clear(*self.identifiers, *path)
def is_group(self, item: str) -> bool:
def is_group(self, item: Any) -> bool:
"""A helper method for `__getattr__`. Most developers will have no need
to use this.
Parameters
----------
item : str
item : Any
See `__getattr__`.
"""
default = self._defaults.get(item)
default = self._defaults.get(str(item))
return isinstance(default, dict)
def is_value(self, item: str) -> bool:
def is_value(self, item: Any) -> bool:
"""A helper method for `__getattr__`. Most developers will have no need
to use this.
Parameters
----------
item : str
item : Any
See `__getattr__`.
"""
try:
default = self._defaults[item]
default = self._defaults[str(item)]
except KeyError:
return False
return not isinstance(default, dict)
def get_attr(self, item: str):
def get_attr(self, item: Union[int, str]):
"""Manually get an attribute of this Group.
This is available to use as an alternative to using normal Python
@@ -312,7 +316,8 @@ class Group(Value):
Parameters
----------
item : str
The name of the data field in `Config`.
The name of the data field in `Config`. This is casted to
`str` for you.
Returns
-------
@@ -320,9 +325,11 @@ class Group(Value):
The attribute which was requested.
"""
if isinstance(item, int):
item = str(item)
return self.__getattr__(item)
async def get_raw(self, *nested_path: str, default=...):
async def get_raw(self, *nested_path: Any, default=...):
"""
Allows a developer to access data as if it was stored in a standard
Python dictionary.
@@ -345,7 +352,7 @@ class Group(Value):
----------
nested_path : str
Multiple arguments that mirror the arguments passed in for nested
dict access.
dict access. These are casted to `str` for you.
default
Default argument for the value attempting to be accessed. If the
value does not exist the default will be returned.
@@ -410,7 +417,6 @@ class Group(Value):
If no defaults are passed, then the instance attribute 'defaults'
will be used.
"""
if defaults is ...:
defaults = self.defaults
@@ -428,7 +434,7 @@ class Group(Value):
raise ValueError("You may only set the value of a group to be a dict.")
await super().set(value)
async def set_raw(self, *nested_path: str, value):
async def set_raw(self, *nested_path: Any, value):
"""
Allows a developer to set data as if it was stored in a standard
Python dictionary.
@@ -444,13 +450,15 @@ class Group(Value):
Parameters
----------
nested_path : str
nested_path : Any
Multiple arguments that mirror the arguments passed in for nested
dict access.
`dict` access. These are casted to `str` for you.
value
The value to store.
"""
path = [str(p) for p in nested_path]
if isinstance(value, dict):
value = _str_key_dict(value)
await self.driver.set(*self.identifiers, *path, value=value)
@@ -461,9 +469,11 @@ class Config:
`get_core_conf` for Config used in the core package.
.. important::
Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`)
however the process for accessing global data is a bit different. There is no :python:`global` method
because global data is accessed by normal attribute access::
Most config data should be accessed through its respective
group method (e.g. :py:meth:`guild`) however the process for
accessing global data is a bit different. There is no
:python:`global` method because global data is accessed by
normal attribute access::
await conf.foo()
@@ -548,7 +558,7 @@ class Config:
A new Config object.
"""
if cog_instance is None and not cog_name is None:
if cog_instance is None and cog_name is not None:
cog_path_override = cog_data_path(raw_name=cog_name)
else:
cog_path_override = cog_data_path(cog_instance=cog_instance)
@@ -635,11 +645,8 @@ class Config:
def _get_defaults_dict(key: str, value) -> dict:
"""
Since we're allowing nested config stuff now, not storing the
_defaults as a flat dict sounds like a good idea. May turn
out to be an awful one but we'll see.
:param key:
:param value:
:return:
_defaults as a flat dict sounds like a good idea. May turn out
to be an awful one but we'll see.
"""
ret = {}
partial = ret
@@ -655,15 +662,12 @@ class Config:
return ret
@staticmethod
def _update_defaults(to_add: dict, _partial: dict):
def _update_defaults(to_add: Dict[str, Any], _partial: Dict[str, Any]):
"""
This tries to update the _defaults dictionary with the nested
partial dict generated by _get_defaults_dict. This WILL
throw an error if you try to have both a value and a group
registered under the same name.
:param to_add:
:param _partial:
:return:
partial dict generated by _get_defaults_dict. This WILL
throw an error if you try to have both a value and a group
registered under the same name.
"""
for k, v in to_add.items():
val_is_dict = isinstance(v, dict)
@@ -679,7 +683,7 @@ class Config:
else:
_partial[k] = v
def _register_default(self, key: str, **kwargs):
def _register_default(self, key: str, **kwargs: Any):
if key not in self._defaults:
self._defaults[key] = {}
@@ -720,8 +724,8 @@ class Config:
**_defaults
)
You can do the same thing without a :python:`_defaults` dict by using double underscore as a variable
name separator::
You can do the same thing without a :python:`_defaults` dict by
using double underscore as a variable name separator::
# This is equivalent to the previous example
conf.register_global(
@@ -802,7 +806,7 @@ class Config:
The guild's Group object.
"""
return self._get_base_group(self.GUILD, guild.id)
return self._get_base_group(self.GUILD, str(guild.id))
def channel(self, channel: discord.TextChannel) -> Group:
"""Returns a `Group` for the given channel.
@@ -820,7 +824,7 @@ class Config:
The channel's Group object.
"""
return self._get_base_group(self.CHANNEL, channel.id)
return self._get_base_group(self.CHANNEL, str(channel.id))
def role(self, role: discord.Role) -> Group:
"""Returns a `Group` for the given role.
@@ -836,7 +840,7 @@ class Config:
The role's Group object.
"""
return self._get_base_group(self.ROLE, role.id)
return self._get_base_group(self.ROLE, str(role.id))
def user(self, user: discord.abc.User) -> Group:
"""Returns a `Group` for the given user.
@@ -852,7 +856,7 @@ class Config:
The user's Group object.
"""
return self._get_base_group(self.USER, user.id)
return self._get_base_group(self.USER, str(user.id))
def member(self, member: discord.Member) -> Group:
"""Returns a `Group` for the given member.
@@ -866,8 +870,9 @@ class Config:
-------
`Group <redbot.core.config.Group>`
The member's Group object.
"""
return self._get_base_group(self.MEMBER, member.guild.id, member.id)
return self._get_base_group(self.MEMBER, str(member.guild.id), str(member.id))
def custom(self, group_identifier: str, *identifiers: str):
"""Returns a `Group` for the given custom group.
@@ -876,17 +881,17 @@ class Config:
----------
group_identifier : str
Used to identify the custom group.
identifiers : str
The attributes necessary to uniquely identify an entry in the
custom group.
custom group. These are casted to `str` for you.
Returns
-------
`Group <redbot.core.config.Group>`
The custom group's Group object.
"""
return self._get_base_group(group_identifier, *identifiers)
return self._get_base_group(str(group_identifier), *map(str, identifiers))
async def _all_from_scope(self, scope: str) -> Dict[int, Dict[Any, Any]]:
"""Get a dict of all values from a particular scope of data.
@@ -982,7 +987,8 @@ class Config:
"""
return await self._all_from_scope(self.USER)
def _all_members_from_guild(self, group: Group, guild_data: dict) -> dict:
@staticmethod
def _all_members_from_guild(group: Group, guild_data: dict) -> dict:
ret = {}
for member_id, member_data in guild_data.items():
new_member_data = group.defaults
@@ -1026,7 +1032,7 @@ class Config:
for guild_id, guild_data in dict_.items():
ret[int(guild_id)] = self._all_members_from_guild(group, guild_data)
else:
group = self._get_base_group(self.MEMBER, guild.id)
group = self._get_base_group(self.MEMBER, str(guild.id))
try:
guild_data = await self.driver.get(*group.identifiers)
except KeyError:
@@ -1054,7 +1060,8 @@ class Config:
"""
if not scopes:
group = Group(identifiers=[], defaults={}, driver=self.driver)
# noinspection PyTypeChecker
group = Group(identifiers=(), defaults={}, driver=self.driver)
else:
group = self._get_base_group(*scopes)
await group.clear()
@@ -1119,7 +1126,7 @@ class Config:
"""
if guild is not None:
await self._clear_scope(self.MEMBER, guild.id)
await self._clear_scope(self.MEMBER, str(guild.id))
return
await self._clear_scope(self.MEMBER)
@@ -1127,5 +1134,34 @@ class Config:
"""Clear all custom group data.
This resets all custom group data to its registered defaults.
Parameters
----------
group_identifier : str
The identifier for the custom group. This is casted to
`str` for you.
"""
await self._clear_scope(group_identifier)
await self._clear_scope(str(group_identifier))
def _str_key_dict(value: Dict[Any, _T]) -> Dict[str, _T]:
"""
Recursively casts all keys in the given `dict` to `str`.
Parameters
----------
value : Dict[Any, Any]
The `dict` to cast keys to `str`.
Returns
-------
Dict[str, Any]
The `dict` with keys (and nested keys) casted to `str`.
"""
ret = {}
for k, v in value.items():
if isinstance(v, dict):
v = _str_key_dict(v)
ret[str(k)] = v
return ret

View File

@@ -13,7 +13,7 @@ from collections import namedtuple
from pathlib import Path
from random import SystemRandom
from string import ascii_letters, digits
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict
import aiohttp
import discord
@@ -25,6 +25,7 @@ from redbot.core import (
VersionInfo,
checks,
commands,
errors,
i18n,
)
from .utils.predicates import MessagePredicate
@@ -59,7 +60,9 @@ class CoreLogic:
self.bot.register_rpc_handler(self._version_info)
self.bot.register_rpc_handler(self._invite_url)
async def _load(self, cog_names: list):
async def _load(
self, cog_names: Iterable[str]
) -> Tuple[List[str], List[str], List[str], List[str]]:
"""
Loads cogs by name.
Parameters
@@ -69,11 +72,12 @@ class CoreLogic:
Returns
-------
tuple
3 element tuple of loaded, failed, and not found cogs.
4-tuple of loaded, failed, not found and already loaded cogs.
"""
failed_packages = []
loaded_packages = []
notfound_packages = []
alreadyloaded_packages = []
bot = self.bot
@@ -98,6 +102,8 @@ class CoreLogic:
try:
self._cleanup_and_refresh_modules(spec.name)
await bot.load_extension(spec)
except errors.PackageAlreadyLoaded:
alreadyloaded_packages.append(name)
except Exception as e:
log.exception("Package loading failed", exc_info=e)
@@ -109,9 +115,10 @@ class CoreLogic:
await bot.add_loaded_package(name)
loaded_packages.append(name)
return loaded_packages, failed_packages, notfound_packages
return loaded_packages, failed_packages, notfound_packages, alreadyloaded_packages
def _cleanup_and_refresh_modules(self, module_name: str):
@staticmethod
def _cleanup_and_refresh_modules(module_name: str) -> None:
"""Interally reloads modules so that changes are detected"""
splitted = module_name.split(".")
@@ -123,6 +130,7 @@ class CoreLogic:
else:
importlib._bootstrap._exec(lib.__spec__, lib)
# noinspection PyTypeChecker
modules = itertools.accumulate(splitted, "{}.{}".format)
for m in modules:
maybe_reload(m)
@@ -131,7 +139,10 @@ class CoreLogic:
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):
@staticmethod
def _get_package_strings(
packages: List[str], fmt: str, other: Optional[Tuple[str, ...]] = None
) -> str:
"""
Gets the strings needed for the load, unload and reload commands
"""
@@ -147,7 +158,7 @@ class CoreLogic:
final_string = fmt.format(**form)
return final_string
async def _unload(self, cog_names: list):
async def _unload(self, cog_names: Iterable[str]) -> Tuple[List[str], List[str]]:
"""
Unloads cogs with the given names.
@@ -175,14 +186,16 @@ class CoreLogic:
return unloaded_packages, failed_packages
async def _reload(self, cog_names):
async def _reload(
self, cog_names: Sequence[str]
) -> Tuple[List[str], List[str], List[str], List[str]]:
await self._unload(cog_names)
loaded, load_failed, not_found = await self._load(cog_names)
loaded, load_failed, not_found, already_loaded = await self._load(cog_names)
return loaded, load_failed, not_found
return loaded, load_failed, not_found, already_loaded
async def _name(self, name: str = None):
async def _name(self, name: Optional[str] = None) -> str:
"""
Gets or sets the bot's username.
@@ -201,7 +214,7 @@ class CoreLogic:
return self.bot.user.name
async def _prefixes(self, prefixes: list = None):
async def _prefixes(self, prefixes: Optional[Sequence[str]] = None) -> List[str]:
"""
Gets or sets the bot's global prefixes.
@@ -220,7 +233,8 @@ class CoreLogic:
await self.bot.db.prefix.set(prefixes)
return await self.bot.db.prefix()
async def _version_info(self):
@classmethod
async def _version_info(cls) -> Dict[str, str]:
"""
Version information for Red and discord.py
@@ -231,7 +245,7 @@ class CoreLogic:
"""
return {"redbot": __version__, "discordpy": discord.__version__}
async def _invite_url(self):
async def _invite_url(self) -> str:
"""
Generates the invite URL for the bot.
@@ -248,11 +262,8 @@ class CoreLogic:
class Core(commands.Cog, CoreLogic):
"""Commands related to core functions"""
def __init__(self, bot):
super().__init__(bot)
@commands.command(hidden=True)
async def ping(self, ctx):
async def ping(self, ctx: commands.Context):
"""Pong."""
await ctx.send("Pong.")
@@ -313,7 +324,7 @@ class Core(commands.Cog, CoreLogic):
passed = self.get_bot_uptime()
await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since))
def get_bot_uptime(self, *, brief=False):
def get_bot_uptime(self, *, brief: bool = False):
# Courtesy of Danny
now = datetime.datetime.utcnow()
delta = now - self.bot.uptime
@@ -416,7 +427,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def traceback(self, ctx, public: bool = False):
async def traceback(self, ctx: commands.Context, public: bool = False):
"""Sends to the owner the last command exception that has occurred
If public (yes is specified), it will be sent to the chat instead"""
@@ -433,20 +444,20 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def invite(self, ctx):
async def invite(self, ctx: commands.Context):
"""Show's Red's invite url"""
await ctx.author.send(await self._invite_url())
@commands.command()
@commands.guild_only()
@checks.is_owner()
async def leave(self, ctx):
async def leave(self, ctx: commands.Context):
"""Leaves server"""
await ctx.send("Are you sure you want me to leave this server? (y/n)")
pred = MessagePredicate.yes_or_no(ctx)
try:
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
await self.bot.wait_for("message", check=pred)
except asyncio.TimeoutError:
await ctx.send("Response timed out.")
return
@@ -460,7 +471,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def servers(self, ctx):
async def servers(self, ctx: commands.Context):
"""Lists and allows to leave servers"""
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
msg = ""
@@ -502,18 +513,21 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def load(self, ctx, *, cog_name: str):
async def load(self, ctx: commands.Context, *cogs: str):
"""Loads packages"""
cog_names = [c.strip() for c in cog_name.split(" ")]
async with ctx.typing():
loaded, failed, not_found = await self._load(cog_names)
loaded, failed, not_found, already_loaded = await self._load(cogs)
if loaded:
fmt = "Loaded {packs}."
formed = self._get_package_strings(loaded, fmt)
await ctx.send(formed)
if already_loaded:
fmt = "The package{plural} {packs} {other} already loaded."
formed = self._get_package_strings(already_loaded, fmt, ("is", "are"))
await ctx.send(formed)
if failed:
fmt = (
"Failed to load package{plural} {packs}. Check your console or "
@@ -529,12 +543,9 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def unload(self, ctx, *, cog_name: str):
async def unload(self, ctx: commands.Context, *cogs: str):
"""Unloads packages"""
cog_names = [c.strip() for c in cog_name.split(" ")]
unloaded, failed = await self._unload(cog_names)
unloaded, failed = await self._unload(cogs)
if unloaded:
fmt = "Package{plural} {packs} {other} unloaded."
@@ -548,10 +559,10 @@ class Core(commands.Cog, CoreLogic):
@commands.command(name="reload")
@checks.is_owner()
async def reload(self, ctx, *cogs: str):
async def reload(self, ctx: commands.Context, *cogs: str):
"""Reloads packages"""
async with ctx.typing():
loaded, failed, not_found = await self._reload(cogs)
loaded, failed, not_found, already_loaded = await self._reload(cogs)
if loaded:
fmt = "Package{plural} {packs} {other} reloaded."
@@ -570,34 +581,30 @@ class Core(commands.Cog, CoreLogic):
@commands.command(name="shutdown")
@checks.is_owner()
async def _shutdown(self, ctx, silently: bool = False):
async def _shutdown(self, ctx: commands.Context, silently: bool = False):
"""Shuts down the bot"""
wave = "\N{WAVING HAND SIGN}"
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
try: # We don't want missing perms to stop our shutdown
with contextlib.suppress(discord.HTTPException):
if not silently:
await ctx.send(_("Shutting down... ") + wave + skin)
except:
pass
await ctx.bot.shutdown()
@commands.command(name="restart")
@checks.is_owner()
async def _restart(self, ctx, silently: bool = False):
async def _restart(self, ctx: commands.Context, silently: bool = False):
"""Attempts to restart Red
Makes Red quit with exit code 26
The restart is not guaranteed: it must be dealt
with by the process manager in use"""
try:
with contextlib.suppress(discord.HTTPException):
if not silently:
await ctx.send(_("Restarting..."))
except:
pass
await ctx.bot.shutdown(restart=True)
@commands.group(name="set")
async def _set(self, ctx):
async def _set(self, ctx: commands.Context):
"""Changes Red's settings"""
if ctx.invoked_subcommand is None:
if ctx.guild:
@@ -629,7 +636,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.guildowner()
@commands.guild_only()
async def adminrole(self, ctx, *, role: discord.Role):
async def adminrole(self, ctx: commands.Context, *, role: discord.Role):
"""Sets the admin role for this server"""
await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id)
await ctx.send(_("The admin role for this guild has been set."))
@@ -637,7 +644,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.guildowner()
@commands.guild_only()
async def modrole(self, ctx, *, role: discord.Role):
async def modrole(self, ctx: commands.Context, *, role: discord.Role):
"""Sets the mod role for this server"""
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
await ctx.send(_("The mod role for this guild has been set."))
@@ -645,7 +652,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["usebotcolor"])
@checks.guildowner()
@commands.guild_only()
async def usebotcolour(self, ctx):
async def usebotcolour(self, ctx: commands.Context):
"""
Toggle whether to use the bot owner-configured colour for embeds.
@@ -663,7 +670,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.guildowner()
@commands.guild_only()
async def serverfuzzy(self, ctx):
async def serverfuzzy(self, ctx: commands.Context):
"""
Toggle whether to enable fuzzy command search for the server.
@@ -679,7 +686,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.is_owner()
async def fuzzy(self, ctx):
async def fuzzy(self, ctx: commands.Context):
"""
Toggle whether to enable fuzzy command search in DMs.
@@ -695,7 +702,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["color"])
@checks.is_owner()
async def colour(self, ctx, *, colour: discord.Colour = None):
async def colour(self, ctx: commands.Context, *, colour: discord.Colour = None):
"""
Sets a default colour to be used for the bot's embeds.
@@ -713,7 +720,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.is_owner()
async def avatar(self, ctx, url: str):
async def avatar(self, ctx: commands.Context, url: str):
"""Sets Red's avatar"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
@@ -737,7 +744,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="game")
@checks.bot_in_a_guild()
@checks.is_owner()
async def _game(self, ctx, *, game: str = None):
async def _game(self, ctx: commands.Context, *, game: str = None):
"""Sets Red's playing status"""
if game:
@@ -751,7 +758,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="listening")
@checks.bot_in_a_guild()
@checks.is_owner()
async def _listening(self, ctx, *, listening: str = None):
async def _listening(self, ctx: commands.Context, *, listening: str = None):
"""Sets Red's listening status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
@@ -765,7 +772,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="watching")
@checks.bot_in_a_guild()
@checks.is_owner()
async def _watching(self, ctx, *, watching: str = None):
async def _watching(self, ctx: commands.Context, *, watching: str = None):
"""Sets Red's watching status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
@@ -779,7 +786,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.bot_in_a_guild()
@checks.is_owner()
async def status(self, ctx, *, status: str):
async def status(self, ctx: commands.Context, *, status: str):
"""Sets Red's status
Available statuses:
@@ -808,7 +815,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.bot_in_a_guild()
@checks.is_owner()
async def stream(self, ctx, streamer=None, *, stream_title=None):
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
"""Sets Red's streaming status
Leaving both streamer and stream_title empty will clear it."""
@@ -829,7 +836,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="username", aliases=["name"])
@checks.is_owner()
async def _username(self, ctx, *, username: str):
async def _username(self, ctx: commands.Context, *, username: str):
"""Sets Red's username"""
try:
await self._name(name=username)
@@ -848,7 +855,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="nickname")
@checks.admin()
@commands.guild_only()
async def _nickname(self, ctx, *, nickname: str = None):
async def _nickname(self, ctx: commands.Context, *, nickname: str = None):
"""Sets Red's nickname"""
try:
await ctx.guild.me.edit(nick=nickname)
@@ -859,7 +866,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["prefixes"])
@checks.is_owner()
async def prefix(self, ctx, *prefixes):
async def prefix(self, ctx: commands.Context, *prefixes: str):
"""Sets Red's global prefix(es)"""
if not prefixes:
await ctx.send_help()
@@ -870,7 +877,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["serverprefixes"])
@checks.admin()
@commands.guild_only()
async def serverprefix(self, ctx, *prefixes):
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
"""Sets Red's server prefix(es)"""
if not prefixes:
await ctx.bot.db.guild(ctx.guild).prefix.set([])
@@ -882,7 +889,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
async def owner(self, ctx):
async def owner(self, ctx: commands.Context):
"""Sets Red's main owner"""
# According to the Python docs this is suitable for cryptographic use
random = SystemRandom()
@@ -926,7 +933,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command()
@checks.is_owner()
async def token(self, ctx, token: str):
async def token(self, ctx: commands.Context, token: str):
"""Change bot token."""
if not isinstance(ctx.channel, discord.DMChannel):
@@ -1071,7 +1078,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@checks.is_owner()
async def backup(self, ctx, backup_path: str = None):
async def backup(self, ctx: commands.Context, backup_path: str = None):
"""Creates a backup of all data for the instance."""
from redbot.core.data_manager import basic_config, instance_name
from redbot.core.drivers.red_json import JSON
@@ -1080,21 +1087,20 @@ class Core(commands.Cog, CoreLogic):
if basic_config["STORAGE_TYPE"] == "MongoDB":
from redbot.core.drivers.red_mongo import Mongo
m = Mongo("Core", **basic_config["STORAGE_DETAILS"])
m = Mongo("Core", "0", **basic_config["STORAGE_DETAILS"])
db = m.db
collection_names = await db.collection_names(include_system_collections=False)
collection_names = await db.list_collection_names()
for c_name in collection_names:
if c_name == "Core":
c_data_path = data_dir / basic_config["CORE_PATH_APPEND"]
else:
c_data_path = data_dir / basic_config["COG_PATH_APPEND"]
output = {}
c_data_path = data_dir / basic_config["COG_PATH_APPEND"] / c_name
docs = await db[c_name].find().to_list(None)
for item in docs:
item_id = str(item.pop("_id"))
output[item_id] = item
target = JSON(c_name, data_path_override=c_data_path)
await target.jsonIO._threadsafe_save_json(output)
output = item
target = JSON(c_name, item_id, data_path_override=c_data_path)
await target.jsonIO._threadsafe_save_json(output)
backup_filename = "redv3-{}-{}.tar.gz".format(
instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S")
)
@@ -1134,8 +1140,11 @@ class Core(commands.Cog, CoreLogic):
tar.add(str(f), recursive=False)
print(str(backup_file))
await ctx.send(
_("A backup has been made of this instance. It is at {}.").format((backup_file))
_("A backup has been made of this instance. It is at {}.").format(backup_file)
)
if backup_file.stat().st_size > 8_000_000:
await ctx.send(_("This backup is to large to send via DM."))
return
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
pred = MessagePredicate.yes_or_no(ctx)
@@ -1146,10 +1155,18 @@ class Core(commands.Cog, CoreLogic):
else:
if pred.result is True:
await ctx.send(_("OK, it's on its way!"))
async with ctx.author.typing():
await ctx.author.send(
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
try:
async with ctx.author.typing():
await ctx.author.send(
_("Here's a copy of the backup"),
file=discord.File(str(backup_file)),
)
except discord.Forbidden:
await ctx.send(
_("I don't seem to be able to DM you. Do you have closed DMs?")
)
except discord.HTTPException:
await ctx.send(_("I could not send the backup file."))
else:
await ctx.send(_("OK then."))
else:
@@ -1157,7 +1174,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command()
@commands.cooldown(1, 60, commands.BucketType.user)
async def contact(self, ctx, *, message: str):
async def contact(self, ctx: commands.Context, *, message: str):
"""Sends a message to the owner"""
guild = ctx.message.guild
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
@@ -1200,7 +1217,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(
_("I cannot send your message, I'm unable to find my owner... *sigh*")
)
except:
except discord.HTTPException:
await ctx.send(_("I'm unable to deliver your message. Sorry."))
else:
await ctx.send(_("Your message has been sent."))
@@ -1212,14 +1229,14 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(
_("I cannot send your message, I'm unable to find my owner... *sigh*")
)
except:
except discord.HTTPException:
await ctx.send(_("I'm unable to deliver your message. Sorry."))
else:
await ctx.send(_("Your message has been sent."))
@commands.command()
@checks.is_owner()
async def dm(self, ctx, user_id: int, *, message: str):
async def dm(self, ctx: commands.Context, user_id: int, *, message: str):
"""Sends a DM to a user
This command needs a user id to work.
@@ -1253,7 +1270,7 @@ class Core(commands.Cog, CoreLogic):
try:
await destination.send(embed=e)
except:
except discord.HTTPException:
await ctx.send(
_("Sorry, I couldn't deliver your message to {}").format(destination)
)
@@ -1263,7 +1280,7 @@ class Core(commands.Cog, CoreLogic):
response = "{}\nMessage:\n\n{}".format(description, message)
try:
await destination.send("{}\n{}".format(box(response), content))
except:
except discord.HTTPException:
await ctx.send(
_("Sorry, I couldn't deliver your message to {}").format(destination)
)
@@ -1272,7 +1289,7 @@ class Core(commands.Cog, CoreLogic):
@commands.group()
@checks.is_owner()
async def whitelist(self, ctx):
async def whitelist(self, ctx: commands.Context):
"""
Whitelist management commands.
"""
@@ -1290,7 +1307,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User added to whitelist."))
@whitelist.command(name="list")
async def whitelist_list(self, ctx):
async def whitelist_list(self, ctx: commands.Context):
"""
Lists whitelisted users.
"""
@@ -1304,7 +1321,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page))
@whitelist.command(name="remove")
async def whitelist_remove(self, ctx, user: discord.User):
async def whitelist_remove(self, ctx: commands.Context, user: discord.User):
"""
Removes user from whitelist.
"""
@@ -1321,7 +1338,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User was not in the whitelist."))
@whitelist.command(name="clear")
async def whitelist_clear(self, ctx):
async def whitelist_clear(self, ctx: commands.Context):
"""
Clears the whitelist.
"""
@@ -1330,19 +1347,19 @@ class Core(commands.Cog, CoreLogic):
@commands.group()
@checks.is_owner()
async def blacklist(self, ctx):
async def blacklist(self, ctx: commands.Context):
"""
blacklist management commands.
"""
pass
@blacklist.command(name="add")
async def blacklist_add(self, ctx, user: discord.User):
async def blacklist_add(self, ctx: commands.Context, user: discord.User):
"""
Adds a user to the blacklist.
"""
if await ctx.bot.is_owner(user):
ctx.send(_("You cannot blacklist an owner!"))
await ctx.send(_("You cannot blacklist an owner!"))
return
async with ctx.bot.db.blacklist() as curr_list:
@@ -1352,7 +1369,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User added to blacklist."))
@blacklist.command(name="list")
async def blacklist_list(self, ctx):
async def blacklist_list(self, ctx: commands.Context):
"""
Lists blacklisted users.
"""
@@ -1366,7 +1383,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page))
@blacklist.command(name="remove")
async def blacklist_remove(self, ctx, user: discord.User):
async def blacklist_remove(self, ctx: commands.Context, user: discord.User):
"""
Removes user from blacklist.
"""
@@ -1383,7 +1400,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User was not in the blacklist."))
@blacklist.command(name="clear")
async def blacklist_clear(self, ctx):
async def blacklist_clear(self, ctx: commands.Context):
"""
Clears the blacklist.
"""
@@ -1393,14 +1410,14 @@ class Core(commands.Cog, CoreLogic):
@commands.group()
@commands.guild_only()
@checks.admin_or_permissions(administrator=True)
async def localwhitelist(self, ctx):
async def localwhitelist(self, ctx: commands.Context):
"""
Whitelist management commands.
"""
pass
@localwhitelist.command(name="add")
async def localwhitelist_add(self, ctx, *, user_or_role: str):
async def localwhitelist_add(self, ctx: commands.Context, *, user_or_role: str):
"""
Adds a user or role to the whitelist.
"""
@@ -1421,7 +1438,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role added to whitelist."))
@localwhitelist.command(name="list")
async def localwhitelist_list(self, ctx):
async def localwhitelist_list(self, ctx: commands.Context):
"""
Lists whitelisted users and roles.
"""
@@ -1435,7 +1452,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page))
@localwhitelist.command(name="remove")
async def localwhitelist_remove(self, ctx, *, user_or_role: str):
async def localwhitelist_remove(self, ctx: commands.Context, *, user_or_role: str):
"""
Removes user or role from whitelist.
"""
@@ -1465,7 +1482,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role was not in the whitelist."))
@localwhitelist.command(name="clear")
async def localwhitelist_clear(self, ctx):
async def localwhitelist_clear(self, ctx: commands.Context):
"""
Clears the whitelist.
"""
@@ -1475,14 +1492,14 @@ class Core(commands.Cog, CoreLogic):
@commands.group()
@commands.guild_only()
@checks.admin_or_permissions(administrator=True)
async def localblacklist(self, ctx):
async def localblacklist(self, ctx: commands.Context):
"""
blacklist management commands.
"""
pass
@localblacklist.command(name="add")
async def localblacklist_add(self, ctx, *, user_or_role: str):
async def localblacklist_add(self, ctx: commands.Context, *, user_or_role: str):
"""
Adds a user or role to the blacklist.
"""
@@ -1495,7 +1512,7 @@ class Core(commands.Cog, CoreLogic):
user = True
if user and await ctx.bot.is_owner(obj):
ctx.send(_("You cannot blacklist an owner!"))
await ctx.send(_("You cannot blacklist an owner!"))
return
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
@@ -1508,7 +1525,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role added to blacklist."))
@localblacklist.command(name="list")
async def localblacklist_list(self, ctx):
async def localblacklist_list(self, ctx: commands.Context):
"""
Lists blacklisted users and roles.
"""
@@ -1522,7 +1539,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page))
@localblacklist.command(name="remove")
async def localblacklist_remove(self, ctx, *, user_or_role: str):
async def localblacklist_remove(self, ctx: commands.Context, *, user_or_role: str):
"""
Removes user or role from blacklist.
"""
@@ -1552,7 +1569,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role was not in the blacklist."))
@localblacklist.command(name="clear")
async def localblacklist_clear(self, ctx):
async def localblacklist_clear(self, ctx: commands.Context):
"""
Clears the blacklist.
"""
@@ -1692,7 +1709,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.tick()
@commands.guild_only()
@checks.guildowner_or_permissions(manage_server=True)
@checks.guildowner_or_permissions(manage_guild=True)
@commands.group(name="autoimmune")
async def autoimmune_group(self, ctx: commands.Context):
"""
@@ -1703,8 +1720,8 @@ class Core(commands.Cog, CoreLogic):
@autoimmune_group.command(name="list")
async def autoimmune_list(self, ctx: commands.Context):
"""
Get's the current members and roles
Get's the current members and roles
configured for automatic moderation action immunity
"""
ai_ids = await ctx.bot.db.guild(ctx.guild).autoimmune_ids()

View File

@@ -1,15 +1,15 @@
import sys
import os
from pathlib import Path
from typing import List
from copy import deepcopy
import hashlib
import shutil
import inspect
import logging
import os
import sys
import tempfile
from copy import deepcopy
from pathlib import Path
import appdirs
import tempfile
from discord.utils import deprecated
from . import commands
from .json_io import JsonIO
__all__ = [
@@ -153,124 +153,28 @@ def core_data_path() -> Path:
return core_path.resolve()
def _find_data_files(init_location: str) -> (Path, List[Path]):
"""
Discovers all files in the bundled data folder of an installed cog.
Parameters
----------
init_location
Returns
-------
(pathlib.Path, list of pathlib.Path)
"""
init_file = Path(init_location)
if not init_file.is_file():
return []
package_folder = init_file.parent.resolve() / "data"
if not package_folder.is_dir():
return []
all_files = list(package_folder.rglob("*"))
return package_folder, [p.resolve() for p in all_files if p.is_file()]
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
"""
Filters out files from ``to_copy`` that already exist, and are the
same, in ``data_dir``. The files that are different are copied into
``data_dir``.
Parameters
----------
to_copy : list of pathlib.Path
bundled_data_dir : pathlib.Path
cog_data_dir : pathlib.Path
"""
def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
for block in bytesiter:
hasher.update(block)
return hasher.hexdigest() if ashexstr else hasher.digest()
def file_as_blockiter(afile, blocksize=65536):
with afile:
block = afile.read(blocksize)
while len(block) > 0:
yield block
block = afile.read(blocksize)
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) for p in to_copy}
for orig, poss_existing in lookup.items():
if not poss_existing.is_file():
poss_existing.parent.mkdir(exist_ok=True, parents=True)
exists_checksum = None
else:
exists_checksum = hash_bytestr_iter(
file_as_blockiter(poss_existing.open("rb")), hashlib.sha256()
)
orig_checksum = ...
if exists_checksum is not None:
orig_checksum = hash_bytestr_iter(file_as_blockiter(orig.open("rb")), hashlib.sha256())
if exists_checksum != orig_checksum:
shutil.copy(str(orig), str(poss_existing))
log.debug("Copying {} to {}".format(orig, poss_existing))
# noinspection PyUnusedLocal
@deprecated("bundled_data_path() without calling this function")
def load_bundled_data(cog_instance, init_location: str):
pass
def bundled_data_path(cog_instance: commands.Cog) -> Path:
"""
This function copies (and overwrites) data from the ``data/`` folder
of the installed cog.
Get the path to the "data" directory bundled with this cog.
The bundled data folder must be located alongside the ``.py`` file
which contains the cog class.
.. important::
This function MUST be called from the ``setup()`` function of your
cog.
Examples
--------
>>> from redbot.core import data_manager
>>>
>>> def setup(bot):
>>> cog = MyCog()
>>> data_manager.load_bundled_data(cog, __file__)
>>> bot.add_cog(cog)
Parameters
----------
cog_instance
An instance of your cog class.
init_location : str
The ``__file__`` attribute of the file where your ``setup()``
function exists.
"""
bundled_data_folder, to_copy = _find_data_files(init_location)
cog_data_folder = cog_data_path(cog_instance) / "bundled_data"
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
def bundled_data_path(cog_instance) -> Path:
"""
The "data" directory that has been copied from installed cogs.
.. important::
You should *NEVER* write to this directory. Data manager will
overwrite files in this directory each time `load_bundled_data`
is called. You should instead write to the directory provided by
`cog_data_path`.
You should *NEVER* write to this directory.
Parameters
----------
cog_instance
An instance of your cog. If calling from a command or method of
your cog, this should be ``self``.
Returns
-------
@@ -280,10 +184,10 @@ def bundled_data_path(cog_instance) -> Path:
Raises
------
FileNotFoundError
If no bundled data folder exists or if it hasn't been loaded yet.
"""
If no bundled data folder exists.
bundled_path = cog_data_path(cog_instance) / "bundled_data"
"""
bundled_path = Path(inspect.getfile(cog_instance.__class__)).parent / "data"
if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format(bundled_path))

View File

@@ -1,7 +1,12 @@
import motor.motor_asyncio
from .red_base import BaseDriver
import re
from typing import Match, Pattern
from urllib.parse import quote_plus
import motor.core
import motor.motor_asyncio
from .red_base import BaseDriver
__all__ = ["Mongo"]
@@ -80,6 +85,7 @@ class Mongo(BaseDriver):
async def get(self, *identifiers: str):
mongo_collection = self.get_collection()
identifiers = (*map(self._escape_key, identifiers),)
dot_identifiers = ".".join(identifiers)
partial = await mongo_collection.find_one(
@@ -91,10 +97,14 @@ class Mongo(BaseDriver):
for i in identifiers:
partial = partial[i]
if isinstance(partial, dict):
return self._unescape_dict_keys(partial)
return partial
async def set(self, *identifiers: str, value=None):
dot_identifiers = ".".join(identifiers)
dot_identifiers = ".".join(map(self._escape_key, identifiers))
if isinstance(value, dict):
value = self._escape_dict_keys(value)
mongo_collection = self.get_collection()
@@ -105,7 +115,7 @@ class Mongo(BaseDriver):
)
async def clear(self, *identifiers: str):
dot_identifiers = ".".join(identifiers)
dot_identifiers = ".".join(map(self._escape_key, identifiers))
mongo_collection = self.get_collection()
if len(identifiers) > 0:
@@ -115,6 +125,62 @@ class Mongo(BaseDriver):
else:
await mongo_collection.delete_one({"_id": self.unique_cog_identifier})
@staticmethod
def _escape_key(key: str) -> str:
return _SPECIAL_CHAR_PATTERN.sub(_replace_with_escaped, key)
@staticmethod
def _unescape_key(key: str) -> str:
return _CHAR_ESCAPE_PATTERN.sub(_replace_with_unescaped, key)
@classmethod
def _escape_dict_keys(cls, data: dict) -> dict:
"""Recursively escape all keys in a dict."""
ret = {}
for key, value in data.items():
key = cls._escape_key(key)
if isinstance(value, dict):
value = cls._escape_dict_keys(value)
ret[key] = value
return ret
@classmethod
def _unescape_dict_keys(cls, data: dict) -> dict:
"""Recursively unescape all keys in a dict."""
ret = {}
for key, value in data.items():
key = cls._unescape_key(key)
if isinstance(value, dict):
value = cls._unescape_dict_keys(value)
ret[key] = value
return ret
_SPECIAL_CHAR_PATTERN: Pattern[str] = re.compile(r"([.$]|\\U0000002E|\\U00000024)")
_SPECIAL_CHARS = {
".": "\\U0000002E",
"$": "\\U00000024",
"\\U0000002E": "\\U&0000002E",
"\\U00000024": "\\U&00000024",
}
def _replace_with_escaped(match: Match[str]) -> str:
return _SPECIAL_CHARS[match[0]]
_CHAR_ESCAPE_PATTERN: Pattern[str] = re.compile(r"(\\U0000002E|\\U00000024)")
_CHAR_ESCAPES = {
"\\U0000002E": ".",
"\\U00000024": "$",
"\\U&0000002E": "\\U0000002E",
"\\U&00000024": "\\U00000024",
}
def _replace_with_unescaped(match: Match[str]) -> str:
return _CHAR_ESCAPES[match[0]]
def get_config_details():
uri = None

44
redbot/core/errors.py Normal file
View File

@@ -0,0 +1,44 @@
import importlib.machinery
from typing import Optional
import discord
from .i18n import Translator
_ = Translator(__name__, __file__)
class RedError(Exception):
"""Base error class for Red-related errors."""
class PackageAlreadyLoaded(RedError):
"""Raised when trying to load an already-loaded package."""
def __init__(self, spec: importlib.machinery.ModuleSpec, *args, **kwargs):
super().__init__(*args, **kwargs)
self.spec: importlib.machinery.ModuleSpec = spec
def __str__(self) -> str:
return f"There is already a package named {self.spec.name.split('.')[-1]} loaded"
class BankError(RedError):
"""Base error class for bank-related errors."""
class BalanceTooHigh(BankError, OverflowError):
"""Raised when trying to set a user's balance to higher than the maximum."""
def __init__(
self, user: discord.abc.User, max_balance: int, currency_name: str, *args, **kwargs
):
super().__init__(*args, **kwargs)
self.user = user
self.max_balance = max_balance
self.currency_name = currency_name
def __str__(self) -> str:
return _("{user}'s balance cannot rise above {max:,} {currency}.").format(
user=self.user, max=self.max_balance, currency=self.currency_name
)

View File

@@ -15,7 +15,7 @@ from pkg_resources import DistributionNotFound
from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered, humanize_list
from .utils.chat_formatting import inline, bordered, format_perms_list
from .utils import fuzzy_command_search, format_fuzzy_results
log = logging.getLogger("red")
@@ -234,18 +234,13 @@ def init_events(bot, cli_flags):
else:
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
elif isinstance(error, commands.BotMissingPermissions):
missing_perms: List[str] = []
for perm, value in error.missing:
if value is True:
perm_name = '"' + perm.replace("_", " ").title() + '"'
missing_perms.append(perm_name)
if len(missing_perms) == 1:
if bin(error.missing.value).count("1") == 1: # Only one perm missing
plural = ""
else:
plural = "s"
await ctx.send(
"I require the {perms} permission{plural} to execute that command.".format(
perms=humanize_list(missing_perms), plural=plural
perms=format_perms_list(error.missing), plural=plural
)
)
elif isinstance(error, commands.CheckFailure):

View File

@@ -23,6 +23,7 @@ discord.py 1.0.0a
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
"""
import contextlib
from collections import namedtuple
from typing import List, Optional, Union
@@ -224,8 +225,8 @@ class Help(dpy_formatter.HelpFormatter):
return ret
async def format_help_for(self, ctx, command_or_bot, reason: str = None):
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED?
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
"""Formats the help page and handles the actual heavy lifting of how
the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method.
@@ -244,10 +245,24 @@ class Help(dpy_formatter.HelpFormatter):
"""
self.context = ctx
self.command = command_or_bot
# We want the permission state to be set as if the author had run the command he is
# requesting help for. This is so the subcommands shown in the help menu correctly reflect
# any permission rules set.
if isinstance(self.command, commands.Command):
with contextlib.suppress(commands.CommandError):
await self.command.can_run(
self.context, check_all_parents=True, change_permission_state=True
)
elif isinstance(self.command, commands.Cog):
with contextlib.suppress(commands.CommandError):
# Cog's don't have a `can_run` method, so we use the `Requires` object directly.
await self.command.requires.verify(self.context)
emb = await self.format()
if reason:
emb["embed"]["title"] = "{0}".format(reason)
emb["embed"]["title"] = reason
ret = []

View File

@@ -3,8 +3,6 @@ import re
from pathlib import Path
from typing import Callable, Union
from . import commands
__all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"]
_current_locale = "en_us"
@@ -219,6 +217,12 @@ class Translator(Callable[[str], str]):
self.translations.update({untranslated: translated})
# This import to be down here to avoid circular import issues.
# This will be cleaned up at a later date
# noinspection PyPep8
from . import commands
def cog_i18n(translator: Translator):
"""Get a class decorator to link the translator to this cog."""

View File

@@ -3,6 +3,7 @@ import json
import os
import asyncio
import logging
from copy import deepcopy
from uuid import uuid4
# This is basically our old DataIO and just a base for much more elaborate classes
@@ -69,7 +70,11 @@ class JsonIO:
async def _threadsafe_save_json(self, data, settings=PRETTY):
loop = asyncio.get_event_loop()
func = functools.partial(self._save_json, data, settings)
# the deepcopy is needed here. otherwise,
# the dict can change during serialization
# and this will break the encoder.
data_copy = deepcopy(data)
func = functools.partial(self._save_json, data_copy, settings)
async with self._lock:
await loop.run_in_executor(None, func)

View File

@@ -666,29 +666,30 @@ async def register_casetypes(new_types: List[dict]) -> List[CaseType]:
return type_list
async def get_modlog_channel(guild: discord.Guild) -> Union[discord.TextChannel, None]:
async def get_modlog_channel(guild: discord.Guild) -> discord.TextChannel:
"""
Get the current modlog channel
Get the current modlog channel.
Parameters
----------
guild: `discord.Guild`
The guild to get the modlog channel for
The guild to get the modlog channel for.
Returns
-------
`discord.TextChannel` or `None`
The channel object representing the modlog channel
`discord.TextChannel`
The channel object representing the modlog channel.
Raises
------
RuntimeError
If the modlog channel is not found
If the modlog channel is not found.
"""
if hasattr(guild, "get_channel"):
channel = guild.get_channel(await _conf.guild(guild).mod_log())
else:
# For unit tests only
channel = await _conf.guild(guild).mod_log()
if channel is None:
raise RuntimeError("Failed to get the mod log channel!")

View File

@@ -1,5 +1,8 @@
import itertools
from typing import Sequence, Iterator, List
import discord
from redbot.core.i18n import Translator
_ = Translator("UtilsChatFormatting", __file__)
@@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
return text
def humanize_list(items: Sequence[str]):
def humanize_list(items: Sequence[str]) -> str:
"""Get comma-separted list, with the last element joined with *and*.
This uses an Oxford comma, because without one, items containing
@@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
if len(items) == 1:
return items[0]
return ", ".join(items[:-1]) + _(", and ") + items[-1]
def format_perms_list(perms: discord.Permissions) -> str:
"""Format a list of permission names.
This will return a humanized list of the names of all enabled
permissions in the provided `discord.Permissions` object.
Parameters
----------
perms : discord.Permissions
The permissions object with the requested permissions to list
enabled.
Returns
-------
str
The humanized list.
"""
perm_names: List[str] = []
for perm, value in perms:
if value is True:
perm_name = '"' + perm.replace("_", " ").title() + '"'
perm_names.append(perm_name)
return humanize_list(perm_names).replace("Guild", "Server")

View File

@@ -73,10 +73,13 @@ async def menu(
# noinspection PyAsyncCall
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
else:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
try:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
except discord.NotFound:
return
try:
react, user = await ctx.bot.wait_for(
@@ -90,9 +93,12 @@ async def menu(
except discord.Forbidden: # cannot remove all reactions
for key in controls.keys():
await message.remove_reaction(key, ctx.bot.user)
return None
return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji)
except discord.NotFound:
return
else:
return await controls[react.emoji](
ctx, pages, controls, message, page, timeout, react.emoji
)
async def next_page(
@@ -106,10 +112,8 @@ async def next_page(
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == len(pages) - 1:
page = 0 # Loop around to the first item
else:
@@ -128,10 +132,8 @@ async def prev_page(
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == 0:
page = len(pages) - 1 # Loop around to the last item
else:
@@ -148,9 +150,8 @@ async def close_menu(
timeout: float,
emoji: str,
):
if message:
with contextlib.suppress(discord.NotFound):
await message.delete()
return None
def start_adding_reactions(
@@ -161,7 +162,7 @@ def start_adding_reactions(
"""Start adding reactions to a message.
This is a non-blocking operation - calling this will schedule the
reactions being added, but will the calling code will continue to
reactions being added, but the calling code will continue to
execute asynchronously. There is no need to await this function.
This is particularly useful if you wish to start waiting for a
@@ -169,7 +170,7 @@ def start_adding_reactions(
this is exactly what `menu` uses to do that.
This spawns a `asyncio.Task` object and schedules it on ``loop``.
If ``loop`` omitted, the loop will be retreived with
If ``loop`` omitted, the loop will be retrieved with
`asyncio.get_event_loop`.
Parameters

View File

@@ -2,7 +2,6 @@ import discord
from datetime import datetime
from redbot.core.utils.chat_formatting import pagify
import io
import sys
import weakref
from typing import List, Optional
from .common_filters import filter_mass_mentions
@@ -151,15 +150,12 @@ class Tunnel(metaclass=TunnelMeta):
"""
files = []
size = 0
max_size = 8 * 1024 * 1024
for a in m.attachments:
_fp = io.BytesIO()
await a.save(_fp)
size += sys.getsizeof(_fp)
if size > max_size:
return []
files.append(discord.File(_fp, filename=a.filename))
max_size = 8 * 1000 * 1000
if m.attachments and sum(a.size for a in m.attachments) <= max_size:
for a in m.attachments:
_fp = io.BytesIO()
await a.save(_fp)
files.append(discord.File(_fp, filename=a.filename))
return files
async def communicate(

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import argparse
import asyncio
import json
@@ -177,26 +176,21 @@ def basic_setup():
async def json_to_mongo(current_data_dir: Path, storage_details: dict):
from redbot.core.drivers.red_mongo import Mongo
core_data_file = list(current_data_dir.glob("core/settings.json"))[0]
m = Mongo("Core", "0", **storage_details)
core_data_file = current_data_dir / "core" / "settings.json"
driver = Mongo(cog_name="Core", identifier="0", **storage_details)
with core_data_file.open(mode="r") as f:
core_data = json.loads(f.read())
collection = m.get_collection()
await collection.update_one(
{"_id": m.unique_cog_identifier}, update={"$set": core_data["0"]}, upsert=True
)
data = core_data.get("0", {})
for key, value in data.items():
await driver.set(key, value=value)
for p in current_data_dir.glob("cogs/**/settings.json"):
cog_name = p.parent.stem
with p.open(mode="r") as f:
cog_data = json.loads(f.read())
cog_i = None
for ident in list(cog_data.keys()):
cog_i = str(hash(ident))
cog_m = Mongo(p.parent.stem, cog_i, **storage_details)
cog_c = cog_m.get_collection()
for ident in list(cog_data.keys()):
await cog_c.update_one(
{"_id": cog_m.unique_cog_identifier}, update={"$set": cog_data[cog_i]}, upsert=True
)
cog_data = json.load(f)
for identifier, data in cog_data.items():
driver = Mongo(cog_name, identifier, **storage_details)
for key, value in data.items():
await driver.set(key, value=value)
async def mongo_to_json(current_data_dir: Path, storage_details: dict):

View File

@@ -9,59 +9,59 @@ install_requires = [
"aiohttp-json-rpc==0.11.2",
"aiohttp==3.4.4",
"appdirs==1.4.3",
"async-timeout==3.0.0",
"async-timeout==3.0.1",
"attrs==18.2.0",
"chardet==3.0.4",
"colorama==0.3.9",
"colorama==0.4.1",
"discord.py>=1.0.0a0",
"distro==1.3.0; sys_platform == 'linux'",
"fuzzywuzzy==0.17.0",
"idna-ssl==1.1.0",
"idna==2.7",
"multidict==4.4.2",
"idna==2.8",
"multidict==4.5.2",
"python-levenshtein==0.12.0",
"pyyaml==3.13",
"raven==6.9.0",
"raven==6.10.0",
"raven-aiohttp==0.7.0",
"schema==0.6.8",
"websockets==6.0",
"yarl==1.2.6",
"yarl==1.3.0",
]
extras_require = {
"test": [
"atomicwrites==1.2.1",
"more-itertools==4.3.0",
"pluggy==0.7.1",
"py==1.6.0",
"pytest==3.8.2",
"pytest-asyncio==0.9.0",
"six==1.11.0",
"more-itertools==5.0.0",
"pluggy==0.8.1",
"py==1.7.0",
"pytest==4.1.0",
"pytest-asyncio==0.10.0",
"six==1.12.0",
],
"mongo": ["motor==2.0.0", "pymongo==3.7.1", "dnspython==1.15.0"],
"mongo": ["motor==2.0.0", "pymongo==3.7.2", "dnspython==1.16.0"],
"docs": [
"alabaster==0.7.11",
"alabaster==0.7.12",
"babel==2.6.0",
"certifi==2018.8.24",
"certifi==2018.11.29",
"docutils==0.14",
"imagesize==1.1.0",
"Jinja2==2.10",
"MarkupSafe==1.0",
"MarkupSafe==1.1.0",
"packaging==18.0",
"pyparsing==2.2.2",
"Pygments==2.2.0",
"pytz==2018.5",
"requests==2.19.1",
"urllib3==1.23",
"six==1.11.0",
"pyparsing==2.3.0",
"Pygments==2.3.1",
"pytz==2018.9",
"requests==2.21.0",
"six==1.12.0",
"snowballstemmer==1.2.1",
"sphinx==1.7.9",
"sphinx_rtd_theme==0.4.1",
"sphinx==1.8.3",
"sphinx_rtd_theme==0.4.2",
"sphinxcontrib-asyncio==0.2.0",
"sphinxcontrib-websupport==1.1.0",
"urllib3==1.24.1",
],
"voice": ["red-lavalink==0.1.2"],
"style": ["black==18.9b0", "click==7.0", "toml==0.9.6"],
"style": ["black==18.9b0", "click==7.0", "toml==0.10.0"],
}
python_requires = ">=3.6.2,<3.8"

View File

@@ -3,7 +3,7 @@ from redbot.cogs.permissions.permissions import Permissions, GLOBAL
def test_schema_update():
old = {
GLOBAL: {
str(GLOBAL): {
"owner_models": {
"cogs": {
"Admin": {"allow": [78631113035100160], "deny": [96733288462286848]},
@@ -19,7 +19,7 @@ def test_schema_update():
},
}
},
43733288462286848: {
"43733288462286848": {
"owner_models": {
"cogs": {
"Admin": {
@@ -43,22 +43,22 @@ def test_schema_update():
assert new == (
{
"Admin": {
GLOBAL: {78631113035100160: True, 96733288462286848: False},
43733288462286848: {24231113035100160: True, 35533288462286848: False},
str(GLOBAL): {"78631113035100160": True, "96733288462286848": False},
"43733288462286848": {"24231113035100160": True, "35533288462286848": False},
},
"Audio": {GLOBAL: {133049272517001216: True, "default": False}},
"General": {43733288462286848: {133049272517001216: True, "default": False}},
"Audio": {str(GLOBAL): {"133049272517001216": True, "default": False}},
"General": {"43733288462286848": {"133049272517001216": True, "default": False}},
},
{
"cleanup bot": {
GLOBAL: {78631113035100160: True, "default": False},
43733288462286848: {17831113035100160: True, "default": True},
str(GLOBAL): {"78631113035100160": True, "default": False},
"43733288462286848": {"17831113035100160": True, "default": True},
},
"ping": {GLOBAL: {96733288462286848: True, "default": True}},
"ping": {str(GLOBAL): {"96733288462286848": True, "default": True}},
"set adminrole": {
43733288462286848: {
87733288462286848: True,
95433288462286848: False,
"43733288462286848": {
"87733288462286848": True,
"95433288462286848": False,
"default": True,
}
},

View File

@@ -10,7 +10,7 @@ def test_trivia_lists():
for l in list_names:
with l.open() as f:
try:
dict_ = yaml.load(f)
dict_ = yaml.safe_load(f)
except yaml.error.YAMLError as e:
problem_lists.append((l.stem, "YAML error:\n{!s}".format(e)))
else:

View File

@@ -475,3 +475,18 @@ async def test_get_raw_mixes_defaults(config):
subgroup = await config.get_raw("subgroup")
assert subgroup == {"foo": True, "bar": False}
@pytest.mark.asyncio
async def test_cast_str_raw(config):
await config.set_raw(123, 456, value=True)
assert await config.get_raw(123, 456) is True
assert await config.get_raw("123", "456") is True
await config.clear_raw("123", 456)
@pytest.mark.asyncio
async def test_cast_str_nested(config):
config.register_global(foo={})
await config.foo.set({123: True, 456: {789: False}})
assert await config.foo() == {"123": True, "456": {"789": False}}