Compare commits

..

70 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
Toby Harradine
1c706e8c45 Bump version to 3.0.0rc1.post1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 19:25:14 +10:00
Toby Harradine
91029b73e5 Use our own redbot.core.VersionInfo over distutils.StrictVersion (#2188)
* Implements our required subset of PEP 440 in redbot.core.VersionInfo
* Added unit tests for version string parsing and comparisons

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 19:11:05 +10:00
zephyrkul
de4b42a11e [mongo] fix missing URI variable assignment (#2186) 2018-10-06 14:48:03 +10:00
51 changed files with 1814 additions and 1130 deletions

View File

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

453
Pipfile.lock generated
View File

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

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 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. 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 .. automodule:: redbot.core.config
Config Config

View File

@@ -359,7 +359,7 @@ class Admin(commands.Cog):
selfroles = await self._valid_selfroles(ctx.guild) selfroles = await self._valid_selfroles(ctx.guild)
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles]) 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")) await ctx.send(box(msg, "diff"))
async def _serverlock_check(self, guild: discord.Guild) -> bool: 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.""" """Try to execute help for the base command of the alias."""
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name) is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
if is_alias: if is_alias:
base_cmd = alias.command[0] 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 = copy(ctx.message)
new_msg.content = _("{prefix}help {command}").format( 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): async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists() jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo(*await cog.config.current_build()) current_build = redbot.core.VersionInfo.from_json(await cog.config.current_version())
if not jar_exists or current_build < redbot.core.version_info: if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar") log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
async with ClientSession(loop=loop) as session: async with ClientSession(loop=loop) as session:
await download_lavalink(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)) shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))

View File

@@ -48,7 +48,7 @@ class Audio(commands.Cog):
"ws_port": "2332", "ws_port": "2332",
"password": "youshallnotpass", "password": "youshallnotpass",
"status": False, "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, "use_external_lavalink": False,
} }
@@ -253,7 +253,9 @@ class Audio(commands.Cog):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled)) await self._embed_msg(
ctx, _("DJ role enabled: {true_or_false}.".format(true_or_false=not dj_enabled))
)
@audioset.command() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@@ -332,7 +334,7 @@ class Audio(commands.Cog):
jarbuild = redbot.core.__version__ jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"] vote_percent = data["vote_percent"]
msg = "----" + _("Server Settings") + "----" msg = "----" + _("Server Settings") + "----\n"
if emptydc_enabled: if emptydc_enabled:
msg += _("Disconnect timer: [{num_seconds}]\n").format( msg += _("Disconnect timer: [{num_seconds}]\n").format(
num_seconds=self._dynamic_time(emptydc_timer) num_seconds=self._dynamic_time(emptydc_timer)
@@ -370,7 +372,9 @@ class Audio(commands.Cog):
"""Toggle displaying a thumbnail on audio messages.""" """Toggle displaying a thumbnail on audio messages."""
thumbnail = await self.config.guild(ctx.guild).thumbnail() thumbnail = await self.config.guild(ctx.guild).thumbnail()
await self.config.guild(ctx.guild).thumbnail.set(not 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() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@@ -567,6 +571,8 @@ class Audio(commands.Cog):
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS) return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
else: else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS) await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
@local.command(name="search") @local.command(name="search")
async def local_search(self, ctx, *, search_words): async def local_search(self, ctx, *, search_words):
@@ -1097,7 +1103,7 @@ class Audio(commands.Cog):
( (
bold(playlist_name), bold(playlist_name),
_("Tracks: {num}").format(num=len(tracks)), _("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: try:
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
for track in playlists[playlist_name]["tracks"]: 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)) player.add(author_obj, lavalink.rest_api.Track(data=track))
track_count = track_count + 1 track_count = track_count + 1
embed = discord.Embed( embed = discord.Embed(
@@ -1974,6 +1983,7 @@ class Audio(commands.Cog):
async def seek(self, ctx, seconds: int = 30): async def seek(self, ctx, seconds: int = 30):
"""Seek ahead or behind on a track by seconds.""" """Seek ahead or behind on a track by seconds."""
dj_enabled = await self.config.guild(ctx.guild).dj_enabled() 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): if not self._player_check(ctx):
return await self._embed_msg(ctx, _("Nothing playing.")) return await self._embed_msg(ctx, _("Nothing playing."))
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
@@ -1986,6 +1996,13 @@ class Audio(commands.Cog):
ctx, ctx.author ctx, ctx.author
): ):
return await self._embed_msg(ctx, _("You need the DJ role to use seek.")) 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:
if player.current.is_stream: if player.current.is_stream:
return await self._embed_msg(ctx, _("Can't seek on a 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(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=await ctx.embed_colour(), 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.")) embed.set_footer(text=_("External lavalink server set to True."))
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ class RPSParser:
elif argument == "scissors": elif argument == "scissors":
self.choice = RPS.scissors self.choice = RPS.scissors
else: else:
raise ValueError self.choice = None
@cog_i18n(_) @cog_i18n(_)
@@ -121,6 +121,8 @@ class General(commands.Cog):
"""Play Rock Paper Scissors.""" """Play Rock Paper Scissors."""
author = ctx.author author = ctx.author
player_choice = your_choice.choice 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)) red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
cond = { cond = {
(RPS.rock, RPS.paper): False, (RPS.rock, RPS.paper): False,
@@ -263,12 +265,13 @@ class General(commands.Cog):
except aiohttp.ClientError: except aiohttp.ClientError:
await ctx.send( 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 return
if data.get("error") != 404: if data.get("error") != 404:
if not data["list"]:
return await ctx.send(_("No Urban Dictionary entries were found."))
if await ctx.embed_requested(): if await ctx.embed_requested():
# a list of embeds # a list of embeds
embeds = [] embeds = []
@@ -303,14 +306,14 @@ class General(commands.Cog):
else: else:
messages = [] messages = []
for ud in data["list"]: for ud in data["list"]:
ud.set_default("example", "N/A") ud.setdefault("example", "N/A")
description = _("{definition}\n\n**Example:** {example}").format(**ud) description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048: if len(description) > 2048:
description = "{}...".format(description[:2045]) description = "{}...".format(description[:2045])
message = _( message = _(
"<{permalink}>\n {word} by {author}\n\n{description}\n\n" "<{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) ).format(word=ud.pop("word").capitalize(), description=description, **ud)
messages.append(message) messages.append(message)
@@ -325,6 +328,5 @@ class General(commands.Cog):
) )
else: else:
await ctx.send( 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 import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple from collections import deque, defaultdict, namedtuple
from typing import cast from typing import cast, Optional
import discord import discord
from redbot.core import checks, Config, modlog, commands from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, escape from redbot.core.utils.chat_formatting import box, escape, format_perms_list
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions 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 redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log from .log import log
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
_ = T_ = Translator("Mod", __file__) _ = T_ = Translator("Mod", __file__)
@@ -193,7 +192,7 @@ class Mod(commands.Cog):
yes_or_no=_("Yes") if respect_hierarchy else _("No") yes_or_no=_("Yes") if respect_hierarchy else _("No")
) )
msg += _("Delete delay: {num_seconds}\n").format( 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 if delete_delay != -1
else _("None") else _("None")
) )
@@ -311,13 +310,15 @@ class Mod(commands.Cog):
if not cur_setting: if not cur_setting:
await self.settings.guild(guild).reinvite_on_unban.set(True) await self.settings.guild(guild).reinvite_on_unban.set(True)
await ctx.send( 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: else:
await self.settings.guild(guild).reinvite_on_unban.set(False) await self.settings.guild(guild).reinvite_on_unban.set(False)
await ctx.send( await ctx.send(
_("Users unbanned with {command} will not be reinvited.").format( _("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 to send the newly unbanned user
:returns: :class:`Invite`""" :returns: :class:`Invite`"""
guild = ctx.guild 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: if "VANITY_URL" in guild.features:
# guild has a vanity url so use it as the one to send # guild has a vanity url so use it as the one to send
return await guild.vanity_invite() return await guild.vanity_invite()
@@ -778,15 +780,60 @@ class Mod(commands.Cog):
except discord.HTTPException: except discord.HTTPException:
return 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.command()
@commands.guild_only() @commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True) @checks.admin_or_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): 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.""" """Ban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice user_voice_state: discord.VoiceState = user.voice
if user_voice_state is None: if (
await ctx.send(_("No voice state for that user!")) await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return return
needs_mute = True if user_voice_state.mute is False else False needs_mute = True if user_voice_state.mute is False else False
needs_deafen = True if user_voice_state.deaf 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.command()
@commands.guild_only() @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): 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 user_voice_state = user.voice
if user_voice_state is None: if (
await ctx.send(_("No voice state for that user!")) await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return return
needs_unmute = True if user_voice_state.mute else False needs_unmute = True if user_voice_state.mute else False
needs_undeafen = True if user_voice_state.deaf else False needs_undeafen = True if user_voice_state.deaf else False
@@ -863,49 +912,79 @@ class Mod(commands.Cog):
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_nicknames=True) @commands.bot_has_permissions(manage_nicknames=True)
@checks.admin_or_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. """Change a user's nickname.
Leaving the nickname empty will remove it. Leaving the nickname empty will remove it.
""" """
nickname = nickname.strip() nickname = nickname.strip()
if nickname == "": me = cast(discord.Member, ctx.me)
if not nickname:
nickname = None nickname = None
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) await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.") 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.group()
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channels=True)
async def mute(self, ctx: commands.Context): async def mute(self, ctx: commands.Context):
"""Mute users.""" """Mute users."""
pass pass
@mute.command(name="voice") @mute.command(name="voice")
@commands.guild_only() @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): async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mute a user in their current voice channel.""" """Mute a user in their current voice channel."""
user_voice_state = user.voice 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 guild = ctx.guild
author = ctx.author author = ctx.author
if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
if channel and channel.permissions_for(user).speak: audit_reason = get_audit_reason(author, reason)
overwrites = channel.overwrites_for(user)
overwrites.speak = False success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) if success:
await ctx.send( await ctx.send(
_("Muted {user} in channel {channel.name}").format(user, channel=channel) _("Muted {user} in channel {channel.name}").format(user=user, channel=channel)
) )
try: try:
await modlog.create_case( await modlog.create_case(
self.bot, self.bot,
guild, guild,
ctx.message.created_at, ctx.message.created_at,
"boicemute", "vmute",
user, user,
author, author,
reason, reason,
@@ -914,17 +993,8 @@ class Mod(commands.Cog):
) )
except RuntimeError as e: except RuntimeError as e:
await ctx.send(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: else:
await ctx.send(_("That user is not in a voice channel right now!")) await ctx.send(issue)
else:
await ctx.send(_("No voice state for the target!"))
return
@mute.command(name="channel") @mute.command(name="channel")
@commands.guild_only() @commands.guild_only()
@@ -937,13 +1007,7 @@ class Mod(commands.Cog):
author = ctx.message.author author = ctx.message.author
channel = ctx.message.channel channel = ctx.message.channel
guild = ctx.guild guild = ctx.guild
audit_reason = get_audit_reason(author, reason)
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
)
success, issue = await self.mute_user(guild, channel, author, user, audit_reason) success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
@@ -974,24 +1038,10 @@ class Mod(commands.Cog):
"""Mutes user in the server""" """Mutes user in the server"""
author = ctx.message.author author = ctx.message.author
guild = ctx.guild guild = ctx.guild
if reason is None: audit_reason = get_audit_reason(author, reason)
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)
mute_success = [] mute_success = []
for channel in guild.channels: 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) success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
mute_success.append((success, issue)) mute_success.append((success, issue))
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -1014,7 +1064,7 @@ class Mod(commands.Cog):
async def mute_user( async def mute_user(
self, self,
guild: discord.Guild, guild: discord.Guild,
channel: discord.TextChannel, channel: discord.abc.GuildChannel,
author: discord.Member, author: discord.Member,
user: discord.Member, user: discord.Member,
reason: str, reason: str,
@@ -1022,64 +1072,73 @@ class Mod(commands.Cog):
"""Mutes the specified user in the specified channel""" """Mutes the specified user in the specified channel"""
overwrites = channel.overwrites_for(user) overwrites = channel.overwrites_for(user)
permissions = channel.permissions_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"]) return False, T_(mute_unmute_issues["already_muted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"]) return False, T_(mute_unmute_issues["hierarchy_problem"])
perms_cache[str(channel.id)] = { old_overs = {k: getattr(overwrites, k) for k in new_overs}
"send_messages": overwrites.send_messages, overwrites.update(**new_overs)
"add_reactions": overwrites.add_reactions,
}
overwrites.update(send_messages=False, add_reactions=False)
try: try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason) await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden: except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"]) return False, T_(mute_unmute_issues["permissions_issue"])
else: 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 return True, None
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_roles=True) @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): async def unmute(self, ctx: commands.Context):
"""Unmute users.""" """Unmute users."""
pass pass
@unmute.command(name="voice") @unmute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def unmute_voice( async def unmute_voice(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None self, ctx: commands.Context, user: discord.Member, *, reason: str = None
): ):
"""Unmute a user in their current voice channel.""" """Unmute a user in their current voice channel."""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state: if (
channel = user_voice_state.channel await self._voice_perm_check(
if channel and channel.permissions_for(user).speak is False: ctx, user_voice_state, mute_members=True, manage_channels=True
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
) )
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: try:
await modlog.create_case( await modlog.create_case(
self.bot, self.bot,
guild, guild,
ctx.message.created_at, ctx.message.created_at,
"voiceunmute", "vunmute",
user, user,
author, author,
reason, reason,
@@ -1088,14 +1147,8 @@ class Mod(commands.Cog):
) )
except RuntimeError as e: except RuntimeError as e:
await ctx.send(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: else:
await ctx.send(_("That user is not in a voice channel right now!")) await ctx.send(_("Unmute failed. Reason: {}").format(message))
else:
await ctx.send(_("No voice state for the target!"))
return
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel") @unmute.command(name="channel")
@@ -1108,8 +1161,9 @@ class Mod(commands.Cog):
channel = ctx.channel channel = ctx.channel
author = ctx.author author = ctx.author
guild = ctx.guild 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: if success:
await ctx.send(_("User unmuted in this channel.")) await ctx.send(_("User unmuted in this channel."))
@@ -1140,16 +1194,11 @@ class Mod(commands.Cog):
"""Unmute a user in this server.""" """Unmute a user in this server."""
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
audit_reason = get_audit_reason(author, reason)
unmute_success = [] unmute_success = []
for channel in guild.channels: for channel in guild.channels:
if not isinstance(channel, discord.TextChannel): success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
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)
unmute_success.append((success, message)) unmute_success.append((success, message))
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await ctx.send(_("User has been unmuted in this server.")) await ctx.send(_("User has been unmuted in this server."))
@@ -1170,45 +1219,37 @@ class Mod(commands.Cog):
async def unmute_user( async def unmute_user(
self, self,
guild: discord.Guild, guild: discord.Guild,
channel: discord.TextChannel, channel: discord.abc.GuildChannel,
author: discord.Member, author: discord.Member,
user: discord.Member, user: discord.Member,
reason: str,
) -> (bool, str): ) -> (bool, str):
overwrites = channel.overwrites_for(user) overwrites = channel.overwrites_for(user)
permissions = channel.permissions_for(user)
perms_cache = await self.settings.member(user).perms_cache() 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"]) return False, T_(mute_unmute_issues["already_unmuted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"]) return False, T_(mute_unmute_issues["hierarchy_problem"])
if channel.id in perms_cache: overwrites.update(**old_values)
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)
try: try:
if not is_empty: if overwrites.is_empty():
await channel.set_permissions(user, overwrite=overwrites)
else:
await channel.set_permissions( 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: except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"]) return False, T_(mute_unmute_issues["permissions_issue"])
else: else:
try: await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
del perms_cache[channel.id]
except KeyError:
pass
else:
await self.settings.member(user).perms_cache.set(perms_cache)
return True, None return True, None
@commands.group() @commands.group()
@@ -1591,8 +1632,9 @@ class Mod(commands.Cog):
""" """
An event for modlog case creation An event for modlog case creation
""" """
try:
mod_channel = await modlog.get_modlog_channel(case.guild) mod_channel = await modlog.get_modlog_channel(case.guild)
if mod_channel is None: except RuntimeError:
return return
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me) use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
case_content = await case.message_content(use_embeds) case_content = await case.message_content(use_embeds)
@@ -1694,20 +1736,15 @@ class Mod(commands.Cog):
while len(nick_list) > 20: while len(nick_list) > 20:
nick_list.pop(0) 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 _ = lambda s: s
mute_unmute_issues = { mute_unmute_issues = {
"already_muted": _("That user can't send messages in this channel."), "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": _( "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": _( "permissions_issue": _(
"Failed to mute user. I need the manage roles " "Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be " "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 import commands
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
_ = Translator("PermissionsConverters", __file__) _ = 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): class CogOrCommand(NamedTuple):
type: str 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.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate 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__) _ = Translator("Permissions", __file__)
@@ -142,23 +148,20 @@ class Permissions(commands.Cog):
if not command: if not command:
return await ctx.send_help() return await ctx.send_help()
message = copy(ctx.message) fake_message = copy(ctx.message)
message.author = user fake_message.author = user
message.content = "{}{}".format(ctx.prefix, command) fake_message.content = "{}{}".format(ctx.prefix, command)
com = ctx.bot.get_command(command) com = ctx.bot.get_command(command)
if com is None: if com is None:
out = _("No such command") out = _("No such command")
else: else:
fake_context = await ctx.bot.get_context(fake_message)
try: try:
testcontext = await ctx.bot.get_context(message, cls=commands.Context) can = await com.can_run(
to_check = [*reversed(com.parents)] + [com] fake_context, check_all_parents=True, change_permission_state=False
can = False )
for cmd in to_check: except commands.CommandError:
can = await cmd.can_run(testcontext)
if can is False:
break
except commands.CheckFailure:
can = False can = False
out = ( out = (
@@ -275,7 +278,7 @@ class Permissions(commands.Cog):
ctx: commands.Context, ctx: commands.Context,
allow_or_deny: RuleType, allow_or_deny: RuleType,
cog_or_command: CogOrCommand, cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel, who_or_what: GlobalUniqueObjectFinder,
): ):
"""Add a global rule to a command. """Add a global rule to a command.
@@ -303,7 +306,7 @@ class Permissions(commands.Cog):
ctx: commands.Context, ctx: commands.Context,
allow_or_deny: RuleType, allow_or_deny: RuleType,
cog_or_command: CogOrCommand, cog_or_command: CogOrCommand,
who_or_what: commands.GuildPermissionModel, who_or_what: GuildUniqueObjectFinder,
): ):
"""Add a rule to a command in this server. """Add a rule to a command in this server.
@@ -328,7 +331,7 @@ class Permissions(commands.Cog):
self, self,
ctx: commands.Context, ctx: commands.Context,
cog_or_command: CogOrCommand, cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel, who_or_what: GlobalUniqueObjectFinder,
): ):
"""Remove a global rule from a command. """Remove a global rule from a command.
@@ -351,7 +354,7 @@ class Permissions(commands.Cog):
ctx: commands.Context, ctx: commands.Context,
cog_or_command: CogOrCommand, cog_or_command: CogOrCommand,
*, *,
who_or_what: commands.GuildPermissionModel, who_or_what: GuildUniqueObjectFinder,
): ):
"""Remove a server rule from a command. """Remove a server rule from a command.
@@ -542,7 +545,8 @@ class Permissions(commands.Cog):
continue continue
conf = self.config.custom(category) conf = self.config.custom(category)
for cmd_name, cmd_rules in rules_dict.items(): 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) cmd_obj = getter(cmd_name)
if cmd_obj is not None: if cmd_obj is not None:
self._load_rules_for(cmd_obj, {guild_id: cmd_rules}) self._load_rules_for(cmd_obj, {guild_id: cmd_rules})
@@ -651,14 +655,14 @@ class Permissions(commands.Cog):
if category in old_rules: if category in old_rules:
for name, rules in old_rules[category].items(): for name, rules in old_rules[category].items():
these_rules = new_rules.setdefault(name, {}) 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 # Since allow rules would take precedence if the same model ID
# sat in both the allow and deny list, we add the deny entries # sat in both the allow and deny list, we add the deny entries
# first and let any conflicting allow entries overwrite. # first and let any conflicting allow entries overwrite.
for model_id in rules.get("deny", []): 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", []): for model_id in rules.get("allow", []):
guild_rules[model_id] = True guild_rules[str(model_id)] = True
if "default" in rules: if "default" in rules:
default = rules["default"] default = rules["default"]
if default == "allow": if default == "allow":
@@ -689,7 +693,9 @@ class Permissions(commands.Cog):
""" """
for guild_id, guild_dict in _int_key_map(rule_dict.items()): for guild_id, guild_dict in _int_key_map(rule_dict.items()):
for model_id, rule in _int_key_map(guild_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) cog_or_command.allow_for(model_id, guild_id=guild_id)
elif rule is False: elif rule is False:
cog_or_command.deny_to(model_id, guild_id=guild_id) cog_or_command.deny_to(model_id, guild_id=guild_id)
@@ -724,9 +730,16 @@ class Permissions(commands.Cog):
rules. rules.
""" """
for guild_id, guild_dict in _int_key_map(rule_dict.items()): for guild_id, guild_dict in _int_key_map(rule_dict.items()):
for model_id in map(int, guild_dict.keys()): for model_id in guild_dict.keys():
cog_or_command.clear_rule_for(model_id, guild_id) 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]]: def _int_key_map(items_view: ItemsView[str, Any]) -> Iterator[Tuple[Union[str, int], Any]]:
return map(lambda tup: (int(tup[0]), tup[1]), items_view) 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 self.tunnel_store[k]["msgs"] = msgs
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_members=True) @checks.mod_or_permissions(manage_roles=True)
@report.command(name="interact") @report.command(name="interact")
async def response(self, ctx, ticket_number: int): async def response(self, ctx, ticket_number: int):
"""Open a message tunnel. """Open a message tunnel.

View File

@@ -28,7 +28,7 @@ from . import streamtypes as _streamtypes
from collections import defaultdict from collections import defaultdict
import asyncio import asyncio
import re import re
from typing import Optional, List from typing import Optional, List, Tuple
CHECK_DELAY = 60 CHECK_DELAY = 60
@@ -320,6 +320,7 @@ class Streams(commands.Cog):
@commands.group() @commands.group()
@checks.mod() @checks.mod()
async def streamset(self, ctx: commands.Context): async def streamset(self, ctx: commands.Context):
"""Set tokens for accessing streams."""
pass pass
@streamset.command() @streamset.command()
@@ -396,9 +397,6 @@ class Streams(commands.Cog):
async def role(self, ctx: commands.Context, *, role: discord.Role): async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggle a role mention.""" """Toggle a role mention."""
current_setting = await self.db.role(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: if current_setting:
await self.db.role(role).mention.set(False) await self.db.role(role).mention.set(False)
await ctx.send( await ctx.send(
@@ -408,11 +406,17 @@ class Streams(commands.Cog):
) )
else: else:
await self.db.role(role).mention.set(True) await self.db.role(role).mention.set(True)
await ctx.send( msg = _(
_(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned." "When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role) ).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() @streamset.command()
@commands.guild_only() @commands.guild_only()
@@ -535,30 +539,46 @@ class Streams(commands.Cog):
continue continue
for channel_id in stream.channels: for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id) 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: if mention_str:
content = _("{mention}, {stream.name} is live!").format( content = _("{mention}, {stream.name} is live!").format(
mention=mention_str, stream=stream mention=mention_str, stream=stream
) )
else: 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) m = await channel.send(content, embed=embed)
stream._messages_cache.append(m) stream._messages_cache.append(m)
if edited_roles:
for role in edited_roles:
await role.edit(mentionable=False)
await self.save_streams() 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) settings = self.db.guild(guild)
mentions = [] mentions = []
edited_roles = []
if await settings.mention_everyone(): if await settings.mention_everyone():
mentions.append("@everyone") mentions.append("@everyone")
if await settings.mention_here(): if await settings.mention_here():
mentions.append("@here") mentions.append("@here")
can_manage_roles = guild.me.guild_permissions.manage_roles
for role in guild.roles: for role in guild.roles:
if await self.db.role(role).mention(): 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) mentions.append(role.mention)
return " ".join(mentions) return " ".join(mentions), edited_roles
async def check_communities(self): async def check_communities(self):
for community in self.communities: for community in self.communities:
@@ -589,12 +609,15 @@ class Streams(commands.Cog):
emb = await community.make_embed(streams) emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn] chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg: if not chn_msg:
mentions = await self._get_mention_str(chn.guild) mentions, roles = await self._get_mention_str(chn.guild)
if mentions: if mentions:
msg = await chn.send(mentions, embed=emb) msg = await chn.send(mentions, embed=emb)
else: else:
msg = await chn.send(embed=emb) msg = await chn.send(embed=emb)
community._messages_cache.append(msg) community._messages_cache.append(msg)
if roles:
for role in roles:
await role.edit(mentionable=False)
await self.save_communities() await self.save_communities()
else: else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0] chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
@@ -626,7 +649,12 @@ class Streams(commands.Cog):
raw_stream["_messages_cache"] = [] raw_stream["_messages_cache"] = []
for raw_msg in raw_msg_cache: for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"]) chn = self.bot.get_channel(raw_msg["channel"])
if chn is not None:
try:
msg = await chn.get_message(raw_msg["message"]) msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_stream["_messages_cache"].append(msg) raw_stream["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
if token is not None: if token is not None:
@@ -646,7 +674,12 @@ class Streams(commands.Cog):
raw_community["_messages_cache"] = [] raw_community["_messages_cache"] = []
for raw_msg in raw_msg_cache: for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"]) chn = self.bot.get_channel(raw_msg["channel"])
if chn is not None:
try:
msg = await chn.get_message(raw_msg["message"]) msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_community["_messages_cache"].append(msg) raw_community["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
communities.append(_class(token=token, **raw_community)) communities.append(_class(token=token, **raw_community))

View File

@@ -114,7 +114,7 @@ class TriviaSession:
async with self.ctx.typing(): async with self.ctx.typing():
await asyncio.sleep(3) await asyncio.sleep(3)
self.count += 1 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) await self.ctx.send(msg)
continue_ = await self.wait_for_answer(answers, delay, timeout) continue_ = await self.wait_for_answer(answers, delay, timeout)
if continue_ is False: if continue_ is False:
@@ -322,9 +322,9 @@ def _parse_answers(answers):
for answer in answers: for answer in answers:
if isinstance(answer, bool): if isinstance(answer, bool):
if answer is True: if answer is True:
ret.extend(["True", "Yes", _("Yes")]) ret.extend(["True", "Yes", "On"])
else: else:
ret.extend(["False", "No", _("No")]) ret.extend(["False", "No", "Off"])
else: else:
ret.append(str(answer)) ret.append(str(answer))
# Uniquify list # Uniquify list

View File

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

View File

@@ -19,9 +19,11 @@ async def warning_points_add_check(
act = {} act = {}
async with guild_settings.actions() as registered_actions: async with guild_settings.actions() as registered_actions:
for a in 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"]: if points >= a["points"]:
act = a act = a
else:
break break
if act and act["exceed_command"] is not None: # some action needs to be taken if act and act["exceed_command"] is not None: # some action needs to be taken
await create_and_invoke_context(ctx, act["exceed_command"], user) await create_and_invoke_context(ctx, act["exceed_command"], user)

View File

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

View File

@@ -1,40 +1,152 @@
import re as _re
from math import inf as _inf
from typing import (
Any as _Any,
ClassVar as _ClassVar,
Dict as _Dict,
List as _List,
Optional as _Optional,
Pattern as _Pattern,
Tuple as _Tuple,
Union as _Union,
)
from .config import Config from .config import Config
__all__ = ["Config", "__version__"] __all__ = ["Config", "__version__", "version_info", "VersionInfo"]
class VersionInfo: class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial): ALPHA = "alpha"
self._levels = ["alpha", "beta", "release candidate", "final"] BETA = "beta"
self.major = major RELEASE_CANDIDATE = "release candidate"
self.minor = minor FINAL = "final"
self.micro = micro
if releaselevel not in self._levels: _VERSION_STR_PATTERN: _ClassVar[_Pattern[str]] = _re.compile(
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels))) r"^"
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<micro>0|[1-9]\d*)"
r"(?:(?P<releaselevel>a|b|rc)(?P<serial>0|[1-9]\d*))?"
r"(?:\.post(?P<post_release>0|[1-9]\d*))?"
r"(?:\.dev(?P<dev_release>0|[1-9]\d*))?"
r"$",
flags=_re.IGNORECASE,
)
_RELEASE_LEVELS: _ClassVar[_List[str]] = [ALPHA, BETA, RELEASE_CANDIDATE, FINAL]
_SHORT_RELEASE_LEVELS: _ClassVar[_Dict[str, str]] = {
"a": ALPHA,
"b": BETA,
"rc": RELEASE_CANDIDATE,
}
self.releaselevel = releaselevel def __init__(
self.serial = serial self,
major: int,
minor: int,
micro: int,
releaselevel: str,
serial: _Optional[int] = None,
post_release: _Optional[int] = None,
dev_release: _Optional[int] = None,
) -> None:
self.major: int = major
self.minor: int = minor
self.micro: int = micro
def __lt__(self, other): if releaselevel not in self._RELEASE_LEVELS:
my_index = self._levels.index(self.releaselevel) raise TypeError(f"'releaselevel' must be one of: {', '.join(self._RELEASE_LEVELS)}")
other_index = self._levels.index(other.releaselevel)
return (self.major, self.minor, self.micro, my_index, self.serial) < ( self.releaselevel: str = releaselevel
other.major, self.serial: _Optional[int] = serial
other.minor, self.post_release: _Optional[int] = post_release
other.micro, self.dev_release: _Optional[int] = dev_release
other_index,
other.serial, @classmethod
def from_str(cls, version_str: str) -> "VersionInfo":
"""Parse a string into a VersionInfo object.
Raises
------
ValueError
If the version info string is invalid.
"""
match = cls._VERSION_STR_PATTERN.match(version_str)
if not match:
raise ValueError(f"Invalid version string: {version_str}")
kwargs: _Dict[str, _Union[str, int]] = {}
for key in ("major", "minor", "micro"):
kwargs[key] = int(match[key])
releaselevel = match["releaselevel"]
if releaselevel is not None:
kwargs["releaselevel"] = cls._SHORT_RELEASE_LEVELS[releaselevel]
else:
kwargs["releaselevel"] = cls.FINAL
for key in ("serial", "post_release", "dev_release"):
if match[key] is not None:
kwargs[key] = int(match[key])
return cls(**kwargs)
@classmethod
def from_json(
cls, data: _Union[_Dict[str, _Union[int, str]], _List[_Union[int, str]]]
) -> "VersionInfo":
if isinstance(data, _List):
# For old versions, data was stored as a list:
# [MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL]
return cls(*data)
else:
return cls(**data)
def to_json(self) -> _Dict[str, _Union[int, str]]:
return {
"major": self.major,
"minor": self.minor,
"micro": self.micro,
"releaselevel": self.releaselevel,
"serial": self.serial,
"post_release": self.post_release,
"dev_release": self.dev_release,
}
def __lt__(self, other: _Any) -> bool:
if not isinstance(other, VersionInfo):
return NotImplemented
tups: _List[_Tuple[int, int, int, int, int, int, int]] = []
for obj in (self, other):
tups.append(
(
obj.major,
obj.minor,
obj.micro,
obj._RELEASE_LEVELS.index(obj.releaselevel),
obj.serial if obj.serial is not None else _inf,
obj.post_release if obj.post_release is not None else -_inf,
obj.dev_release if obj.dev_release is not None else _inf,
)
)
return tups[0] < tups[1]
def __str__(self) -> str:
ret = f"{self.major}.{self.minor}.{self.micro}"
if self.releaselevel != self.FINAL:
short = next(
k for k, v in self._SHORT_RELEASE_LEVELS.items() if v == self.releaselevel
)
ret += f"{short}{self.serial}"
if self.post_release is not None:
ret += f".post{self.post_release}"
if self.dev_release is not None:
ret += f".dev{self.dev_release}"
return ret
def __repr__(self) -> str:
return (
"VersionInfo(major={major}, minor={minor}, micro={micro}, "
"releaselevel={releaselevel}, serial={serial}, post={post_release}, "
"dev={dev_release})".format(**self.to_json())
) )
def __repr__(self):
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
self.major, self.minor, self.micro, self.releaselevel, self.serial
)
def to_json(self): __version__ = "3.0.0rc3"
return [self.major, self.minor, self.micro, self.releaselevel, self.serial] version_info = VersionInfo.from_str(__version__)
__version__ = "3.0.0rc1"
version_info = VersionInfo(3, 0, 0, "release candidate", 1)

View File

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

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import inspect
import os import os
import logging import logging
from collections import Counter from collections import Counter
@@ -11,10 +12,10 @@ import discord
import sys import sys
from discord.ext.commands import when_mentioned_or from discord.ext.commands import when_mentioned_or
from . import Config, i18n, commands, errors
from .cog_manager import CogManager from .cog_manager import CogManager
from . import Config, i18n, commands
from .rpc import RPCMixin
from .help_formatter import Help, help as help_ from .help_formatter import Help, help as help_
from .rpc import RPCMixin
from .sentry import SentryManager from .sentry import SentryManager
from .utils import common_filters from .utils import common_filters
@@ -110,7 +111,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
self.main_dir = bot_dir 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) 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): async def is_admin(self, member: discord.Member):
"""Checks if a member is an admin of their guild.""" """Checks if a member is an admin of their guild."""
admin_role = await self.db.guild(member.guild).admin_role() 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): async def is_mod(self, member: discord.Member):
"""Checks if a member is a mod or admin of their guild.""" """Checks if a member is a mod or admin of their guild."""
mod_role = await self.db.guild(member.guild).mod_role() mod_role = await self.db.guild(member.guild).mod_role()
admin_role = await self.db.guild(member.guild).admin_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): async def get_context(self, message, *, cls=commands.Context):
return await super().get_context(message, cls=cls) 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): async def load_extension(self, spec: ModuleSpec):
name = spec.name.split(".")[-1] name = spec.name.split(".")[-1]
if name in self.extensions: 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() lib = spec.loader.load_module()
if not hasattr(lib, "setup"): if not hasattr(lib, "setup"):
@@ -236,16 +247,9 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
if cog is None: if cog is None:
return return
for when in ("before", "after"): for cls in inspect.getmro(cog.__class__):
try: 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: except AttributeError:
pass pass
else: else:
@@ -340,7 +344,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
ids_to_check = [to_check.id] ids_to_check = [to_check.id]
else: else:
author = getattr(to_check, "author", to_check) author = getattr(to_check, "author", to_check)
try:
ids_to_check = [r.id for r in author.roles] 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) ids_to_check.append(author.id)
immune_ids = await self.db.guild(guild).autoimmune_ids() 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"): if not hasattr(cog, "requires"):
commands.Cog.__init__(cog) 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): for attr in dir(cog):
_attr = getattr(cog, attr) _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( if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
_attr, commands.Command _attr, commands.Command
): ):

View File

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

View File

@@ -145,7 +145,7 @@ class Command(CogCommandMixin, commands.Command):
@property @property
def parents(self) -> List["Group"]: 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. 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. If the command has no parents, this will be an empty list.
@@ -157,12 +157,31 @@ class Command(CogCommandMixin, commands.Command):
cmd = cmd.parent cmd = cmd.parent
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True) 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. """Check if this command can be run in the given context.
This function first checks if the command can be run using This function first checks if the command can be run using
discord.py's method `discord.ext.commands.Command.can_run`, discord.py's method `discord.ext.commands.Command.can_run`,
then will return the result of `Requires.verify`. 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) ret = await super().can_run(ctx)
if ret is False: 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 is so contexts invoking other commands can be checked with
# this command as well # this command as well
original_command = ctx.command original_command = ctx.command
original_state = ctx.permission_state
ctx.command = self 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: if self.parent is None and self.instance is not None:
# For top-level commands, we need to check the cog's requires too # For top-level commands, we need to check the cog's requires too
ret = await self.instance.requires.verify(ctx) ret = await self.instance.requires.verify(ctx)
@@ -183,6 +215,17 @@ class Command(CogCommandMixin, commands.Command):
return await self.requires.verify(ctx) return await self.requires.verify(ctx)
finally: finally:
ctx.command = original_command 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( async def do_conversion(
self, ctx: "Context", converter, argument: str, param: inspect.Parameter self, ctx: "Context", converter, argument: str, param: inspect.Parameter
@@ -238,7 +281,9 @@ class Command(CogCommandMixin, commands.Command):
if cmd.hidden: if cmd.hidden:
return False return False
try: 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: except commands.CheckFailure:
return False return False
else: else:

View File

@@ -281,12 +281,14 @@ class Requires:
if isinstance(user_perms, dict): if isinstance(user_perms, dict):
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none() self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
_validate_perms_dict(user_perms)
self.user_perms.update(**user_perms) self.user_perms.update(**user_perms)
else: else:
self.user_perms = user_perms self.user_perms = user_perms
if isinstance(bot_perms, dict): if isinstance(bot_perms, dict):
self.bot_perms: discord.Permissions = discord.Permissions.none() self.bot_perms: discord.Permissions = discord.Permissions.none()
_validate_perms_dict(bot_perms)
self.bot_perms.update(**bot_perms) self.bot_perms.update(**bot_perms)
else: else:
self.bot_perms = bot_perms self.bot_perms = bot_perms
@@ -311,6 +313,7 @@ class Requires:
if user_perms is None: if user_perms is None:
func.requires.user_perms = None func.requires.user_perms = None
else: else:
_validate_perms_dict(user_perms)
func.requires.user_perms.update(**user_perms) func.requires.user_perms.update(**user_perms)
return func return func
@@ -417,9 +420,13 @@ class Requires:
""" """
await self._verify_bot(ctx) 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: 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) hook_result = await ctx.bot.verify_permissions_hooks(ctx)
if hook_result is not None: if hook_result is not None:
@@ -445,7 +452,20 @@ class Requires:
should_invoke = await self._verify_user(ctx) should_invoke = await self._verify_user(ctx)
elif isinstance(next_state, dict): elif isinstance(next_state, dict):
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition? # 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 ctx.permission_state = next_state
return should_invoke return should_invoke
@@ -584,6 +604,7 @@ def bot_has_permissions(**perms: bool):
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
func.__requires_bot_perms__ = perms func.__requires_bot_perms__ = perms
else: else:
_validate_perms_dict(perms)
func.requires.bot_perms.update(**perms) func.requires.bot_perms.update(**perms)
return func return func
@@ -595,6 +616,8 @@ def has_permissions(**perms: bool):
This check can be overridden by rules. 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) return Requires.get_decorator(None, perms)
@@ -666,3 +689,20 @@ class _IntKeyDict(Dict[int, _T]):
if not isinstance(key, int): if not isinstance(key, int):
raise TypeError("Keys must be of type `int`") raise TypeError("Keys must be of type `int`")
return super().__setitem__(key, value) 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): async def __aenter__(self):
self.raw_value = await self self.raw_value = await self
self.__original_value = deepcopy(self.raw_value)
if not isinstance(self.raw_value, (list, dict)): if not isinstance(self.raw_value, (list, dict)):
raise TypeError( raise TypeError(
"Type of retrieved value must be mutable (i.e. " "Type of retrieved value must be mutable (i.e. "
"list or dict) in order to use a config value as " "list or dict) in order to use a config value as "
"a context manager." "a context manager."
) )
self.__original_value = deepcopy(self.raw_value)
return self.raw_value return self.raw_value
async def __aexit__(self, exc_type, exc, tb): 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) await self.value_obj.set(self.raw_value)
@@ -58,7 +62,7 @@ class Value:
Attributes Attributes
---------- ----------
identifiers : `tuple` of `str` identifiers : Tuple[str]
This attribute provides all the keys necessary to get a specific data This attribute provides all the keys necessary to get a specific data
element from a json document. element from a json document.
default default
@@ -69,15 +73,10 @@ class Value:
""" """
def __init__(self, identifiers: Tuple[str], default_value, driver): def __init__(self, identifiers: Tuple[str], default_value, driver):
self._identifiers = identifiers self.identifiers = identifiers
self.default = default_value self.default = default_value
self.driver = driver self.driver = driver
@property
def identifiers(self):
return tuple(str(i) for i in self._identifiers)
async def _get(self, default=...): async def _get(self, default=...):
try: try:
ret = await self.driver.get(*self.identifiers) ret = await self.driver.get(*self.identifiers)
@@ -149,6 +148,8 @@ class Value:
The new literal value of this attribute. The new literal value of this attribute.
""" """
if isinstance(value, dict):
value = _str_key_dict(value)
await self.driver.set(*self.identifiers, value=value) await self.driver.set(*self.identifiers, value=value)
async def clear(self): async def clear(self):
@@ -192,7 +193,10 @@ class Group(Value):
async def _get(self, default: Dict[str, Any] = ...) -> Dict[str, Any]: async def _get(self, default: Dict[str, Any] = ...) -> Dict[str, Any]:
default = default if default is not ... else self.defaults default = default if default is not ... else self.defaults
raw = await super()._get(default) raw = await super()._get(default)
if isinstance(raw, dict):
return self.nested_update(raw, default) return self.nested_update(raw, default)
else:
return raw
# noinspection PyTypeChecker # noinspection PyTypeChecker
def __getattr__(self, item: str) -> Union["Group", Value]: def __getattr__(self, item: str) -> Union["Group", Value]:
@@ -238,7 +242,7 @@ class Group(Value):
else: else:
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver) 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 Allows a developer to clear data as if it was stored in a standard
Python dictionary. Python dictionary.
@@ -254,44 +258,44 @@ class Group(Value):
Parameters Parameters
---------- ----------
nested_path : str nested_path : Any
Multiple arguments that mirror the arguments passed in for nested 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] path = [str(p) for p in nested_path]
await self.driver.clear(*self.identifiers, *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 """A helper method for `__getattr__`. Most developers will have no need
to use this. to use this.
Parameters Parameters
---------- ----------
item : str item : Any
See `__getattr__`. See `__getattr__`.
""" """
default = self._defaults.get(item) default = self._defaults.get(str(item))
return isinstance(default, dict) 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 """A helper method for `__getattr__`. Most developers will have no need
to use this. to use this.
Parameters Parameters
---------- ----------
item : str item : Any
See `__getattr__`. See `__getattr__`.
""" """
try: try:
default = self._defaults[item] default = self._defaults[str(item)]
except KeyError: except KeyError:
return False return False
return not isinstance(default, dict) 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. """Manually get an attribute of this Group.
This is available to use as an alternative to using normal Python This is available to use as an alternative to using normal Python
@@ -312,7 +316,8 @@ class Group(Value):
Parameters Parameters
---------- ----------
item : str 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 Returns
------- -------
@@ -320,9 +325,11 @@ class Group(Value):
The attribute which was requested. The attribute which was requested.
""" """
if isinstance(item, int):
item = str(item)
return self.__getattr__(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 Allows a developer to access data as if it was stored in a standard
Python dictionary. Python dictionary.
@@ -345,7 +352,7 @@ class Group(Value):
---------- ----------
nested_path : str nested_path : str
Multiple arguments that mirror the arguments passed in for nested Multiple arguments that mirror the arguments passed in for nested
dict access. dict access. These are casted to `str` for you.
default default
Default argument for the value attempting to be accessed. If the Default argument for the value attempting to be accessed. If the
value does not exist the default will be returned. 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' If no defaults are passed, then the instance attribute 'defaults'
will be used. will be used.
""" """
if defaults is ...: if defaults is ...:
defaults = self.defaults 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.") raise ValueError("You may only set the value of a group to be a dict.")
await super().set(value) 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 Allows a developer to set data as if it was stored in a standard
Python dictionary. Python dictionary.
@@ -444,13 +450,15 @@ class Group(Value):
Parameters Parameters
---------- ----------
nested_path : str nested_path : Any
Multiple arguments that mirror the arguments passed in for nested Multiple arguments that mirror the arguments passed in for nested
dict access. `dict` access. These are casted to `str` for you.
value value
The value to store. The value to store.
""" """
path = [str(p) for p in nested_path] 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) 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. `get_core_conf` for Config used in the core package.
.. important:: .. important::
Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`) Most config data should be accessed through its respective
however the process for accessing global data is a bit different. There is no :python:`global` method group method (e.g. :py:meth:`guild`) however the process for
because global data is accessed by normal attribute access:: 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() await conf.foo()
@@ -548,7 +558,7 @@ class Config:
A new Config object. 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) cog_path_override = cog_data_path(raw_name=cog_name)
else: else:
cog_path_override = cog_data_path(cog_instance=cog_instance) cog_path_override = cog_data_path(cog_instance=cog_instance)
@@ -635,11 +645,8 @@ class Config:
def _get_defaults_dict(key: str, value) -> dict: def _get_defaults_dict(key: str, value) -> dict:
""" """
Since we're allowing nested config stuff now, not storing the Since we're allowing nested config stuff now, not storing the
_defaults as a flat dict sounds like a good idea. May turn _defaults as a flat dict sounds like a good idea. May turn out
out to be an awful one but we'll see. to be an awful one but we'll see.
:param key:
:param value:
:return:
""" """
ret = {} ret = {}
partial = ret partial = ret
@@ -655,15 +662,12 @@ class Config:
return ret return ret
@staticmethod @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 This tries to update the _defaults dictionary with the nested
partial dict generated by _get_defaults_dict. This WILL partial dict generated by _get_defaults_dict. This WILL
throw an error if you try to have both a value and a group throw an error if you try to have both a value and a group
registered under the same name. registered under the same name.
:param to_add:
:param _partial:
:return:
""" """
for k, v in to_add.items(): for k, v in to_add.items():
val_is_dict = isinstance(v, dict) val_is_dict = isinstance(v, dict)
@@ -679,7 +683,7 @@ class Config:
else: else:
_partial[k] = v _partial[k] = v
def _register_default(self, key: str, **kwargs): def _register_default(self, key: str, **kwargs: Any):
if key not in self._defaults: if key not in self._defaults:
self._defaults[key] = {} self._defaults[key] = {}
@@ -720,8 +724,8 @@ class Config:
**_defaults **_defaults
) )
You can do the same thing without a :python:`_defaults` dict by using double underscore as a variable You can do the same thing without a :python:`_defaults` dict by
name separator:: using double underscore as a variable name separator::
# This is equivalent to the previous example # This is equivalent to the previous example
conf.register_global( conf.register_global(
@@ -802,7 +806,7 @@ class Config:
The guild's Group object. 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: def channel(self, channel: discord.TextChannel) -> Group:
"""Returns a `Group` for the given channel. """Returns a `Group` for the given channel.
@@ -820,7 +824,7 @@ class Config:
The channel's Group object. 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: def role(self, role: discord.Role) -> Group:
"""Returns a `Group` for the given role. """Returns a `Group` for the given role.
@@ -836,7 +840,7 @@ class Config:
The role's Group object. 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: def user(self, user: discord.abc.User) -> Group:
"""Returns a `Group` for the given user. """Returns a `Group` for the given user.
@@ -852,7 +856,7 @@ class Config:
The user's Group object. 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: def member(self, member: discord.Member) -> Group:
"""Returns a `Group` for the given member. """Returns a `Group` for the given member.
@@ -866,8 +870,9 @@ class Config:
------- -------
`Group <redbot.core.config.Group>` `Group <redbot.core.config.Group>`
The member's Group object. 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): def custom(self, group_identifier: str, *identifiers: str):
"""Returns a `Group` for the given custom group. """Returns a `Group` for the given custom group.
@@ -876,17 +881,17 @@ class Config:
---------- ----------
group_identifier : str group_identifier : str
Used to identify the custom group. Used to identify the custom group.
identifiers : str identifiers : str
The attributes necessary to uniquely identify an entry in the The attributes necessary to uniquely identify an entry in the
custom group. custom group. These are casted to `str` for you.
Returns Returns
------- -------
`Group <redbot.core.config.Group>` `Group <redbot.core.config.Group>`
The custom group's Group object. 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]]: 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. """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) 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 = {} ret = {}
for member_id, member_data in guild_data.items(): for member_id, member_data in guild_data.items():
new_member_data = group.defaults new_member_data = group.defaults
@@ -1026,7 +1032,7 @@ class Config:
for guild_id, guild_data in dict_.items(): for guild_id, guild_data in dict_.items():
ret[int(guild_id)] = self._all_members_from_guild(group, guild_data) ret[int(guild_id)] = self._all_members_from_guild(group, guild_data)
else: else:
group = self._get_base_group(self.MEMBER, guild.id) group = self._get_base_group(self.MEMBER, str(guild.id))
try: try:
guild_data = await self.driver.get(*group.identifiers) guild_data = await self.driver.get(*group.identifiers)
except KeyError: except KeyError:
@@ -1054,7 +1060,8 @@ class Config:
""" """
if not scopes: if not scopes:
group = Group(identifiers=[], defaults={}, driver=self.driver) # noinspection PyTypeChecker
group = Group(identifiers=(), defaults={}, driver=self.driver)
else: else:
group = self._get_base_group(*scopes) group = self._get_base_group(*scopes)
await group.clear() await group.clear()
@@ -1119,7 +1126,7 @@ class Config:
""" """
if guild is not None: if guild is not None:
await self._clear_scope(self.MEMBER, guild.id) await self._clear_scope(self.MEMBER, str(guild.id))
return return
await self._clear_scope(self.MEMBER) await self._clear_scope(self.MEMBER)
@@ -1127,5 +1134,34 @@ class Config:
"""Clear all custom group data. """Clear all custom group data.
This resets all custom group data to its registered defaults. 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,17 +13,21 @@ from collections import namedtuple
from pathlib import Path from pathlib import Path
from random import SystemRandom from random import SystemRandom
from string import ascii_letters, digits from string import ascii_letters, digits
from distutils.version import StrictVersion from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict
from typing import TYPE_CHECKING, Union
import aiohttp import aiohttp
import discord import discord
import pkg_resources import pkg_resources
from redbot.core import __version__ from redbot.core import (
from redbot.core import checks __version__,
from redbot.core import i18n version_info as red_version_info,
from redbot.core import commands VersionInfo,
checks,
commands,
errors,
i18n,
)
from .utils.predicates import MessagePredicate from .utils.predicates import MessagePredicate
from .utils.chat_formatting import pagify, box, inline from .utils.chat_formatting import pagify, box, inline
@@ -56,7 +60,9 @@ class CoreLogic:
self.bot.register_rpc_handler(self._version_info) self.bot.register_rpc_handler(self._version_info)
self.bot.register_rpc_handler(self._invite_url) 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. Loads cogs by name.
Parameters Parameters
@@ -66,11 +72,12 @@ class CoreLogic:
Returns Returns
------- -------
tuple tuple
3 element tuple of loaded, failed, and not found cogs. 4-tuple of loaded, failed, not found and already loaded cogs.
""" """
failed_packages = [] failed_packages = []
loaded_packages = [] loaded_packages = []
notfound_packages = [] notfound_packages = []
alreadyloaded_packages = []
bot = self.bot bot = self.bot
@@ -95,6 +102,8 @@ class CoreLogic:
try: try:
self._cleanup_and_refresh_modules(spec.name) self._cleanup_and_refresh_modules(spec.name)
await bot.load_extension(spec) await bot.load_extension(spec)
except errors.PackageAlreadyLoaded:
alreadyloaded_packages.append(name)
except Exception as e: except Exception as e:
log.exception("Package loading failed", exc_info=e) log.exception("Package loading failed", exc_info=e)
@@ -106,9 +115,10 @@ class CoreLogic:
await bot.add_loaded_package(name) await bot.add_loaded_package(name)
loaded_packages.append(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""" """Interally reloads modules so that changes are detected"""
splitted = module_name.split(".") splitted = module_name.split(".")
@@ -120,6 +130,7 @@ class CoreLogic:
else: else:
importlib._bootstrap._exec(lib.__spec__, lib) importlib._bootstrap._exec(lib.__spec__, lib)
# noinspection PyTypeChecker
modules = itertools.accumulate(splitted, "{}.{}".format) modules = itertools.accumulate(splitted, "{}.{}".format)
for m in modules: for m in modules:
maybe_reload(m) maybe_reload(m)
@@ -128,7 +139,10 @@ class CoreLogic:
for child_name, lib in children.items(): for child_name, lib in children.items():
importlib._bootstrap._exec(lib.__spec__, lib) 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 Gets the strings needed for the load, unload and reload commands
""" """
@@ -144,7 +158,7 @@ class CoreLogic:
final_string = fmt.format(**form) final_string = fmt.format(**form)
return final_string 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. Unloads cogs with the given names.
@@ -172,14 +186,16 @@ class CoreLogic:
return unloaded_packages, failed_packages 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) 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. Gets or sets the bot's username.
@@ -198,7 +214,7 @@ class CoreLogic:
return self.bot.user.name 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. Gets or sets the bot's global prefixes.
@@ -217,7 +233,8 @@ class CoreLogic:
await self.bot.db.prefix.set(prefixes) await self.bot.db.prefix.set(prefixes)
return await self.bot.db.prefix() 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 Version information for Red and discord.py
@@ -228,7 +245,7 @@ class CoreLogic:
""" """
return {"redbot": __version__, "discordpy": discord.__version__} return {"redbot": __version__, "discordpy": discord.__version__}
async def _invite_url(self): async def _invite_url(self) -> str:
""" """
Generates the invite URL for the bot. Generates the invite URL for the bot.
@@ -245,11 +262,8 @@ class CoreLogic:
class Core(commands.Cog, CoreLogic): class Core(commands.Cog, CoreLogic):
"""Commands related to core functions""" """Commands related to core functions"""
def __init__(self, bot):
super().__init__(bot)
@commands.command(hidden=True) @commands.command(hidden=True)
async def ping(self, ctx): async def ping(self, ctx: commands.Context):
"""Pong.""" """Pong."""
await ctx.send("Pong.") await ctx.send("Pong.")
@@ -274,7 +288,7 @@ class Core(commands.Cog, CoreLogic):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get("{}/json".format(red_pypi)) as r: async with session.get("{}/json".format(red_pypi)) as r:
data = await r.json() data = await r.json()
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__) outdated = VersionInfo.from_str(data["info"]["version"]) > red_version_info
about = ( about = (
"This is an instance of [Red, an open source Discord bot]({}) " "This is an instance of [Red, an open source Discord bot]({}) "
"created by [Twentysix]({}) and [improved by many]({}).\n\n" "created by [Twentysix]({}) and [improved by many]({}).\n\n"
@@ -310,7 +324,7 @@ class Core(commands.Cog, CoreLogic):
passed = self.get_bot_uptime() passed = self.get_bot_uptime()
await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since)) 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 # Courtesy of Danny
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
delta = now - self.bot.uptime delta = now - self.bot.uptime
@@ -413,7 +427,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @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 """Sends to the owner the last command exception that has occurred
If public (yes is specified), it will be sent to the chat instead""" If public (yes is specified), it will be sent to the chat instead"""
@@ -430,20 +444,20 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def invite(self, ctx): async def invite(self, ctx: commands.Context):
"""Show's Red's invite url""" """Show's Red's invite url"""
await ctx.author.send(await self._invite_url()) await ctx.author.send(await self._invite_url())
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.is_owner() @checks.is_owner()
async def leave(self, ctx): async def leave(self, ctx: commands.Context):
"""Leaves server""" """Leaves server"""
await ctx.send("Are you sure you want me to leave this server? (y/n)") await ctx.send("Are you sure you want me to leave this server? (y/n)")
pred = MessagePredicate.yes_or_no(ctx) pred = MessagePredicate.yes_or_no(ctx)
try: try:
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx)) await self.bot.wait_for("message", check=pred)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await ctx.send("Response timed out.") await ctx.send("Response timed out.")
return return
@@ -457,7 +471,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def servers(self, ctx): async def servers(self, ctx: commands.Context):
"""Lists and allows to leave servers""" """Lists and allows to leave servers"""
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower()) guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
msg = "" msg = ""
@@ -499,18 +513,21 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def load(self, ctx, *, cog_name: str): async def load(self, ctx: commands.Context, *cogs: str):
"""Loads packages""" """Loads packages"""
cog_names = [c.strip() for c in cog_name.split(" ")]
async with ctx.typing(): 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: if loaded:
fmt = "Loaded {packs}." fmt = "Loaded {packs}."
formed = self._get_package_strings(loaded, fmt) formed = self._get_package_strings(loaded, fmt)
await ctx.send(formed) 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: if failed:
fmt = ( fmt = (
"Failed to load package{plural} {packs}. Check your console or " "Failed to load package{plural} {packs}. Check your console or "
@@ -526,12 +543,9 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def unload(self, ctx, *, cog_name: str): async def unload(self, ctx: commands.Context, *cogs: str):
"""Unloads packages""" """Unloads packages"""
unloaded, failed = await self._unload(cogs)
cog_names = [c.strip() for c in cog_name.split(" ")]
unloaded, failed = await self._unload(cog_names)
if unloaded: if unloaded:
fmt = "Package{plural} {packs} {other} unloaded." fmt = "Package{plural} {packs} {other} unloaded."
@@ -545,10 +559,10 @@ class Core(commands.Cog, CoreLogic):
@commands.command(name="reload") @commands.command(name="reload")
@checks.is_owner() @checks.is_owner()
async def reload(self, ctx, *cogs: str): async def reload(self, ctx: commands.Context, *cogs: str):
"""Reloads packages""" """Reloads packages"""
async with ctx.typing(): async with ctx.typing():
loaded, failed, not_found = await self._reload(cogs) loaded, failed, not_found, already_loaded = await self._reload(cogs)
if loaded: if loaded:
fmt = "Package{plural} {packs} {other} reloaded." fmt = "Package{plural} {packs} {other} reloaded."
@@ -567,34 +581,30 @@ class Core(commands.Cog, CoreLogic):
@commands.command(name="shutdown") @commands.command(name="shutdown")
@checks.is_owner() @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""" """Shuts down the bot"""
wave = "\N{WAVING HAND SIGN}" wave = "\N{WAVING HAND SIGN}"
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}" 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: if not silently:
await ctx.send(_("Shutting down... ") + wave + skin) await ctx.send(_("Shutting down... ") + wave + skin)
except:
pass
await ctx.bot.shutdown() await ctx.bot.shutdown()
@commands.command(name="restart") @commands.command(name="restart")
@checks.is_owner() @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 """Attempts to restart Red
Makes Red quit with exit code 26 Makes Red quit with exit code 26
The restart is not guaranteed: it must be dealt The restart is not guaranteed: it must be dealt
with by the process manager in use""" with by the process manager in use"""
try: with contextlib.suppress(discord.HTTPException):
if not silently: if not silently:
await ctx.send(_("Restarting...")) await ctx.send(_("Restarting..."))
except:
pass
await ctx.bot.shutdown(restart=True) await ctx.bot.shutdown(restart=True)
@commands.group(name="set") @commands.group(name="set")
async def _set(self, ctx): async def _set(self, ctx: commands.Context):
"""Changes Red's settings""" """Changes Red's settings"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
if ctx.guild: if ctx.guild:
@@ -626,7 +636,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.guildowner() @checks.guildowner()
@commands.guild_only() @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""" """Sets the admin role for this server"""
await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id) await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id)
await ctx.send(_("The admin role for this guild has been set.")) await ctx.send(_("The admin role for this guild has been set."))
@@ -634,7 +644,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.guildowner() @checks.guildowner()
@commands.guild_only() @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""" """Sets the mod role for this server"""
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id) await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
await ctx.send(_("The mod role for this guild has been set.")) await ctx.send(_("The mod role for this guild has been set."))
@@ -642,7 +652,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["usebotcolor"]) @_set.command(aliases=["usebotcolor"])
@checks.guildowner() @checks.guildowner()
@commands.guild_only() @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. Toggle whether to use the bot owner-configured colour for embeds.
@@ -660,7 +670,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.guildowner() @checks.guildowner()
@commands.guild_only() @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. Toggle whether to enable fuzzy command search for the server.
@@ -676,7 +686,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.is_owner() @checks.is_owner()
async def fuzzy(self, ctx): async def fuzzy(self, ctx: commands.Context):
""" """
Toggle whether to enable fuzzy command search in DMs. Toggle whether to enable fuzzy command search in DMs.
@@ -692,7 +702,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["color"]) @_set.command(aliases=["color"])
@checks.is_owner() @checks.is_owner()
async def colour(self, ctx, *, colour: discord.Colour = None): async def colour(self, ctx: commands.Context, *, colour: discord.Colour = None):
""" """
Sets a default colour to be used for the bot's embeds. Sets a default colour to be used for the bot's embeds.
@@ -710,7 +720,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.is_owner() @checks.is_owner()
async def avatar(self, ctx, url: str): async def avatar(self, ctx: commands.Context, url: str):
"""Sets Red's avatar""" """Sets Red's avatar"""
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as r: async with session.get(url) as r:
@@ -734,7 +744,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="game") @_set.command(name="game")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @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""" """Sets Red's playing status"""
if game: if game:
@@ -748,7 +758,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="listening") @_set.command(name="listening")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @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""" """Sets Red's listening status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
@@ -762,7 +772,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="watching") @_set.command(name="watching")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @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""" """Sets Red's watching status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
@@ -776,7 +786,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @checks.is_owner()
async def status(self, ctx, *, status: str): async def status(self, ctx: commands.Context, *, status: str):
"""Sets Red's status """Sets Red's status
Available statuses: Available statuses:
@@ -805,7 +815,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @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 """Sets Red's streaming status
Leaving both streamer and stream_title empty will clear it.""" Leaving both streamer and stream_title empty will clear it."""
@@ -826,7 +836,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="username", aliases=["name"]) @_set.command(name="username", aliases=["name"])
@checks.is_owner() @checks.is_owner()
async def _username(self, ctx, *, username: str): async def _username(self, ctx: commands.Context, *, username: str):
"""Sets Red's username""" """Sets Red's username"""
try: try:
await self._name(name=username) await self._name(name=username)
@@ -845,7 +855,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(name="nickname") @_set.command(name="nickname")
@checks.admin() @checks.admin()
@commands.guild_only() @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""" """Sets Red's nickname"""
try: try:
await ctx.guild.me.edit(nick=nickname) await ctx.guild.me.edit(nick=nickname)
@@ -856,7 +866,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["prefixes"]) @_set.command(aliases=["prefixes"])
@checks.is_owner() @checks.is_owner()
async def prefix(self, ctx, *prefixes): async def prefix(self, ctx: commands.Context, *prefixes: str):
"""Sets Red's global prefix(es)""" """Sets Red's global prefix(es)"""
if not prefixes: if not prefixes:
await ctx.send_help() await ctx.send_help()
@@ -867,7 +877,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command(aliases=["serverprefixes"]) @_set.command(aliases=["serverprefixes"])
@checks.admin() @checks.admin()
@commands.guild_only() @commands.guild_only()
async def serverprefix(self, ctx, *prefixes): async def serverprefix(self, ctx: commands.Context, *prefixes: str):
"""Sets Red's server prefix(es)""" """Sets Red's server prefix(es)"""
if not prefixes: if not prefixes:
await ctx.bot.db.guild(ctx.guild).prefix.set([]) await ctx.bot.db.guild(ctx.guild).prefix.set([])
@@ -879,7 +889,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@commands.cooldown(1, 60 * 10, commands.BucketType.default) @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""" """Sets Red's main owner"""
# According to the Python docs this is suitable for cryptographic use # According to the Python docs this is suitable for cryptographic use
random = SystemRandom() random = SystemRandom()
@@ -923,7 +933,7 @@ class Core(commands.Cog, CoreLogic):
@_set.command() @_set.command()
@checks.is_owner() @checks.is_owner()
async def token(self, ctx, token: str): async def token(self, ctx: commands.Context, token: str):
"""Change bot token.""" """Change bot token."""
if not isinstance(ctx.channel, discord.DMChannel): if not isinstance(ctx.channel, discord.DMChannel):
@@ -1068,7 +1078,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @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.""" """Creates a backup of all data for the instance."""
from redbot.core.data_manager import basic_config, instance_name from redbot.core.data_manager import basic_config, instance_name
from redbot.core.drivers.red_json import JSON from redbot.core.drivers.red_json import JSON
@@ -1077,20 +1087,19 @@ class Core(commands.Cog, CoreLogic):
if basic_config["STORAGE_TYPE"] == "MongoDB": if basic_config["STORAGE_TYPE"] == "MongoDB":
from redbot.core.drivers.red_mongo import Mongo 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 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: for c_name in collection_names:
if c_name == "Core": if c_name == "Core":
c_data_path = data_dir / basic_config["CORE_PATH_APPEND"] c_data_path = data_dir / basic_config["CORE_PATH_APPEND"]
else: else:
c_data_path = data_dir / basic_config["COG_PATH_APPEND"] c_data_path = data_dir / basic_config["COG_PATH_APPEND"] / c_name
output = {}
docs = await db[c_name].find().to_list(None) docs = await db[c_name].find().to_list(None)
for item in docs: for item in docs:
item_id = str(item.pop("_id")) item_id = str(item.pop("_id"))
output[item_id] = item output = item
target = JSON(c_name, data_path_override=c_data_path) target = JSON(c_name, item_id, data_path_override=c_data_path)
await target.jsonIO._threadsafe_save_json(output) await target.jsonIO._threadsafe_save_json(output)
backup_filename = "redv3-{}-{}.tar.gz".format( backup_filename = "redv3-{}-{}.tar.gz".format(
instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S") instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S")
@@ -1131,8 +1140,11 @@ class Core(commands.Cog, CoreLogic):
tar.add(str(f), recursive=False) tar.add(str(f), recursive=False)
print(str(backup_file)) print(str(backup_file))
await ctx.send( 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)")) await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
pred = MessagePredicate.yes_or_no(ctx) pred = MessagePredicate.yes_or_no(ctx)
@@ -1143,10 +1155,18 @@ class Core(commands.Cog, CoreLogic):
else: else:
if pred.result is True: if pred.result is True:
await ctx.send(_("OK, it's on its way!")) await ctx.send(_("OK, it's on its way!"))
try:
async with ctx.author.typing(): async with ctx.author.typing():
await ctx.author.send( await ctx.author.send(
_("Here's a copy of the backup"), file=discord.File(str(backup_file)) _("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: else:
await ctx.send(_("OK then.")) await ctx.send(_("OK then."))
else: else:
@@ -1154,7 +1174,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@commands.cooldown(1, 60, commands.BucketType.user) @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""" """Sends a message to the owner"""
guild = ctx.message.guild guild = ctx.message.guild
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id) owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
@@ -1197,7 +1217,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send( await ctx.send(
_("I cannot send your message, I'm unable to find my owner... *sigh*") _("I cannot send your message, I'm unable to find my owner... *sigh*")
) )
except: except discord.HTTPException:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
else: else:
await ctx.send(_("Your message has been sent.")) await ctx.send(_("Your message has been sent."))
@@ -1209,14 +1229,14 @@ class Core(commands.Cog, CoreLogic):
await ctx.send( await ctx.send(
_("I cannot send your message, I'm unable to find my owner... *sigh*") _("I cannot send your message, I'm unable to find my owner... *sigh*")
) )
except: except discord.HTTPException:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
else: else:
await ctx.send(_("Your message has been sent.")) await ctx.send(_("Your message has been sent."))
@commands.command() @commands.command()
@checks.is_owner() @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 """Sends a DM to a user
This command needs a user id to work. This command needs a user id to work.
@@ -1250,7 +1270,7 @@ class Core(commands.Cog, CoreLogic):
try: try:
await destination.send(embed=e) await destination.send(embed=e)
except: except discord.HTTPException:
await ctx.send( await ctx.send(
_("Sorry, I couldn't deliver your message to {}").format(destination) _("Sorry, I couldn't deliver your message to {}").format(destination)
) )
@@ -1260,7 +1280,7 @@ class Core(commands.Cog, CoreLogic):
response = "{}\nMessage:\n\n{}".format(description, message) response = "{}\nMessage:\n\n{}".format(description, message)
try: try:
await destination.send("{}\n{}".format(box(response), content)) await destination.send("{}\n{}".format(box(response), content))
except: except discord.HTTPException:
await ctx.send( await ctx.send(
_("Sorry, I couldn't deliver your message to {}").format(destination) _("Sorry, I couldn't deliver your message to {}").format(destination)
) )
@@ -1269,7 +1289,7 @@ class Core(commands.Cog, CoreLogic):
@commands.group() @commands.group()
@checks.is_owner() @checks.is_owner()
async def whitelist(self, ctx): async def whitelist(self, ctx: commands.Context):
""" """
Whitelist management commands. Whitelist management commands.
""" """
@@ -1287,7 +1307,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User added to whitelist.")) await ctx.send(_("User added to whitelist."))
@whitelist.command(name="list") @whitelist.command(name="list")
async def whitelist_list(self, ctx): async def whitelist_list(self, ctx: commands.Context):
""" """
Lists whitelisted users. Lists whitelisted users.
""" """
@@ -1301,7 +1321,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page)) await ctx.send(box(page))
@whitelist.command(name="remove") @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. Removes user from whitelist.
""" """
@@ -1318,7 +1338,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User was not in the whitelist.")) await ctx.send(_("User was not in the whitelist."))
@whitelist.command(name="clear") @whitelist.command(name="clear")
async def whitelist_clear(self, ctx): async def whitelist_clear(self, ctx: commands.Context):
""" """
Clears the whitelist. Clears the whitelist.
""" """
@@ -1327,19 +1347,19 @@ class Core(commands.Cog, CoreLogic):
@commands.group() @commands.group()
@checks.is_owner() @checks.is_owner()
async def blacklist(self, ctx): async def blacklist(self, ctx: commands.Context):
""" """
blacklist management commands. blacklist management commands.
""" """
pass pass
@blacklist.command(name="add") @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. Adds a user to the blacklist.
""" """
if await ctx.bot.is_owner(user): if await ctx.bot.is_owner(user):
ctx.send(_("You cannot blacklist an owner!")) await ctx.send(_("You cannot blacklist an owner!"))
return return
async with ctx.bot.db.blacklist() as curr_list: async with ctx.bot.db.blacklist() as curr_list:
@@ -1349,7 +1369,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User added to blacklist.")) await ctx.send(_("User added to blacklist."))
@blacklist.command(name="list") @blacklist.command(name="list")
async def blacklist_list(self, ctx): async def blacklist_list(self, ctx: commands.Context):
""" """
Lists blacklisted users. Lists blacklisted users.
""" """
@@ -1363,7 +1383,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page)) await ctx.send(box(page))
@blacklist.command(name="remove") @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. Removes user from blacklist.
""" """
@@ -1380,7 +1400,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("User was not in the blacklist.")) await ctx.send(_("User was not in the blacklist."))
@blacklist.command(name="clear") @blacklist.command(name="clear")
async def blacklist_clear(self, ctx): async def blacklist_clear(self, ctx: commands.Context):
""" """
Clears the blacklist. Clears the blacklist.
""" """
@@ -1390,14 +1410,14 @@ class Core(commands.Cog, CoreLogic):
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(administrator=True) @checks.admin_or_permissions(administrator=True)
async def localwhitelist(self, ctx): async def localwhitelist(self, ctx: commands.Context):
""" """
Whitelist management commands. Whitelist management commands.
""" """
pass pass
@localwhitelist.command(name="add") @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. Adds a user or role to the whitelist.
""" """
@@ -1418,7 +1438,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role added to whitelist.")) await ctx.send(_("Role added to whitelist."))
@localwhitelist.command(name="list") @localwhitelist.command(name="list")
async def localwhitelist_list(self, ctx): async def localwhitelist_list(self, ctx: commands.Context):
""" """
Lists whitelisted users and roles. Lists whitelisted users and roles.
""" """
@@ -1432,7 +1452,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page)) await ctx.send(box(page))
@localwhitelist.command(name="remove") @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. Removes user or role from whitelist.
""" """
@@ -1462,7 +1482,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role was not in the whitelist.")) await ctx.send(_("Role was not in the whitelist."))
@localwhitelist.command(name="clear") @localwhitelist.command(name="clear")
async def localwhitelist_clear(self, ctx): async def localwhitelist_clear(self, ctx: commands.Context):
""" """
Clears the whitelist. Clears the whitelist.
""" """
@@ -1472,14 +1492,14 @@ class Core(commands.Cog, CoreLogic):
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(administrator=True) @checks.admin_or_permissions(administrator=True)
async def localblacklist(self, ctx): async def localblacklist(self, ctx: commands.Context):
""" """
blacklist management commands. blacklist management commands.
""" """
pass pass
@localblacklist.command(name="add") @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. Adds a user or role to the blacklist.
""" """
@@ -1492,7 +1512,7 @@ class Core(commands.Cog, CoreLogic):
user = True user = True
if user and await ctx.bot.is_owner(obj): 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 return
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list: async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
@@ -1505,7 +1525,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role added to blacklist.")) await ctx.send(_("Role added to blacklist."))
@localblacklist.command(name="list") @localblacklist.command(name="list")
async def localblacklist_list(self, ctx): async def localblacklist_list(self, ctx: commands.Context):
""" """
Lists blacklisted users and roles. Lists blacklisted users and roles.
""" """
@@ -1519,7 +1539,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(box(page)) await ctx.send(box(page))
@localblacklist.command(name="remove") @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. Removes user or role from blacklist.
""" """
@@ -1549,7 +1569,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(_("Role was not in the blacklist.")) await ctx.send(_("Role was not in the blacklist."))
@localblacklist.command(name="clear") @localblacklist.command(name="clear")
async def localblacklist_clear(self, ctx): async def localblacklist_clear(self, ctx: commands.Context):
""" """
Clears the blacklist. Clears the blacklist.
""" """
@@ -1689,7 +1709,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.tick() await ctx.tick()
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(manage_server=True) @checks.guildowner_or_permissions(manage_guild=True)
@commands.group(name="autoimmune") @commands.group(name="autoimmune")
async def autoimmune_group(self, ctx: commands.Context): async def autoimmune_group(self, ctx: commands.Context):
""" """

View File

@@ -1,15 +1,15 @@
import sys import inspect
import os
from pathlib import Path
from typing import List
from copy import deepcopy
import hashlib
import shutil
import logging import logging
import os
import sys
import tempfile
from copy import deepcopy
from pathlib import Path
import appdirs import appdirs
import tempfile from discord.utils import deprecated
from . import commands
from .json_io import JsonIO from .json_io import JsonIO
__all__ = [ __all__ = [
@@ -153,124 +153,28 @@ def core_data_path() -> Path:
return core_path.resolve() return core_path.resolve()
def _find_data_files(init_location: str) -> (Path, List[Path]): # noinspection PyUnusedLocal
""" @deprecated("bundled_data_path() without calling this function")
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))
def load_bundled_data(cog_instance, init_location: str): 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 Get the path to the "data" directory bundled with this cog.
of the installed cog.
The bundled data folder must be located alongside the ``.py`` file
which contains the cog class.
.. important:: .. important::
This function MUST be called from the ``setup()`` function of your You should *NEVER* write to this directory.
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`.
Parameters Parameters
---------- ----------
cog_instance cog_instance
An instance of your cog. If calling from a command or method of
your cog, this should be ``self``.
Returns Returns
------- -------
@@ -280,10 +184,10 @@ def bundled_data_path(cog_instance) -> Path:
Raises Raises
------ ------
FileNotFoundError 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(): if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format(bundled_path)) raise FileNotFoundError("No such directory {}".format(bundled_path))

View File

@@ -1,7 +1,12 @@
import motor.motor_asyncio import re
from .red_base import BaseDriver from typing import Match, Pattern
from urllib.parse import quote_plus from urllib.parse import quote_plus
import motor.core
import motor.motor_asyncio
from .red_base import BaseDriver
__all__ = ["Mongo"] __all__ = ["Mongo"]
@@ -9,7 +14,7 @@ _conn = None
def _initialize(**kwargs): def _initialize(**kwargs):
kwargs.get("URI", "mongodb") uri = kwargs.get("URI", "mongodb")
host = kwargs["HOST"] host = kwargs["HOST"]
port = kwargs["PORT"] port = kwargs["PORT"]
admin_user = kwargs["USERNAME"] admin_user = kwargs["USERNAME"]
@@ -80,6 +85,7 @@ class Mongo(BaseDriver):
async def get(self, *identifiers: str): async def get(self, *identifiers: str):
mongo_collection = self.get_collection() mongo_collection = self.get_collection()
identifiers = (*map(self._escape_key, identifiers),)
dot_identifiers = ".".join(identifiers) dot_identifiers = ".".join(identifiers)
partial = await mongo_collection.find_one( partial = await mongo_collection.find_one(
@@ -91,10 +97,14 @@ class Mongo(BaseDriver):
for i in identifiers: for i in identifiers:
partial = partial[i] partial = partial[i]
if isinstance(partial, dict):
return self._unescape_dict_keys(partial)
return partial return partial
async def set(self, *identifiers: str, value=None): 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() mongo_collection = self.get_collection()
@@ -105,7 +115,7 @@ class Mongo(BaseDriver):
) )
async def clear(self, *identifiers: str): async def clear(self, *identifiers: str):
dot_identifiers = ".".join(identifiers) dot_identifiers = ".".join(map(self._escape_key, identifiers))
mongo_collection = self.get_collection() mongo_collection = self.get_collection()
if len(identifiers) > 0: if len(identifiers) > 0:
@@ -115,6 +125,62 @@ class Mongo(BaseDriver):
else: else:
await mongo_collection.delete_one({"_id": self.unique_cog_identifier}) 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(): def get_config_details():
uri = None 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

@@ -1,10 +1,10 @@
import contextlib
import sys import sys
import codecs import codecs
import datetime import datetime
import logging import logging
import traceback import traceback
from datetime import timedelta from datetime import timedelta
from distutils.version import StrictVersion
from typing import List from typing import List
import aiohttp import aiohttp
@@ -13,9 +13,9 @@ import pkg_resources
from colorama import Fore, Style, init from colorama import Fore, Style, init
from pkg_resources import DistributionNotFound from pkg_resources import DistributionNotFound
from . import __version__, commands from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
from .data_manager import storage_type 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 from .utils import fuzzy_command_search, format_fuzzy_results
log = logging.getLogger("red") log = logging.getLogger("red")
@@ -105,7 +105,6 @@ def init_events(bot, cli_flags):
prefixes = cli_flags.prefix or (await bot.db.prefix()) prefixes = cli_flags.prefix or (await bot.db.prefix())
lang = await bot.db.locale() lang = await bot.db.locale()
red_version = __version__
red_pkg = pkg_resources.get_distribution("Red-DiscordBot") red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
dpy_version = discord.__version__ dpy_version = discord.__version__
@@ -125,24 +124,22 @@ def init_events(bot, cli_flags):
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands))) INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
try: with contextlib.suppress(aiohttp.ClientError, discord.HTTPException):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r: async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
data = await r.json() data = await r.json()
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version): if VersionInfo.from_str(data["info"]["version"]) > red_version_info:
INFO.append( INFO.append(
"Outdated version! {} is available " "Outdated version! {} is available "
"but you're using {}".format(data["info"]["version"], red_version) "but you're using {}".format(data["info"]["version"], red_version)
) )
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id) owner = await bot.get_user_info(bot.owner_id)
await owner.send( await owner.send(
"Your Red instance is out of date! {} is the current " "Your Red instance is out of date! {} is the current "
"version, however you are using {}!".format( "version, however you are using {}!".format(
data["info"]["version"], red_version data["info"]["version"], red_version
) )
) )
except:
pass
INFO2 = [] INFO2 = []
sentry = await bot.db.enable_sentry() sentry = await bot.db.enable_sentry()
@@ -237,18 +234,13 @@ def init_events(bot, cli_flags):
else: else:
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False)) await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
elif isinstance(error, commands.BotMissingPermissions): elif isinstance(error, commands.BotMissingPermissions):
missing_perms: List[str] = [] if bin(error.missing.value).count("1") == 1: # Only one perm missing
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:
plural = "" plural = ""
else: else:
plural = "s" plural = "s"
await ctx.send( await ctx.send(
"I require the {perms} permission{plural} to execute that command.".format( "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): 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. This help formatter contains work by Rapptz (Danny) and SirThane#1780.
""" """
import contextlib
from collections import namedtuple from collections import namedtuple
from typing import List, Optional, Union from typing import List, Optional, Union
@@ -224,8 +225,8 @@ class Help(dpy_formatter.HelpFormatter):
return ret return ret
async def format_help_for(self, ctx, command_or_bot, reason: str = None): async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED? """Formats the help page and handles the actual heavy lifting of how
the help command looks like. To change the behaviour, override the the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method. :meth:`~.HelpFormatter.format` method.
@@ -244,10 +245,24 @@ class Help(dpy_formatter.HelpFormatter):
""" """
self.context = ctx self.context = ctx
self.command = command_or_bot 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() emb = await self.format()
if reason: if reason:
emb["embed"]["title"] = "{0}".format(reason) emb["embed"]["title"] = reason
ret = [] ret = []

View File

@@ -3,8 +3,6 @@ import re
from pathlib import Path from pathlib import Path
from typing import Callable, Union from typing import Callable, Union
from . import commands
__all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"] __all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"]
_current_locale = "en_us" _current_locale = "en_us"
@@ -219,6 +217,12 @@ class Translator(Callable[[str], str]):
self.translations.update({untranslated: translated}) 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): def cog_i18n(translator: Translator):
"""Get a class decorator to link the translator to this cog.""" """Get a class decorator to link the translator to this cog."""

View File

@@ -3,6 +3,7 @@ import json
import os import os
import asyncio import asyncio
import logging import logging
from copy import deepcopy
from uuid import uuid4 from uuid import uuid4
# This is basically our old DataIO and just a base for much more elaborate classes # 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): async def _threadsafe_save_json(self, data, settings=PRETTY):
loop = asyncio.get_event_loop() 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: async with self._lock:
await loop.run_in_executor(None, func) 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 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 Parameters
---------- ----------
guild: `discord.Guild` guild: `discord.Guild`
The guild to get the modlog channel for The guild to get the modlog channel for.
Returns Returns
------- -------
`discord.TextChannel` or `None` `discord.TextChannel`
The channel object representing the modlog channel The channel object representing the modlog channel.
Raises Raises
------ ------
RuntimeError RuntimeError
If the modlog channel is not found If the modlog channel is not found.
""" """
if hasattr(guild, "get_channel"): if hasattr(guild, "get_channel"):
channel = guild.get_channel(await _conf.guild(guild).mod_log()) channel = guild.get_channel(await _conf.guild(guild).mod_log())
else: else:
# For unit tests only
channel = await _conf.guild(guild).mod_log() channel = await _conf.guild(guild).mod_log()
if channel is None: if channel is None:
raise RuntimeError("Failed to get the mod log channel!") raise RuntimeError("Failed to get the mod log channel!")

View File

@@ -1,5 +1,8 @@
import itertools import itertools
from typing import Sequence, Iterator, List from typing import Sequence, Iterator, List
import discord
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
_ = Translator("UtilsChatFormatting", __file__) _ = Translator("UtilsChatFormatting", __file__)
@@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
return text 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*. """Get comma-separted list, with the last element joined with *and*.
This uses an Oxford comma, because without one, items containing This uses an Oxford comma, because without one, items containing
@@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
if len(items) == 1: if len(items) == 1:
return items[0] return items[0]
return ", ".join(items[:-1]) + _(", and ") + items[-1] 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 # noinspection PyAsyncCall
start_adding_reactions(message, controls.keys(), ctx.bot.loop) start_adding_reactions(message, controls.keys(), ctx.bot.loop)
else: else:
try:
if isinstance(current_page, discord.Embed): if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page) await message.edit(embed=current_page)
else: else:
await message.edit(content=current_page) await message.edit(content=current_page)
except discord.NotFound:
return
try: try:
react, user = await ctx.bot.wait_for( react, user = await ctx.bot.wait_for(
@@ -90,9 +93,12 @@ async def menu(
except discord.Forbidden: # cannot remove all reactions except discord.Forbidden: # cannot remove all reactions
for key in controls.keys(): for key in controls.keys():
await message.remove_reaction(key, ctx.bot.user) await message.remove_reaction(key, ctx.bot.user)
return None except discord.NotFound:
return
return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji) else:
return await controls[react.emoji](
ctx, pages, controls, message, page, timeout, react.emoji
)
async def next_page( async def next_page(
@@ -106,10 +112,8 @@ async def next_page(
): ):
perms = message.channel.permissions_for(ctx.me) perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react if perms.manage_messages: # Can manage messages, so remove react
try: with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author) await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == len(pages) - 1: if page == len(pages) - 1:
page = 0 # Loop around to the first item page = 0 # Loop around to the first item
else: else:
@@ -128,10 +132,8 @@ async def prev_page(
): ):
perms = message.channel.permissions_for(ctx.me) perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react if perms.manage_messages: # Can manage messages, so remove react
try: with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author) await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == 0: if page == 0:
page = len(pages) - 1 # Loop around to the last item page = len(pages) - 1 # Loop around to the last item
else: else:
@@ -148,9 +150,8 @@ async def close_menu(
timeout: float, timeout: float,
emoji: str, emoji: str,
): ):
if message: with contextlib.suppress(discord.NotFound):
await message.delete() await message.delete()
return None
def start_adding_reactions( def start_adding_reactions(
@@ -161,7 +162,7 @@ def start_adding_reactions(
"""Start adding reactions to a message. """Start adding reactions to a message.
This is a non-blocking operation - calling this will schedule the 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. execute asynchronously. There is no need to await this function.
This is particularly useful if you wish to start waiting for a 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 is exactly what `menu` uses to do that.
This spawns a `asyncio.Task` object and schedules it on ``loop``. 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`. `asyncio.get_event_loop`.
Parameters Parameters

View File

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

View File

@@ -8,18 +8,14 @@ import asyncio
import aiohttp import aiohttp
import pkg_resources import pkg_resources
from pathlib import Path
from distutils.version import StrictVersion
from redbot.setup import ( from redbot.setup import (
basic_setup, basic_setup,
load_existing_config, load_existing_config,
remove_instance, remove_instance,
remove_instance_interaction, remove_instance_interaction,
create_backup, create_backup,
save_config,
) )
from redbot.core import __version__ from redbot.core import __version__, version_info as red_version_info, VersionInfo
from redbot.core.utils import safe_delete
from redbot.core.cli import confirm from redbot.core.cli import confirm
if sys.platform == "linux": if sys.platform == "linux":
@@ -390,7 +386,7 @@ async def is_outdated():
async with session.get("{}/json".format(red_pypi)) as r: async with session.get("{}/json".format(red_pypi)) as r:
data = await r.json() data = await r.json()
new_version = data["info"]["version"] new_version = data["info"]["version"]
return StrictVersion(new_version) > StrictVersion(__version__), new_version return VersionInfo.from_str(new_version) > red_version_info, new_version
def main_menu(): def main_menu():

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import argparse import argparse
import asyncio import asyncio
import json import json
@@ -177,26 +176,21 @@ def basic_setup():
async def json_to_mongo(current_data_dir: Path, storage_details: dict): async def json_to_mongo(current_data_dir: Path, storage_details: dict):
from redbot.core.drivers.red_mongo import Mongo from redbot.core.drivers.red_mongo import Mongo
core_data_file = list(current_data_dir.glob("core/settings.json"))[0] core_data_file = current_data_dir / "core" / "settings.json"
m = Mongo("Core", "0", **storage_details) driver = Mongo(cog_name="Core", identifier="0", **storage_details)
with core_data_file.open(mode="r") as f: with core_data_file.open(mode="r") as f:
core_data = json.loads(f.read()) core_data = json.loads(f.read())
collection = m.get_collection() data = core_data.get("0", {})
await collection.update_one( for key, value in data.items():
{"_id": m.unique_cog_identifier}, update={"$set": core_data["0"]}, upsert=True await driver.set(key, value=value)
)
for p in current_data_dir.glob("cogs/**/settings.json"): for p in current_data_dir.glob("cogs/**/settings.json"):
cog_name = p.parent.stem
with p.open(mode="r") as f: with p.open(mode="r") as f:
cog_data = json.loads(f.read()) cog_data = json.load(f)
cog_i = None for identifier, data in cog_data.items():
for ident in list(cog_data.keys()): driver = Mongo(cog_name, identifier, **storage_details)
cog_i = str(hash(ident)) for key, value in data.items():
cog_m = Mongo(p.parent.stem, cog_i, **storage_details) await driver.set(key, value=value)
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
)
async def mongo_to_json(current_data_dir: Path, storage_details: dict): 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-json-rpc==0.11.2",
"aiohttp==3.4.4", "aiohttp==3.4.4",
"appdirs==1.4.3", "appdirs==1.4.3",
"async-timeout==3.0.0", "async-timeout==3.0.1",
"attrs==18.2.0", "attrs==18.2.0",
"chardet==3.0.4", "chardet==3.0.4",
"colorama==0.3.9", "colorama==0.4.1",
"discord.py>=1.0.0a0", "discord.py>=1.0.0a0",
"distro==1.3.0; sys_platform == 'linux'", "distro==1.3.0; sys_platform == 'linux'",
"fuzzywuzzy==0.17.0", "fuzzywuzzy==0.17.0",
"idna-ssl==1.1.0", "idna-ssl==1.1.0",
"idna==2.7", "idna==2.8",
"multidict==4.4.2", "multidict==4.5.2",
"python-levenshtein==0.12.0", "python-levenshtein==0.12.0",
"pyyaml==3.13", "pyyaml==3.13",
"raven==6.9.0", "raven==6.10.0",
"raven-aiohttp==0.7.0", "raven-aiohttp==0.7.0",
"schema==0.6.8", "schema==0.6.8",
"websockets==6.0", "websockets==6.0",
"yarl==1.2.6", "yarl==1.3.0",
] ]
extras_require = { extras_require = {
"test": [ "test": [
"atomicwrites==1.2.1", "atomicwrites==1.2.1",
"more-itertools==4.3.0", "more-itertools==5.0.0",
"pluggy==0.7.1", "pluggy==0.8.1",
"py==1.6.0", "py==1.7.0",
"pytest==3.8.2", "pytest==4.1.0",
"pytest-asyncio==0.9.0", "pytest-asyncio==0.10.0",
"six==1.11.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": [ "docs": [
"alabaster==0.7.11", "alabaster==0.7.12",
"babel==2.6.0", "babel==2.6.0",
"certifi==2018.8.24", "certifi==2018.11.29",
"docutils==0.14", "docutils==0.14",
"imagesize==1.1.0", "imagesize==1.1.0",
"Jinja2==2.10", "Jinja2==2.10",
"MarkupSafe==1.0", "MarkupSafe==1.1.0",
"packaging==18.0", "packaging==18.0",
"pyparsing==2.2.2", "pyparsing==2.3.0",
"Pygments==2.2.0", "Pygments==2.3.1",
"pytz==2018.5", "pytz==2018.9",
"requests==2.19.1", "requests==2.21.0",
"urllib3==1.23", "six==1.12.0",
"six==1.11.0",
"snowballstemmer==1.2.1", "snowballstemmer==1.2.1",
"sphinx==1.7.9", "sphinx==1.8.3",
"sphinx_rtd_theme==0.4.1", "sphinx_rtd_theme==0.4.2",
"sphinxcontrib-asyncio==0.2.0", "sphinxcontrib-asyncio==0.2.0",
"sphinxcontrib-websupport==1.1.0", "sphinxcontrib-websupport==1.1.0",
"urllib3==1.24.1",
], ],
"voice": ["red-lavalink==0.1.2"], "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" 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(): def test_schema_update():
old = { old = {
GLOBAL: { str(GLOBAL): {
"owner_models": { "owner_models": {
"cogs": { "cogs": {
"Admin": {"allow": [78631113035100160], "deny": [96733288462286848]}, "Admin": {"allow": [78631113035100160], "deny": [96733288462286848]},
@@ -19,7 +19,7 @@ def test_schema_update():
}, },
} }
}, },
43733288462286848: { "43733288462286848": {
"owner_models": { "owner_models": {
"cogs": { "cogs": {
"Admin": { "Admin": {
@@ -43,22 +43,22 @@ def test_schema_update():
assert new == ( assert new == (
{ {
"Admin": { "Admin": {
GLOBAL: {78631113035100160: True, 96733288462286848: False}, str(GLOBAL): {"78631113035100160": True, "96733288462286848": False},
43733288462286848: {24231113035100160: True, 35533288462286848: False}, "43733288462286848": {"24231113035100160": True, "35533288462286848": False},
}, },
"Audio": {GLOBAL: {133049272517001216: True, "default": False}}, "Audio": {str(GLOBAL): {"133049272517001216": True, "default": False}},
"General": {43733288462286848: {133049272517001216: True, "default": False}}, "General": {"43733288462286848": {"133049272517001216": True, "default": False}},
}, },
{ {
"cleanup bot": { "cleanup bot": {
GLOBAL: {78631113035100160: True, "default": False}, str(GLOBAL): {"78631113035100160": True, "default": False},
43733288462286848: {17831113035100160: True, "default": True}, "43733288462286848": {"17831113035100160": True, "default": True},
}, },
"ping": {GLOBAL: {96733288462286848: True, "default": True}}, "ping": {str(GLOBAL): {"96733288462286848": True, "default": True}},
"set adminrole": { "set adminrole": {
43733288462286848: { "43733288462286848": {
87733288462286848: True, "87733288462286848": True,
95433288462286848: False, "95433288462286848": False,
"default": True, "default": True,
} }
}, },

View File

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

View File

@@ -475,3 +475,18 @@ async def test_get_raw_mixes_defaults(config):
subgroup = await config.get_raw("subgroup") subgroup = await config.get_raw("subgroup")
assert subgroup == {"foo": True, "bar": False} 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}}

View File

@@ -1,6 +1,36 @@
from redbot import core from redbot import core
from redbot.core import VersionInfo
def test_version_working(): def test_version_working():
assert hasattr(core, "__version__") assert hasattr(core, "__version__")
assert core.__version__[0] == "3" assert core.__version__[0] == "3"
# When adding more of these, ensure they are added in ascending order of precedence
version_tests = (
"3.0.0a32.post10.dev12",
"3.0.0rc1.dev1",
"3.0.0rc1",
"3.0.0",
"3.0.1",
"3.0.1.post1.dev1",
"3.0.1.post1",
"2018.10.6b21",
)
def test_version_info_str_parsing():
for version_str in version_tests:
assert version_str == str(VersionInfo.from_str(version_str))
def test_version_info_lt():
for next_idx, cur in enumerate(version_tests[:-1], start=1):
cur_test = VersionInfo.from_str(cur)
next_test = VersionInfo.from_str(version_tests[next_idx])
assert cur_test < next_test
def test_version_info_gt():
assert VersionInfo.from_str(version_tests[1]) > VersionInfo.from_str(version_tests[0])