Compare commits

...

22 Commits
3.3.3 ... 3.0.1

Author SHA1 Message Date
Caleb Johnson
343132a371 [Audio] Connect to lavalink in the background (#2460)
Also:
- restart and reconnect if connection settings change
  - shutdown and restart if not configured to use external
- show a message in [p]play et al. when the connection hasn't been made
- move the JAR download to manager so audio.py can access it
- only start if no process exists
- bump red-lavalink to 0.2.3

Resolves #2306
2019-02-17 09:47:19 +11:00
Toby Harradine
e97240e568 Bump version to 3.0.1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 12:25:26 +11:00
Michael H
6383e9f738 [Utils] Add filters for spoiler markdown (#2401)
This also wraps some fields of the modlog with the same sanitization, as well as the `[p]names` command.
2019-02-16 11:56:20 +11:00
Michael H
d96062226d [Audio] Remove players which no longer have a guild. (#2414)
Cleanup players when the bot has one for a guild it leaves.

Bumps Red-Lavalink to v0.2.2
2019-02-16 11:17:18 +11:00
Twentysix
eebed27fe7 [Downloader] [p]pipinstall: Handle no args 2019-02-16 11:06:56 +11:00
Toby Harradine
278abecdef Add missing version bumps from 7c8ac9c
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 11:06:27 +11:00
ZeLarpMaster
21ac81679f [Trivia] Fix typo in cars.yaml (#2456) 2019-02-16 09:17:43 +11:00
DiscordLiz
5cf0c98bca Prevent error when ctx.send_interactive prompt is deleted (#2447)
Supress excpetions which may occur when attempting to delete a prompt.

fixes #2380
2019-02-16 09:17:34 +11:00
Toby Harradine
7c8ac9cd54 Update dependencies and copyright year (#2436)
- aiohttp 3.5
- websockets 7
- Rapptz/discord.py@700dbb5
- A few others

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:17:02 +11:00
Toby Harradine
cfd8ef6025 Guard parsing of CLI args in launcher, setup scripts (#2432)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:06:28 +11:00
PredaaA
174754f800 Update help_formatter.py (#2431) 2019-02-16 09:06:11 +11:00
Twentysix
53aa84bb94 [p]userinfo: Handle target w/ 'None' Member.joined_at (#2426) 2019-02-16 09:06:04 +11:00
Toby Harradine
a2c0bddd6b Default rules for subcommands precede supercommands (#2422)
This incorporates default rules into the same resolution techniques used by concrete rules.

Resolves #2313.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:05:58 +11:00
zephyrkul
7dd3310377 [Downloader] Use shlex for subprocesses (#2421) 2019-02-16 09:05:50 +11:00
Toby Harradine
0fa2e36629 Always tick Voice requirements on startup screen (#2413)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:05:41 +11:00
zephyrkul
3b38a5f9b9 Send help on empty [p]load/unload/reload (#2410)
Rather than attempting to load / reload / unload nothing, send command help on bare `[p]load`, etc. commands.
2019-02-16 09:05:32 +11:00
zephyrkul
f61e8e0907 [Core] Utilize consume rest, Union (#2407) 2019-02-16 09:05:17 +11:00
Michael H
5fc8f9fee1 prevent traceback (#2406)
* prevent traceback related to  Rapptz/discord.py#1638

* formatting
2019-02-16 09:05:08 +11:00
Michael H
644bb0a560 Improve usability of warnings/unwarn\n resolves #2403 (#2404) 2019-02-16 09:04:58 +11:00
PredaaA
93ea773b7c [Core cmds] [p]servers: handle message deletion (#2400) 2019-02-16 09:04:50 +11:00
Redjumpman
655b5a96ba [Utils] Fix for MessagePredicate.lower_contained_in (#2399)
Added a missing str.lower() method when checking to see if the content is in the list.
2019-02-16 09:04:41 +11:00
Toby Harradine
bbe88293ab Use python-Levenshtein-wheels (#2393)
This removes the compiler detection logic in setup.py. python-Levenshtein-wheels includes pre-built wheels for virtually all operating systems and architectures we support.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:04:32 +11:00
74 changed files with 995 additions and 546 deletions

View File

@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
Red - A fully customizable Discord bot Red - A fully customizable Discord bot
Copyright (C) 2015-2018 Twentysix Copyright (C) 2015-2019 Twentysix
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
Red-DiscordBot Copyright (C) 2015-2018 Twentysix Red-DiscordBot Copyright (C) 2015-2019 Twentysix
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

View File

@@ -9,5 +9,5 @@ gettext:
REF?=rewrite REF?=rewrite
update_vendor: update_vendor:
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/$(REF).tar.gz#egg=discord.py pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/$(REF).tar.gz#egg=discord.py
rm -r discord.py*.egg-info rm -r discord.py*-info
$(MAKE) reformat $(MAKE) reformat

View File

@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
"e1839a8" = { path = ".", editable = true, extras = ['mongo', 'voice'] } red-discordbot = {path = ".",editable = true,extras = ['mongo', 'voice']}
[dev-packages] [dev-packages]
tox = "*" tox = "*"
"e1839a9" = { path = ".", editable = true, extras = ['docs', 'test', 'style'] } red-discordbot = {path = ".",editable = true,extras = ['docs', 'test', 'style']}

376
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "57184ef83392116db24a1966022ad358f54048bb43d428d47a6e31f1a88fc434" "sha256": "b9f385e4c53c659dd76e8722d1fb69c244d3a76e4b0dfc40956ff2493277c1f6"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -16,37 +16,37 @@
"default": { "default": {
"aiohttp": { "aiohttp": {
"hashes": [ "hashes": [
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b", "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08", "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd", "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac", "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650", "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa", "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95", "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330", "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc", "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b", "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de", "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4", "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7", "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b", "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8", "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd", "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2", "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698", "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95", "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6", "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0", "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07" "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
], ],
"version": "==3.4.4" "version": "==3.5.4"
}, },
"aiohttp-json-rpc": { "aiohttp-json-rpc": {
"hashes": [ "hashes": [
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32", "sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8" "sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
], ],
"version": "==0.11.2" "version": "==0.12"
}, },
"appdirs": { "appdirs": {
"hashes": [ "hashes": [
@@ -85,10 +85,10 @@
}, },
"distro": { "distro": {
"hashes": [ "hashes": [
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742", "sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158" "sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
], ],
"version": "==1.3.0" "version": "==1.4.0"
}, },
"dnspython": { "dnspython": {
"hashes": [ "hashes": [
@@ -97,14 +97,6 @@
], ],
"version": "==1.16.0" "version": "==1.16.0"
}, },
"e1839a8": {
"editable": true,
"extras": [
"mongo",
"voice"
],
"path": "."
},
"fuzzywuzzy": { "fuzzywuzzy": {
"hashes": [ "hashes": [
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254", "sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
@@ -205,11 +197,43 @@
], ],
"version": "==3.7.2" "version": "==3.7.2"
}, },
"python-levenshtein": { "python-levenshtein-wheels": {
"hashes": [ "hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" "sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
], ],
"version": "==0.12.0" "version": "==0.13.1"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
@@ -241,11 +265,20 @@
], ],
"version": "==0.7.0" "version": "==0.7.0"
}, },
"red-discordbot": {
"editable": true,
"extras": [
"mongo",
"voice"
],
"path": "."
},
"red-lavalink": { "red-lavalink": {
"hashes": [ "hashes": [
"sha256:6a1a34471ccf4630eee537049568dd87e8e93614f1d1ce355dd74e5b10079782" "sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
], ],
"version": "==0.1.2" "version": "==0.2.3"
}, },
"schema": { "schema": {
"hashes": [ "hashes": [
@@ -256,29 +289,29 @@
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", "sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", "sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", "sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", "sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", "sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", "sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", "sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", "sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", "sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", "sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", "sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", "sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", "sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", "sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", "sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", "sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", "sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", "sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", "sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", "sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" "sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
], ],
"version": "==6.0" "version": "==7.0"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [
@@ -300,37 +333,37 @@
"develop": { "develop": {
"aiohttp": { "aiohttp": {
"hashes": [ "hashes": [
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b", "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08", "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd", "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac", "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650", "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa", "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95", "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330", "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc", "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b", "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de", "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4", "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7", "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b", "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8", "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd", "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2", "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698", "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95", "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6", "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0", "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07" "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
], ],
"version": "==3.4.4" "version": "==3.5.4"
}, },
"aiohttp-json-rpc": { "aiohttp-json-rpc": {
"hashes": [ "hashes": [
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32", "sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8" "sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
], ],
"version": "==0.11.2" "version": "==0.12"
}, },
"alabaster": { "alabaster": {
"hashes": [ "hashes": [
@@ -355,10 +388,10 @@
}, },
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
], ],
"version": "==1.2.1" "version": "==1.3.0"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
@@ -411,10 +444,10 @@
}, },
"distro": { "distro": {
"hashes": [ "hashes": [
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742", "sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158" "sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
], ],
"version": "==1.3.0" "version": "==1.4.0"
}, },
"docutils": { "docutils": {
"hashes": [ "hashes": [
@@ -424,15 +457,6 @@
], ],
"version": "==0.14" "version": "==0.14"
}, },
"e1839a9": {
"editable": true,
"extras": [
"docs",
"test",
"style"
],
"path": "."
},
"filelock": { "filelock": {
"hashes": [ "hashes": [
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
@@ -509,11 +533,10 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
], ],
"version": "==5.0.0" "version": "==6.0.0"
}, },
"multidict": { "multidict": {
"hashes": [ "hashes": [
@@ -551,10 +574,10 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
], ],
"version": "==18.0" "version": "==19.0"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@@ -579,17 +602,17 @@
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
], ],
"version": "==2.3.0" "version": "==2.3.1"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02", "sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d" "sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
], ],
"version": "==4.1.0" "version": "==4.2.0"
}, },
"pytest-asyncio": { "pytest-asyncio": {
"hashes": [ "hashes": [
@@ -598,11 +621,43 @@
], ],
"version": "==0.10.0" "version": "==0.10.0"
}, },
"python-levenshtein": { "python-levenshtein-wheels": {
"hashes": [ "hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" "sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
], ],
"version": "==0.12.0" "version": "==0.13.1"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
@@ -641,6 +696,21 @@
], ],
"version": "==0.7.0" "version": "==0.7.0"
}, },
"red-discordbot": {
"editable": true,
"extras": [
"mongo",
"voice"
],
"path": "."
},
"red-lavalink": {
"hashes": [
"sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
],
"version": "==0.2.3"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
@@ -671,17 +741,17 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", "sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287",
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8" "sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c"
], ],
"version": "==1.8.3" "version": "==1.8.4"
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [
"sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09", "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
"sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96" "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
], ],
"version": "==0.4.2" "version": "==0.4.3"
}, },
"sphinxcontrib-asyncio": { "sphinxcontrib-asyncio": {
"hashes": [ "hashes": [
@@ -720,36 +790,36 @@
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c", "sha256:8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db",
"sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd" "sha256:cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"
], ],
"version": "==16.2.0" "version": "==16.4.0"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", "sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", "sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", "sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", "sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", "sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", "sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", "sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", "sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", "sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", "sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", "sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", "sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", "sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", "sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", "sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", "sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", "sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", "sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", "sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", "sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" "sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
], ],
"version": "==6.0" "version": "==7.0"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [

View File

@@ -6,7 +6,7 @@ Discord API Wrapper
A basic wrapper for the Discord API. A basic wrapper for the Discord API.
:copyright: (c) 2015-2017 Rapptz :copyright: (c) 2015-2019 Rapptz
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
@@ -14,7 +14,7 @@ A basic wrapper for the Discord API.
__title__ = "discord" __title__ = "discord"
__author__ = "Rapptz" __author__ = "Rapptz"
__license__ = "MIT" __license__ = "MIT"
__copyright__ = "Copyright 2015-2017 Rapptz" __copyright__ = "Copyright 2015-2019 Rapptz"
__version__ = "1.0.0a" __version__ = "1.0.0a"
from collections import namedtuple from collections import namedtuple

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -667,6 +667,28 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
ret.sort(key=comparator) ret.sort(key=comparator)
return ret return ret
@property
def text_channels(self):
"""List[:class:`TextChannel`]: Returns the text channels that are under this category."""
ret = [
c
for c in self.guild.channels
if c.category_id == self.id and isinstance(c, TextChannel)
]
ret.sort(key=lambda c: (c.position, c.id))
return ret
@property
def voice_channels(self):
"""List[:class:`VoiceChannel`]: Returns the text channels that are under this category."""
ret = [
c
for c in self.guild.channels
if c.category_id == self.id and isinstance(c, VoiceChannel)
]
ret.sort(key=lambda c: (c.position, c.id))
return ret
class DMChannel(discord.abc.Messageable, Hashable): class DMChannel(discord.abc.Messageable, Hashable):
"""Represents a Discord direct message channel. """Represents a Discord direct message channel.

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -76,6 +76,13 @@ class PartialEmoji(namedtuple("PartialEmoji", "animated name id")):
return "<a:%s:%s>" % (self.name, self.id) return "<a:%s:%s>" % (self.name, self.id)
return "<:%s:%s>" % (self.name, self.id) return "<:%s:%s>" % (self.name, self.id)
def __eq__(self, other):
if self.is_unicode_emoji():
return isinstance(other, PartialEmoji) and self.name == other.name
if isinstance(other, (PartialEmoji, Emoji)):
return self.id == other.id
def is_custom_emoji(self): def is_custom_emoji(self):
"""Checks if this is a custom non-Unicode emoji.""" """Checks if this is a custom non-Unicode emoji."""
return self.id is not None return self.id is not None
@@ -186,6 +193,9 @@ class Emoji(Hashable):
def __repr__(self): def __repr__(self):
return "<Emoji id={0.id} name={0.name!r}>".format(self) return "<Emoji id={0.id} name={0.name!r}>".format(self)
def __eq__(self, other):
return isinstance(other, (PartialEmoji, Emoji)) and self.id == other.id
@property @property
def created_at(self): def created_at(self):
"""Returns the emoji's creation time in UTC.""" """Returns the emoji's creation time in UTC."""

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -30,6 +30,7 @@ __all__ = [
"ChannelType", "ChannelType",
"MessageType", "MessageType",
"VoiceRegion", "VoiceRegion",
"SpeakingState",
"VerificationLevel", "VerificationLevel",
"ContentFilter", "ContentFilter",
"Status", "Status",
@@ -91,6 +92,16 @@ class VoiceRegion(Enum):
return self.value return self.value
class SpeakingState(IntEnum):
none = 0
voice = 1
soundshare = 2
priority = 4
def __str__(self):
return self.name
class VerificationLevel(IntEnum): class VerificationLevel(IntEnum):
none = 0 none = 0
low = 1 low = 1

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -6,7 +6,7 @@ discord.ext.commands
An extension module to facilitate creation of bot commands. An extension module to facilitate creation of bot commands.
:copyright: (c) 2017 Rapptz :copyright: (c) 2019 Rapptz
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -443,9 +443,9 @@ class BotBase(GroupMixin):
Parameters Parameters
----------- -----------
func : :ref:`coroutine <coroutine>` func : :ref:`coroutine <coroutine>`
The extra event to listen to. The function to call.
name : Optional[str] name : Optional[str]
The name of the command to use. Defaults to ``func.__name__``. The name of the event to listen for. Defaults to ``func.__name__``.
Example Example
-------- --------

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -523,7 +523,7 @@ class clean_content(Converter):
result = pattern.sub(repl, argument) result = pattern.sub(repl, argument)
if self.escape_markdown: if self.escape_markdown:
transformations = {re.escape(c): "\\" + c for c in ("*", "`", "_", "~", "\\")} transformations = {re.escape(c): "\\" + c for c in ("*", "`", "_", "~", "\\", "||")}
def replace(obj): def replace(obj):
return transformations.get(re.escape(obj.group(0)), "") return transformations.get(re.escape(obj.group(0)), "")

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -385,22 +385,18 @@ class Command:
# for use with a manual undo # for use with a manual undo
previous = view.index previous = view.index
# parsing errors get propagated
view.skip_ws() view.skip_ws()
argument = quoted_word(view) argument = quoted_word(view)
try: try:
value = await self.do_conversion(ctx, converter, argument, param) value = await self.do_conversion(ctx, converter, argument, param)
except CommandError: except CommandError:
if not result:
if required:
raise
else:
view.index = previous
return param.default
view.index = previous view.index = previous
break break
else: else:
result.append(value) result.append(value)
if not result and not required:
return param.default
return result return result
async def _transform_greedy_var_pos(self, ctx, param, converter): async def _transform_greedy_var_pos(self, ctx, param, converter):
@@ -750,9 +746,9 @@ class Command:
If that lookup leads to an empty string then the first line of the If that lookup leads to an empty string then the first line of the
:attr:`help` attribute is used instead. :attr:`help` attribute is used instead.
""" """
if self.brief: if self.brief is not None:
return self.brief return self.brief
if self.help: if self.help is not None:
return self.help.split("\n", 1)[0] return self.help.split("\n", 1)[0]
return "" return ""
@@ -771,7 +767,7 @@ class Command:
name = self.name if not parent else parent + " " + self.name name = self.name if not parent else parent + " " + self.name
result.append(name) result.append(name)
if self.usage: if self.usage is not None:
result.append(self.usage) result.append(self.usage)
return " ".join(result) return " ".join(result)

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE.
import itertools import itertools
import inspect import inspect
import discord.utils
from .core import GroupMixin, Command from .core import GroupMixin, Command
from .errors import CommandError from .errors import CommandError
@@ -183,7 +184,9 @@ class HelpFormatter:
if commands: if commands:
return max( return max(
map( map(
lambda c: len(c.name) if self.show_hidden or not c.hidden else 0, lambda c: discord.utils._string_width(c.name)
if self.show_hidden or not c.hidden
else 0,
commands.values(), commands.values(),
) )
) )
@@ -272,8 +275,10 @@ class HelpFormatter:
if name in command.aliases: if name in command.aliases:
# skip aliases # skip aliases
continue continue
width_gap = discord.utils._string_width(name) - len(name)
entry = " {0:<{width}} {1}".format(name, command.short_doc, width=max_width) entry = " {0:<{width}} {1}".format(
name, command.short_doc, width=max_width - width_gap
)
shortened = self.shorten(entry) shortened = self.shorten(entry)
self._paginator.add_line(shortened) self._paginator.add_line(shortened)

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE.
import asyncio import asyncio
from collections import namedtuple from collections import namedtuple
import concurrent.futures
import json import json
import logging import logging
import struct import struct
@@ -38,6 +39,7 @@ import websockets
from . import utils from . import utils
from .activity import _ActivityTag from .activity import _ActivityTag
from .enums import SpeakingState
from .errors import ConnectionClosed, InvalidArgument from .errors import ConnectionClosed, InvalidArgument
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -72,6 +74,8 @@ class KeepAliveHandler(threading.Thread):
self.daemon = True self.daemon = True
self.shard_id = shard_id self.shard_id = shard_id
self.msg = "Keeping websocket alive with sequence %s." self.msg = "Keeping websocket alive with sequence %s."
self.block_msg = "Heartbeat blocked for more than %s seconds."
self.behind_msg = "Can't keep up, websocket is %.1fs behind."
self._stop_ev = threading.Event() self._stop_ev = threading.Event()
self._last_ack = time.perf_counter() self._last_ack = time.perf_counter()
self._last_send = time.perf_counter() self._last_send = time.perf_counter()
@@ -102,7 +106,15 @@ class KeepAliveHandler(threading.Thread):
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try: try:
# block until sending is complete # block until sending is complete
f.result() total = 0
while True:
try:
f.result(5)
break
except concurrent.futures.TimeoutError:
total += 5
log.warning(self.block_msg, total)
except Exception: except Exception:
self.stop() self.stop()
else: else:
@@ -118,12 +130,16 @@ class KeepAliveHandler(threading.Thread):
ack_time = time.perf_counter() ack_time = time.perf_counter()
self._last_ack = ack_time self._last_ack = ack_time
self.latency = ack_time - self._last_send self.latency = ack_time - self._last_send
if self.latency > 10:
log.warning(self.behind_msg, self.latency)
class VoiceKeepAliveHandler(KeepAliveHandler): class VoiceKeepAliveHandler(KeepAliveHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.msg = "Keeping voice websocket alive with timestamp %s." self.msg = "Keeping voice websocket alive with timestamp %s."
self.block_msg = "Voice heartbeat blocked for more than %s seconds"
self.behind_msg = "Can't keep up, voice websocket is %.1fs behind"
def get_payload(self): def get_payload(self):
return {"op": self.ws.HEARTBEAT, "d": int(time.time() * 1000)} return {"op": self.ws.HEARTBEAT, "d": int(time.time() * 1000)}
@@ -482,7 +498,7 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
async def send_as_json(self, data): async def send_as_json(self, data):
try: try:
await super().send(utils.to_json(data)) await self.send(utils.to_json(data))
except websockets.exceptions.ConnectionClosed as exc: except websockets.exceptions.ConnectionClosed as exc:
if not self._can_handle_close(exc.code): if not self._can_handle_close(exc.code):
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
@@ -561,6 +577,10 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
Receive only. Tells you that your websocket connection was acknowledged. Receive only. Tells you that your websocket connection was acknowledged.
INVALIDATE_SESSION INVALIDATE_SESSION
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY. Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
CLIENT_CONNECT
Indicates a user has connected to voice.
CLIENT_DISCONNECT
Receive only. Indicates a user has disconnected from voice.
""" """
IDENTIFY = 0 IDENTIFY = 0
@@ -573,6 +593,8 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
RESUME = 7 RESUME = 7
HELLO = 8 HELLO = 8
INVALIDATE_SESSION = 9 INVALIDATE_SESSION = 9
CLIENT_CONNECT = 12
CLIENT_DISCONNECT = 13
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -611,7 +633,7 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
@classmethod @classmethod
async def from_client(cls, client, *, resume=False): async def from_client(cls, client, *, resume=False):
"""Creates a voice websocket for the :class:`VoiceClient`.""" """Creates a voice websocket for the :class:`VoiceClient`."""
gateway = "wss://" + client.endpoint + "/?v=3" gateway = "wss://" + client.endpoint + "/?v=4"
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None) ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
ws.gateway = gateway ws.gateway = gateway
ws._connection = client ws._connection = client
@@ -624,19 +646,21 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
return ws return ws
async def select_protocol(self, ip, port): async def select_protocol(self, ip, port, mode):
payload = { payload = {
"op": self.SELECT_PROTOCOL, "op": self.SELECT_PROTOCOL,
"d": { "d": {"protocol": "udp", "data": {"address": ip, "port": port, "mode": mode}},
"protocol": "udp",
"data": {"address": ip, "port": port, "mode": "xsalsa20_poly1305"},
},
} }
await self.send_as_json(payload) await self.send_as_json(payload)
async def speak(self, is_speaking=True): async def client_connect(self):
payload = {"op": self.SPEAKING, "d": {"speaking": is_speaking, "delay": 0}} payload = {"op": self.CLIENT_CONNECT, "d": {"audio_ssrc": self._connection.ssrc}}
await self.send_as_json(payload)
async def speak(self, state=SpeakingState.voice):
payload = {"op": self.SPEAKING, "d": {"speaking": int(state), "delay": 0}}
await self.send_as_json(payload) await self.send_as_json(payload)
@@ -646,9 +670,6 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
data = msg.get("d") data = msg.get("d")
if op == self.READY: if op == self.READY:
interval = data["heartbeat_interval"] / 1000.0
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
self._keep_alive.start()
await self.initial_connection(data) await self.initial_connection(data)
elif op == self.HEARTBEAT_ACK: elif op == self.HEARTBEAT_ACK:
self._keep_alive.ack() self._keep_alive.ack()
@@ -656,7 +677,12 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
log.info("Voice RESUME failed.") log.info("Voice RESUME failed.")
await self.identify() await self.identify()
elif op == self.SESSION_DESCRIPTION: elif op == self.SESSION_DESCRIPTION:
self._connection.mode = data["mode"]
await self.load_secret_key(data) await self.load_secret_key(data)
elif op == self.HELLO:
interval = data["heartbeat_interval"] / 1000.0
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
self._keep_alive.start()
async def initial_connection(self, data): async def initial_connection(self, data):
state = self._connection state = self._connection
@@ -677,15 +703,23 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
# the port is a little endian unsigned short in the last two bytes # the port is a little endian unsigned short in the last two bytes
# yes, this is different endianness from everything else # yes, this is different endianness from everything else
state.port = struct.unpack_from("<H", recv, len(recv) - 2)[0] state.port = struct.unpack_from("<H", recv, len(recv) - 2)[0]
log.debug("detected ip: %s port: %s", state.ip, state.port) log.debug("detected ip: %s port: %s", state.ip, state.port)
await self.select_protocol(state.ip, state.port)
log.info("selected the voice protocol for use") # there *should* always be at least one supported mode (xsalsa20_poly1305)
modes = [mode for mode in data["modes"] if mode in self._connection.supported_modes]
log.debug("received supported encryption modes: %s", ", ".join(modes))
mode = modes[0]
await self.select_protocol(state.ip, state.port, mode)
log.info("selected the voice protocol for use (%s)", mode)
await self.client_connect()
async def load_secret_key(self, data): async def load_secret_key(self, data):
log.info("received secret key for voice connection") log.info("received secret key for voice connection")
self._connection.secret_key = data.get("secret_key") self._connection.secret_key = data.get("secret_key")
await self.speak() await self.speak()
await self.speak(False)
async def poll_event(self): async def poll_event(self):
try: try:

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -592,7 +592,7 @@ class Guild(Hashable):
return utils.find(pred, members) return utils.find(pred, members)
def _create_channel(self, name, overwrites, channel_type, category=None, reason=None): def _create_channel(self, name, overwrites, channel_type, category=None, **options):
if overwrites is None: if overwrites is None:
overwrites = {} overwrites = {}
elif not isinstance(overwrites, dict): elif not isinstance(overwrites, dict):
@@ -615,17 +615,24 @@ class Guild(Hashable):
perms.append(payload) perms.append(payload)
try:
options["rate_limit_per_user"] = options.pop("slowmode_delay")
except KeyError:
pass
parent_id = category.id if category else None parent_id = category.id if category else None
return self._state.http.create_channel( return self._state.http.create_channel(
self.id, self.id,
name,
channel_type.value, channel_type.value,
name=name,
parent_id=parent_id, parent_id=parent_id,
permission_overwrites=perms, permission_overwrites=perms,
reason=reason, **options
) )
async def create_text_channel(self, name, *, overwrites=None, category=None, reason=None): async def create_text_channel(
self, name, *, overwrites=None, category=None, reason=None, **options
):
"""|coro| """|coro|
Creates a :class:`TextChannel` for the guild. Creates a :class:`TextChannel` for the guild.
@@ -638,6 +645,12 @@ class Guild(Hashable):
overwrites with the target (either a :class:`Member` or a :class:`Role`) overwrites with the target (either a :class:`Member` or a :class:`Role`)
as the key and a :class:`PermissionOverwrite` as the value. as the key and a :class:`PermissionOverwrite` as the value.
Note
--------
Creating a channel of a specified position will not update the position of
other channels to follow suit. A follow-up call to :meth:`~TextChannel.edit`
will be required to update the position of the channel in the channel list.
Examples Examples
---------- ----------
@@ -660,7 +673,7 @@ class Guild(Hashable):
Parameters Parameters
----------- -----------
name: str name: :class:`str`
The channel's name. The channel's name.
overwrites overwrites
A :class:`dict` of target (either a role or a member) to A :class:`dict` of target (either a role or a member) to
@@ -670,7 +683,17 @@ class Guild(Hashable):
The category to place the newly created channel under. The category to place the newly created channel under.
The permissions will be automatically synced to category if no The permissions will be automatically synced to category if no
overwrites are provided. overwrites are provided.
reason: Optional[str] position: :class:`int`
The position in the channel list. This is a number that starts
at 0. e.g. the top channel is position 0.
topic: Optional[:class:`str`]
The new channel's topic.
slowmode_delay: :class:`int`
Specifies the slowmode rate limit for user in this channel.
The maximum value possible is `120`.
nsfw: :class:`bool`
To mark the channel as NSFW or not.
reason: Optional[:class:`str`]
The reason for creating this channel. Shows up on the audit log. The reason for creating this channel. Shows up on the audit log.
Raises Raises
@@ -688,7 +711,7 @@ class Guild(Hashable):
The channel that was just created. The channel that was just created.
""" """
data = await self._create_channel( data = await self._create_channel(
name, overwrites, ChannelType.text, category, reason=reason name, overwrites, ChannelType.text, category, reason=reason, **options
) )
channel = TextChannel(state=self._state, guild=self, data=data) channel = TextChannel(state=self._state, guild=self, data=data)
@@ -696,13 +719,23 @@ class Guild(Hashable):
self._channels[channel.id] = channel self._channels[channel.id] = channel
return channel return channel
async def create_voice_channel(self, name, *, overwrites=None, category=None, reason=None): async def create_voice_channel(
self, name, *, overwrites=None, category=None, reason=None, **options
):
"""|coro| """|coro|
Same as :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead. This is similar to :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead, in addition
to having the following new parameters.
Parameters
-----------
bitrate: :class:`int`
The channel's preferred audio bitrate in bits per second.
user_limit: :class:`int`
The channel's limit for number of members that can be in a voice channel.
""" """
data = await self._create_channel( data = await self._create_channel(
name, overwrites, ChannelType.voice, category, reason=reason name, overwrites, ChannelType.voice, category, reason=reason, **options
) )
channel = VoiceChannel(state=self._state, guild=self, data=data) channel = VoiceChannel(state=self._state, guild=self, data=data)

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -152,7 +152,7 @@ class HTTPClient:
# wait until the global lock is complete # wait until the global lock is complete
await self._global_over.wait() await self._global_over.wait()
await lock await lock.acquire()
with MaybeUnlock(lock) as maybe_lock: with MaybeUnlock(lock) as maybe_lock:
for tries in range(5): for tries in range(5):
async with self._session.request(method, url, **kwargs) as r: async with self._session.request(method, url, **kwargs) as r:
@@ -596,23 +596,21 @@ class HTTPClient:
r = Route("PATCH", "/guilds/{guild_id}/channels", guild_id=guild_id) r = Route("PATCH", "/guilds/{guild_id}/channels", guild_id=guild_id)
return self.request(r, json=data, reason=reason) return self.request(r, json=data, reason=reason)
def create_channel( def create_channel(self, guild_id, channel_type, *, reason=None, **options):
self, payload = {"type": channel_type}
guild_id,
name,
channel_type,
parent_id=None,
permission_overwrites=None,
*,
reason=None
):
payload = {"name": name, "type": channel_type}
if permission_overwrites is not None: valid_keys = (
payload["permission_overwrites"] = permission_overwrites "name",
"parent_id",
if parent_id is not None: "topic",
payload["parent_id"] = parent_id "bitrate",
"nsfw",
"user_limit",
"position",
"permission_overwrites",
"rate_limit_per_user",
)
payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None})
return self.request( return self.request(
Route("POST", "/guilds/{guild_id}/channels", guild_id=guild_id), Route("POST", "/guilds/{guild_id}/channels", guild_id=guild_id),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -160,7 +160,7 @@ class ReactionIterator(_AsyncIterator):
if data: if data:
self.limit -= retrieve self.limit -= retrieve
self.after = Object(id=int(data[0]["id"])) self.after = Object(id=int(data[-1]["id"]))
if self.guild is None: if self.guild is None:
for element in reversed(data): for element in reversed(data):

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -157,7 +157,7 @@ class Permissions:
- kick_members - kick_members
- ban_members - ban_members
- administrator - administrator
- change_nicknames - change_nickname
- manage_nicknames - manage_nicknames
""" """
return cls(0b00110011111101111111110001010001) return cls(0b00110011111101111111110001010001)
@@ -543,6 +543,10 @@ class PermissionOverwrite:
+-----------+------------------------------------------+ +-----------+------------------------------------------+
| Operation | Description | | Operation | Description |
+===========+==========================================+ +===========+==========================================+
| x == y | Checks if two overwrites are equal. |
+-----------+------------------------------------------+
| x != y | Checks if two overwrites are not equal. |
+-----------+------------------------------------------+
| iter(x) | Returns an iterator of (perm, value) | | iter(x) | Returns an iterator of (perm, value) |
| | pairs. This allows this class to be used | | | pairs. This allows this class to be used |
| | as an iterable in e.g. set/list/dict | | | as an iterable in e.g. set/list/dict |
@@ -566,6 +570,9 @@ class PermissionOverwrite:
setattr(self, key, value) setattr(self, key, value)
def __eq__(self, other):
return self._values == other._values
def _set(self, key, value): def _set(self, key, value):
if value not in (True, None, False): if value not in (True, None, False):
raise TypeError( raise TypeError(

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -27,6 +27,7 @@ DEALINGS IN THE SOFTWARE.
import threading import threading
import subprocess import subprocess
import audioop import audioop
import asyncio
import logging import logging
import shlex import shlex
import time import time
@@ -286,6 +287,7 @@ class AudioPlayer(threading.Thread):
# getattr lookup speed ups # getattr lookup speed ups
play_audio = self.client.send_audio_packet play_audio = self.client.send_audio_packet
self._speak(True)
while not self._end.is_set(): while not self._end.is_set():
# are we paused? # are we paused?
@@ -334,14 +336,19 @@ class AudioPlayer(threading.Thread):
def stop(self): def stop(self):
self._end.set() self._end.set()
self._resumed.set() self._resumed.set()
self._speak(False)
def pause(self): def pause(self, *, update_speaking=True):
self._resumed.clear() self._resumed.clear()
if update_speaking:
self._speak(False)
def resume(self): def resume(self, *, update_speaking=True):
self.loops = 0 self.loops = 0
self._start = time.time() self._start = time.time()
self._resumed.set() self._resumed.set()
if update_speaking:
self._speak(True)
def is_playing(self): def is_playing(self):
return self._resumed.is_set() and not self._end.is_set() return self._resumed.is_set() and not self._end.is_set()
@@ -351,6 +358,12 @@ class AudioPlayer(threading.Thread):
def _set_source(self, source): def _set_source(self, source):
with self._lock: with self._lock:
self.pause() self.pause(update_speaking=False)
self.source = source self.source = source
self.resume() self.resume(update_speaking=False)
def _speak(self, speaking):
try:
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop)
except Exception as e:
log.info("Speaking call in player failed: %s", e)

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2018 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -103,7 +103,7 @@ class RawReactionActionEvent:
message_id: :class:`int` message_id: :class:`int`
The message ID that got or lost a reaction. The message ID that got or lost a reaction.
user_id: :class:`int` user_id: :class:`int`
The user ID who added or removed the reaction. The user ID who added the reaction or whose reaction was removed.
channel_id: :class:`int` channel_id: :class:`int`
The channel ID where the reaction got added or removed. The channel ID where the reaction got added or removed.
guild_id: Optional[:class:`int`] guild_id: Optional[:class:`int`]

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE.
import array import array
import asyncio import asyncio
import unicodedata
from base64 import b64encode from base64 import b64encode
from bisect import bisect_left from bisect import bisect_left
import datetime import datetime
@@ -33,7 +34,7 @@ from email.utils import parsedate_to_datetime
import functools import functools
from inspect import isawaitable as _isawaitable from inspect import isawaitable as _isawaitable
import json import json
from re import split as re_split import re
import warnings import warnings
from .errors import InvalidArgument from .errors import InvalidArgument
@@ -83,7 +84,7 @@ def cached_slot_property(name):
def parse_time(timestamp): def parse_time(timestamp):
if timestamp: if timestamp:
return datetime.datetime(*map(int, re_split(r"[^\d]", timestamp.replace("+00:00", "")))) return datetime.datetime(*map(int, re.split(r"[^\d]", timestamp.replace("+00:00", ""))))
return None return None
@@ -265,9 +266,7 @@ def _get_mime_type_for_image(data):
return "image/png" return "image/png"
elif data.startswith(b"\xFF\xD8") and data.rstrip(b"\0").endswith(b"\xFF\xD9"): elif data.startswith(b"\xFF\xD8") and data.rstrip(b"\0").endswith(b"\xFF\xD9"):
return "image/jpeg" return "image/jpeg"
elif data.startswith(b"\x47\x49\x46\x38\x37\x61") or data.startswith( elif data.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")):
b"\x47\x49\x46\x38\x39\x61"
):
return "image/gif" return "image/gif"
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP": elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "image/webp" return "image/webp"
@@ -351,3 +350,20 @@ class SnowflakeList(array.array):
def has(self, element): def has(self, element):
i = bisect_left(self, element) i = bisect_left(self, element)
return i != len(self) and self[i] == element return i != len(self) and self[i] == element
_IS_ASCII = re.compile(r"^[\x00-\x7f]+$")
def _string_width(string, *, _IS_ASCII=_IS_ASCII):
"""Returns string's width."""
match = _IS_ASCII.match(string)
if match:
return match.endpos
UNICODE_WIDE_CHAR_TYPE = "WFA"
width = 0
func = unicodedata.east_asian_width
for char in string:
width += 2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1
return width

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -105,6 +105,7 @@ class VoiceClient:
self._connected = threading.Event() self._connected = threading.Event()
self._handshake_complete = asyncio.Event(loop=self.loop) self._handshake_complete = asyncio.Event(loop=self.loop)
self.mode = None
self._connections = 0 self._connections = 0
self.sequence = 0 self.sequence = 0
self.timestamp = 0 self.timestamp = 0
@@ -113,6 +114,7 @@ class VoiceClient:
self.encoder = opus.Encoder() self.encoder = opus.Encoder()
warn_nacl = not has_nacl warn_nacl = not has_nacl
supported_modes = ("xsalsa20_poly1305_suffix", "xsalsa20_poly1305")
@property @property
def guild(self): def guild(self):
@@ -305,22 +307,30 @@ class VoiceClient:
def _get_voice_packet(self, data): def _get_voice_packet(self, data):
header = bytearray(12) header = bytearray(12)
nonce = bytearray(24)
box = nacl.secret.SecretBox(bytes(self.secret_key))
# Formulate header # Formulate rtp header
header[0] = 0x80 header[0] = 0x80
header[1] = 0x78 header[1] = 0x78
struct.pack_into(">H", header, 2, self.sequence) struct.pack_into(">H", header, 2, self.sequence)
struct.pack_into(">I", header, 4, self.timestamp) struct.pack_into(">I", header, 4, self.timestamp)
struct.pack_into(">I", header, 8, self.ssrc) struct.pack_into(">I", header, 8, self.ssrc)
# Copy header to nonce's first 12 bytes encrypt_packet = getattr(self, "_encrypt_" + self.mode)
return encrypt_packet(header, data)
def _encrypt_xsalsa20_poly1305(self, header, data):
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = bytearray(24)
nonce[:12] = header nonce[:12] = header
# Encrypt and return the data
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
def _encrypt_xsalsa20_poly1305_suffix(self, header, data):
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
return header + box.encrypt(bytes(data), nonce).ciphertext + nonce
def play(self, source, *, after=None): def play(self, source, *, after=None):
"""Plays an :class:`AudioSource`. """Plays an :class:`AudioSource`.

View File

@@ -3,7 +3,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -103,7 +103,7 @@ class WebhookAdapter:
def store_user(self, data): def store_user(self, data):
# mocks a ConnectionState for appropriate use for Message # mocks a ConnectionState for appropriate use for Message
return BaseUser(state=self, data=data) return BaseUser(state=self.webhook._state, data=data)
def execute_webhook(self, *, payload, wait=False, file=None, files=None): def execute_webhook(self, *, payload, wait=False, file=None, files=None):
if file is not None: if file is not None:
@@ -195,7 +195,7 @@ class AsyncWebhookAdapter(WebhookAdapter):
# transform into Message object # transform into Message object
from .message import Message from .message import Message
return Message(data=data, state=self, channel=self.webhook.channel) return Message(data=data, state=self.webhook._state, channel=self.webhook.channel)
class RequestsWebhookAdapter(WebhookAdapter): class RequestsWebhookAdapter(WebhookAdapter):
@@ -279,7 +279,7 @@ class RequestsWebhookAdapter(WebhookAdapter):
# transform into Message object # transform into Message object
from .message import Message from .message import Message
return Message(data=response, state=self, channel=self.webhook.channel) return Message(data=response, state=self.webhook._state, channel=self.webhook.channel)
class Webhook: class Webhook:

View File

@@ -28,7 +28,7 @@ if [%REF%] == [] (
set REF2=%REF% set REF2=%REF%
) )
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/!REF2!.tar.gz#egg=discord.py pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/!REF2!.tar.gz#egg=discord.py
del /S /Q "discord.py*.egg-info" del /S /Q "discord.py*-info"
for /F %%i in ('dir /S /B discord.py*.egg-info') do rmdir /S /Q %%i for /F %%i in ('dir /S /B discord.py*.egg-info') do rmdir /S /Q %%i
goto reformat goto reformat

View File

@@ -183,7 +183,7 @@ def main():
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True) gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
gathered.cancel() gathered.cancel()
try: try:
red.rpc.server.close() loop.run_until_complete(red.rpc.close())
except AttributeError: except AttributeError:
pass pass

View File

@@ -1,10 +1,8 @@
from pathlib import Path from pathlib import Path
from aiohttp import ClientSession
import shutil
import logging import logging
from .audio import Audio from .audio import Audio
from .manager import start_lavalink_server from .manager import start_lavalink_server, maybe_download_lavalink
from redbot.core import commands from redbot.core import commands
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
import redbot.core import redbot.core
@@ -22,30 +20,6 @@ APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
BUNDLED_APP_YML_FILE = Path(__file__).parent / "data/application.yml" BUNDLED_APP_YML_FILE = Path(__file__).parent / "data/application.yml"
async def download_lavalink(session):
with LAVALINK_JAR_FILE.open(mode="wb") as f:
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
while True:
chunk = await resp.content.read(512)
if not chunk:
break
f.write(chunk)
async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_version())
if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
async with ClientSession(loop=loop) as session:
await download_lavalink(session)
await cog.config.current_version.set(redbot.core.version_info.to_json())
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
async def setup(bot: commands.Bot): async def setup(bot: commands.Bot):
cog = Audio(bot) cog = Audio(bot)
if not await cog.config.use_external_lavalink(): if not await cog.config.use_external_lavalink():

View File

@@ -25,7 +25,7 @@ from redbot.core.utils.menus import (
) )
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from urllib.parse import urlparse from urllib.parse import urlparse
from .manager import shutdown_lavalink_server from .manager import shutdown_lavalink_server, start_lavalink_server, maybe_download_lavalink
_ = Translator("Audio", __file__) _ = Translator("Audio", __file__)
@@ -73,10 +73,29 @@ class Audio(commands.Cog):
self.config.register_global(**default_global) self.config.register_global(**default_global)
self.skip_votes = {} self.skip_votes = {}
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
self._connect_task = None
self._disconnect_task = None self._disconnect_task = None
self._cleaned_up = False self._cleaned_up = False
async def initialize(self): async def initialize(self):
self._restart_connect()
self._disconnect_task = self.bot.loop.create_task(self.disconnect_timer())
lavalink.register_event_listener(self.event_handler)
def _restart_connect(self):
if self._connect_task:
self._connect_task.cancel()
self._connect_task = self.bot.loop.create_task(self.attempt_connect())
async def attempt_connect(self, timeout: int = 30):
while True: # run until success
external = await self.config.use_external_lavalink()
if not external:
shutdown_lavalink_server()
await maybe_download_lavalink(self.bot.loop, self)
await start_lavalink_server(self.bot.loop)
try:
host = await self.config.host() host = await self.config.host()
password = await self.config.password() password = await self.config.password()
rest_port = await self.config.rest_port() rest_port = await self.config.rest_port()
@@ -88,11 +107,13 @@ class Audio(commands.Cog):
password=password, password=password,
rest_port=rest_port, rest_port=rest_port,
ws_port=ws_port, ws_port=ws_port,
timeout=60, timeout=timeout,
) )
lavalink.register_event_listener(self.event_handler) return # break infinite loop
except Exception:
self._disconnect_task = self.bot.loop.create_task(self.disconnect_timer()) if not external:
shutdown_lavalink_server()
await asyncio.sleep(1) # prevent busylooping
async def event_handler(self, player, event_type, extra): async def event_handler(self, player, event_type, extra):
notify = await self.config.guild(player.channel.guild).notify() notify = await self.config.guild(player.channel.guild).notify()
@@ -883,6 +904,10 @@ class Audio(commands.Cog):
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
except AttributeError: except AttributeError:
return await self._embed_msg(ctx, _("Connect to a voice channel first.")) return await self._embed_msg(ctx, _("Connect to a voice channel first."))
except IndexError:
return await self._embed_msg(
ctx, _("Connection to Lavalink has not yet been established.")
)
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author): if not await self._can_instaskip(ctx, ctx.author):
return await self._embed_msg(ctx, _("You need the DJ role to queue tracks.")) return await self._embed_msg(ctx, _("You need the DJ role to queue tracks."))
@@ -1388,9 +1413,15 @@ class Audio(commands.Cog):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
except IndexError:
await self._embed_msg(
ctx, _("Connection to Lavalink has not yet been established.")
)
return False
except AttributeError: except AttributeError:
await self._embed_msg(ctx, _("Connect to a voice channel first.")) await self._embed_msg(ctx, _("Connect to a voice channel first."))
return False return False
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id) player.store("guild", ctx.guild.id)
@@ -1768,6 +1799,10 @@ class Audio(commands.Cog):
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
except AttributeError: except AttributeError:
return await self._embed_msg(ctx, _("Connect to a voice channel first.")) return await self._embed_msg(ctx, _("Connect to a voice channel first."))
except IndexError:
return await self._embed_msg(
ctx, _("Connection to Lavalink has not yet been established.")
)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
shuffle = await self.config.guild(ctx.guild).shuffle() shuffle = await self.config.guild(ctx.guild).shuffle()
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
@@ -1852,6 +1887,10 @@ class Audio(commands.Cog):
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
except AttributeError: except AttributeError:
return await self._embed_msg(ctx, _("Connect to a voice channel first.")) return await self._embed_msg(ctx, _("Connect to a voice channel first."))
except IndexError:
return await self._embed_msg(
ctx, _("Connection to Lavalink has not yet been established.")
)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
jukebox_price = await self.config.guild(ctx.guild).jukebox_price() jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
shuffle = await self.config.guild(ctx.guild).shuffle() shuffle = await self.config.guild(ctx.guild).shuffle()
@@ -1872,7 +1911,6 @@ class Audio(commands.Cog):
except IndexError: except IndexError:
search_choice = tracks[-1] search_choice = tracks[-1]
try: try:
search_check = search_choice.uri
if "localtracks" in search_choice.uri: if "localtracks" in search_choice.uri:
if search_choice.title == "Unknown title": if search_choice.title == "Unknown title":
description = "**{} - {}**\n{}".format( description = "**{} - {}**\n{}".format(
@@ -2308,6 +2346,7 @@ class Audio(commands.Cog):
"""Toggle using external lavalink servers.""" """Toggle using external lavalink servers."""
external = await self.config.use_external_lavalink() external = await self.config.use_external_lavalink()
await self.config.use_external_lavalink.set(not external) await self.config.use_external_lavalink.set(not external)
if external: if external:
await self.config.host.set("localhost") await self.config.host.set("localhost")
await self.config.password.set("youshallnotpass") await self.config.password.set("youshallnotpass")
@@ -2320,13 +2359,15 @@ class Audio(commands.Cog):
), ),
) )
embed.set_footer(text=_("Defaults reset.")) embed.set_footer(text=_("Defaults reset."))
return await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
await self._embed_msg( await self._embed_msg(
ctx, ctx,
_("External lavalink server: {true_or_false}.").format(true_or_false=not external), _("External lavalink server: {true_or_false}.").format(true_or_false=not external),
) )
self._restart_connect()
@llsetup.command() @llsetup.command()
async def host(self, ctx, host): async def host(self, ctx, host):
"""Set the lavalink server host.""" """Set the lavalink server host."""
@@ -2340,6 +2381,8 @@ class Audio(commands.Cog):
else: else:
await self._embed_msg(ctx, _("Host set to {host}.").format(host=host)) await self._embed_msg(ctx, _("Host set to {host}.").format(host=host))
self._restart_connect()
@llsetup.command() @llsetup.command()
async def password(self, ctx, password): async def password(self, ctx, password):
"""Set the lavalink server password.""" """Set the lavalink server password."""
@@ -2356,6 +2399,8 @@ class Audio(commands.Cog):
ctx, _("Server password set to {password}.").format(password=password) ctx, _("Server password set to {password}.").format(password=password)
) )
self._restart_connect()
@llsetup.command() @llsetup.command()
async def restport(self, ctx, rest_port: int): async def restport(self, ctx, rest_port: int):
"""Set the lavalink REST server port.""" """Set the lavalink REST server port."""
@@ -2370,6 +2415,8 @@ class Audio(commands.Cog):
else: else:
await self._embed_msg(ctx, _("REST port set to {port}.").format(port=rest_port)) await self._embed_msg(ctx, _("REST port set to {port}.").format(port=rest_port))
self._restart_connect()
@llsetup.command() @llsetup.command()
async def wsport(self, ctx, ws_port: int): async def wsport(self, ctx, ws_port: int):
"""Set the lavalink websocket server port.""" """Set the lavalink websocket server port."""
@@ -2384,6 +2431,8 @@ class Audio(commands.Cog):
else: else:
await self._embed_msg(ctx, _("Websocket port set to {port}.").format(port=ws_port)) await self._embed_msg(ctx, _("Websocket port set to {port}.").format(port=ws_port))
self._restart_connect()
async def _check_external(self): async def _check_external(self):
external = await self.config.use_external_lavalink() external = await self.config.use_external_lavalink()
if not external: if not external:
@@ -2530,7 +2579,7 @@ class Audio(commands.Cog):
try: try:
query_url = urlparse(url) query_url = urlparse(url)
return all([query_url.scheme, query_url.netloc, query_url.path]) return all([query_url.scheme, query_url.netloc, query_url.path])
except: except Exception:
return False return False
@staticmethod @staticmethod
@@ -2616,11 +2665,33 @@ class Audio(commands.Cog):
def __unload(self): def __unload(self):
if not self._cleaned_up: if not self._cleaned_up:
self.session.detach() self.session.detach()
if self._disconnect_task: if self._disconnect_task:
self._disconnect_task.cancel() self._disconnect_task.cancel()
if self._connect_task:
self._connect_task.cancel()
lavalink.unregister_event_listener(self.event_handler) lavalink.unregister_event_listener(self.event_handler)
self.bot.loop.create_task(lavalink.close()) self.bot.loop.create_task(lavalink.close())
shutdown_lavalink_server() shutdown_lavalink_server()
self._cleaned_up = True self._cleaned_up = True
__del__ = __unload __del__ = __unload
async def on_guild_remove(self, guild: discord.Guild):
"""
This is to clean up players when
the bot either leaves or is removed from a guild
"""
channels = {
x # x is a voice_channel
for y in [g.voice_channels for g in self.bot.guilds]
for x in y # y is a list of voice channels
} # Yes, this is ugly. It's also the most performant and commented.
zombie_players = {p for p in lavalink.player_manager.players if p.channel not in channels}
# Do not unroll to combine with next line.
# Can result in iterator changing size during context switching.
for zombie in zombie_players:
await zombie.destroy()

View File

@@ -8,6 +8,10 @@ import re
from subprocess import Popen, DEVNULL from subprocess import Popen, DEVNULL
from typing import Optional, Tuple from typing import Optional, Tuple
from aiohttp import ClientSession
import redbot.core
_JavaVersion = Tuple[int, int] _JavaVersion = Tuple[int, int]
log = logging.getLogger("red.audio.manager") log = logging.getLogger("red.audio.manager")
@@ -108,6 +112,10 @@ async def start_lavalink_server(loop):
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve()) start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
global proc global proc
if proc and proc.poll() is None:
return # already running
proc = Popen( proc = Popen(
shlex.split(start_cmd, posix=os.name == "posix"), shlex.split(start_cmd, posix=os.name == "posix"),
cwd=str(LAVALINK_DOWNLOAD_DIR), cwd=str(LAVALINK_DOWNLOAD_DIR),
@@ -121,10 +129,39 @@ async def start_lavalink_server(loop):
def shutdown_lavalink_server(): def shutdown_lavalink_server():
log.info("Shutting down lavalink server.") global shutdown
SHUTDOWN.set() shutdown = True
global proc global proc
if proc is not None: if proc is not None:
log.info("Shutting down lavalink server.")
proc.terminate() proc.terminate()
proc.wait() proc.wait()
proc = None proc = None
async def download_lavalink(session):
from . import LAVALINK_DOWNLOAD_URL, LAVALINK_JAR_FILE
with LAVALINK_JAR_FILE.open(mode="wb") as f:
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
while True:
chunk = await resp.content.read(512)
if not chunk:
break
f.write(chunk)
async def maybe_download_lavalink(loop, cog):
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE, BUNDLED_APP_YML_FILE, APP_YML_FILE
jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_version())
if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
async with ClientSession(loop=loop) as session:
await download_lavalink(session)
await cog.config.current_version.set(redbot.core.version_info.to_json())
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))

View File

@@ -194,6 +194,8 @@ class Downloader(commands.Cog):
@checks.is_owner() @checks.is_owner()
async def pipinstall(self, ctx, *deps: str): async def pipinstall(self, ctx, *deps: str):
"""Install a group of dependencies using pip.""" """Install a group of dependencies using pip."""
if not deps:
return await ctx.send_help()
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop) repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
success = await repo.install_raw_requirements(deps, self.LIB_PATH) success = await repo.install_raw_requirements(deps, self.LIB_PATH)

View File

@@ -2,13 +2,15 @@ import asyncio
import functools import functools
import os import os
import pkgutil import pkgutil
import shlex
import shutil import shutil
import re import re
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from subprocess import run as sp_run, PIPE from subprocess import run as sp_run, PIPE
from string import Formatter
from sys import executable from sys import executable
from typing import Tuple, MutableMapping, Union, Optional from typing import List, Tuple, Iterable, MutableMapping, Union, Optional
from redbot.core import data_manager, commands from redbot.core import data_manager, commands
from redbot.core.utils import safe_delete from redbot.core.utils import safe_delete
@@ -22,6 +24,17 @@ from .log import log
_ = Translator("RepoManager", __file__) _ = Translator("RepoManager", __file__)
class ProcessFormatter(Formatter):
def vformat(self, format_string, args, kwargs):
return shlex.split(super().vformat(format_string, args, kwargs))
def get_value(self, key, args, kwargs):
obj = super().get_value(key, args, kwargs)
if isinstance(obj, str) or not isinstance(obj, Iterable):
return shlex.quote(str(obj))
return " ".join(shlex.quote(str(o)) for o in obj)
class Repo(RepoJSONMixin): class Repo(RepoJSONMixin):
GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}" GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}"
GIT_CLONE_NO_BRANCH = "git clone --recurse-submodules {url} {folder}" GIT_CLONE_NO_BRANCH = "git clone --recurse-submodules {url} {folder}"
@@ -94,8 +107,11 @@ class Repo(RepoJSONMixin):
:return: Mapping of filename -> status_letter :return: Mapping of filename -> status_letter
""" """
p = await self._run( p = await self._run(
self.GIT_DIFF_FILE_STATUS.format( ProcessFormatter().format(
path=self.folder_path, old_hash=old_hash, new_hash=new_hash self.GIT_DIFF_FILE_STATUS,
path=self.folder_path,
old_hash=old_hash,
new_hash=new_hash,
) )
) )
@@ -124,7 +140,8 @@ class Repo(RepoJSONMixin):
:return: Git commit note log :return: Git commit note log
""" """
p = await self._run( p = await self._run(
self.GIT_LOG.format( ProcessFormatter().format(
self.GIT_LOG,
path=self.folder_path, path=self.folder_path,
old_hash=old_commit_hash, old_hash=old_commit_hash,
relative_file_path=relative_file_path, relative_file_path=relative_file_path,
@@ -190,13 +207,15 @@ class Repo(RepoJSONMixin):
if self.branch is not None: if self.branch is not None:
p = await self._run( p = await self._run(
self.GIT_CLONE.format( ProcessFormatter().format(
branch=self.branch, url=self.url, folder=self.folder_path self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
).split() )
) )
else: else:
p = await self._run( p = await self._run(
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split() ProcessFormatter().format(
self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
)
) )
if p.returncode: if p.returncode:
@@ -226,7 +245,9 @@ class Repo(RepoJSONMixin):
"A git repo does not exist at path: {}".format(self.folder_path) "A git repo does not exist at path: {}".format(self.folder_path)
) )
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split()) p = await self._run(
ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
)
if p.returncode != 0: if p.returncode != 0:
raise errors.GitException( raise errors.GitException(
@@ -259,7 +280,7 @@ class Repo(RepoJSONMixin):
) )
p = await self._run( p = await self._run(
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split() ProcessFormatter().format(self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch)
) )
if p.returncode != 0: if p.returncode != 0:
@@ -290,7 +311,7 @@ class Repo(RepoJSONMixin):
if folder is None: if folder is None:
folder = self.folder_path folder = self.folder_path
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split()) p = await self._run(ProcessFormatter().format(Repo.GIT_DISCOVER_REMOTE_URL, path=folder))
if p.returncode != 0: if p.returncode != 0:
raise errors.NoRemoteURL("Unable to discover a repo URL.") raise errors.NoRemoteURL("Unable to discover a repo URL.")
@@ -316,7 +337,7 @@ class Repo(RepoJSONMixin):
) )
p = await self._run( p = await self._run(
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split() ProcessFormatter().format(self.GIT_HARD_RESET, path=self.folder_path, branch=branch)
) )
if p.returncode != 0: if p.returncode != 0:
@@ -340,7 +361,7 @@ class Repo(RepoJSONMixin):
await self.hard_reset(branch=curr_branch) await self.hard_reset(branch=curr_branch)
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split()) p = await self._run(ProcessFormatter().format(self.GIT_PULL, path=self.folder_path))
if p.returncode != 0: if p.returncode != 0:
raise errors.UpdateError( raise errors.UpdateError(
@@ -463,9 +484,9 @@ class Repo(RepoJSONMixin):
# TODO: Check and see if any of these modules are already available # TODO: Check and see if any of these modules are already available
p = await self._run( p = await self._run(
self.PIP_INSTALL.format( ProcessFormatter().format(
python=executable, target_dir=target_dir, reqs=" ".join(requirements) self.PIP_INSTALL, python=executable, target_dir=target_dir, reqs=requirements
).split() )
) )
if p.returncode != 0: if p.returncode != 0:
@@ -509,7 +530,7 @@ class Repo(RepoJSONMixin):
class RepoManager: class RepoManager:
GITHUB_OR_GITLAB_RE = re.compile("https?://git(?:hub)|(?:lab)\.com/") GITHUB_OR_GITLAB_RE = re.compile(r"https?://git(?:hub)|(?:lab)\.com/")
TREE_URL_RE = re.compile(r"(?P<tree>/tree)/(?P<branch>\S+)$") TREE_URL_RE = re.compile(r"(?P<tree>/tree)/(?P<branch>\S+)$")
def __init__(self): def __init__(self):

View File

@@ -10,11 +10,14 @@ 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, format_perms_list from redbot.core.utils.chat_formatting import box, escape, format_perms_list
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions from redbot.core.utils.common_filters import (
filter_invites,
filter_various_mentions,
escape_spoilers,
)
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
_ = T_ = Translator("Mod", __file__) _ = T_ = Translator("Mod", __file__)
@@ -1378,11 +1381,18 @@ class Mod(commands.Cog):
joined_at = user.joined_at if not is_special else special_date joined_at = user.joined_at if not is_special else special_date
since_created = (ctx.message.created_at - user.created_at).days since_created = (ctx.message.created_at - user.created_at).days
if joined_at is not None:
since_joined = (ctx.message.created_at - joined_at).days since_joined = (ctx.message.created_at - joined_at).days
user_joined = joined_at.strftime("%d %b %Y %H:%M") user_joined = joined_at.strftime("%d %b %Y %H:%M")
else:
since_joined = "?"
user_joined = "Unknown"
user_created = user.created_at.strftime("%d %b %Y %H:%M") user_created = user.created_at.strftime("%d %b %Y %H:%M")
voice_state = user.voice voice_state = user.voice
member_number = sorted(guild.members, key=lambda m: m.joined_at).index(user) + 1 member_number = (
sorted(guild.members, key=lambda m: m.joined_at or ctx.message.created_at).index(user)
+ 1
)
created_on = _("{}\n({} days ago)").format(user_created, since_created) created_on = _("{}\n({} days ago)").format(user_created, since_created)
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined) joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
@@ -1464,9 +1474,9 @@ class Mod(commands.Cog):
names = await self.settings.user(user).past_names() names = await self.settings.user(user).past_names()
nicks = await self.settings.member(user).past_nicks() nicks = await self.settings.member(user).past_nicks()
if names: if names:
names = [escape(name, mass_mentions=True) for name in names if name] names = [escape_spoilers(escape(name, mass_mentions=True)) for name in names if name]
if nicks: if nicks:
nicks = [escape(nick, mass_mentions=True) for nick in nicks if nick] nicks = [escape_spoilers(escape(nick, mass_mentions=True)) for nick in nicks if nick]
return names, nicks return names, nicks
async def check_tempban_expirations(self): async def check_tempban_expirations(self):

View File

@@ -162,7 +162,8 @@ What car is this? http://i.imgur.com/bCq4ePD.jpg:
- Mclaren P1 - Mclaren P1
- P1 - P1
What car is this? http://i.imgur.com/cOXNsmp.jpg: What car is this? http://i.imgur.com/cOXNsmp.jpg:
- Mitsubishi 3000GT, 3000GT - Mitsubishi 3000GT
- 3000GT
What car is this? http://i.imgur.com/cyLaOzo.jpg: What car is this? http://i.imgur.com/cyLaOzo.jpg:
- Ford GT40 - Ford GT40
- GT40 - GT40

View File

@@ -1,4 +1,5 @@
from collections import namedtuple from collections import namedtuple
from typing import Union, Optional
import discord import discord
import asyncio import asyncio
@@ -306,26 +307,31 @@ class Warnings(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def warnings(self, ctx: commands.Context, userid: int = None): async def warnings(
self, ctx: commands.Context, user: Optional[Union[discord.Member, int]] = None
):
"""List the warnings for the specified user. """List the warnings for the specified user.
Emit `<userid>` to see your own warnings. Omit `<user>` to see your own warnings.
Note that showing warnings for users other than yourself requires Note that showing warnings for users other than yourself requires
appropriate permissions. appropriate permissions.
""" """
if userid is None: if user is None:
user = ctx.author user = ctx.author
else: else:
if not await is_admin_or_superior(self.bot, ctx.author): if not await is_admin_or_superior(self.bot, ctx.author):
await ctx.send( return await ctx.send(
warning(_("You are not allowed to check warnings for other users!")) warning(_("You are not allowed to check warnings for other users!"))
) )
return
else: try:
userid: int = user.id
except AttributeError:
userid: int = user
user = ctx.guild.get_member(userid) user = ctx.guild.get_member(userid)
if user is None: # user not in guild user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
user = namedtuple("Member", "id guild")(userid, ctx.guild)
msg = "" msg = ""
member_settings = self.config.member(user) member_settings = self.config.member(user)
async with member_settings.warnings() as user_warnings: async with member_settings.warnings() as user_warnings:
@@ -356,23 +362,28 @@ class Warnings(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str): async def unwarn(self, ctx: commands.Context, user: Union[discord.Member, int], warn_id: str):
"""Remove a warning from a user.""" """Remove a warning from a user."""
if user_id == ctx.author.id:
await ctx.send(_("You cannot remove warnings from yourself."))
return
guild = ctx.guild
member = guild.get_member(user_id)
if member is None: # no longer in guild, but need a "member" object
member = namedtuple("Member", "guild id")(guild, user_id)
member_settings = self.config.member(member)
guild = ctx.guild
try:
user_id = user.id
member = user
except AttributeError:
user_id = user
member = guild.get_member(user_id)
member = member or namedtuple("Member", "guild id")(guild, user_id)
if user_id == ctx.author.id:
return await ctx.send(_("You cannot remove warnings from yourself."))
member_settings = self.config.member(member)
current_point_count = await member_settings.total_points() current_point_count = await member_settings.total_points()
await warning_points_remove_check(self.config, ctx, member, current_point_count) await warning_points_remove_check(self.config, ctx, member, current_point_count)
async with member_settings.warnings() as user_warnings: async with member_settings.warnings() as user_warnings:
if warn_id not in user_warnings.keys(): if warn_id not in user_warnings.keys():
await ctx.send(_("That warning doesn't exist!")) return await ctx.send(_("That warning doesn't exist!"))
return
else: else:
current_point_count -= user_warnings[warn_id]["points"] current_point_count -= user_warnings[warn_id]["points"]
await member_settings.total_points.set(current_point_count) await member_settings.total_points.set(current_point_count)

View File

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

View File

@@ -5,7 +5,7 @@ replace those from the `discord.ext.commands` module.
""" """
import inspect import inspect
import weakref import weakref
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
import discord import discord
from discord.ext import commands from discord.ext import commands
@@ -50,20 +50,54 @@ class CogCommandMixin:
checks=getattr(decorated, "__requires_checks__", []), checks=getattr(decorated, "__requires_checks__", []),
) )
def allow_for(self, model_id: int, guild_id: int) -> None: def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
"""Actively allow this command for the given model.""" """Actively allow this command for the given model.
Parameters
----------
model_id : Union[int, str]
Must be an `int` if supplying an ID. `str` is only valid
for "default".
guild_id : int
The guild ID to allow this cog or command in. For global
rules, use ``0``.
"""
self.requires.set_rule(model_id, PermState.ACTIVE_ALLOW, guild_id=guild_id) self.requires.set_rule(model_id, PermState.ACTIVE_ALLOW, guild_id=guild_id)
def deny_to(self, model_id: int, guild_id: int) -> None: def deny_to(self, model_id: Union[int, str], guild_id: int) -> None:
"""Actively deny this command to the given model.""" """Actively deny this command to the given model.
Parameters
----------
model_id : Union[int, str]
Must be an `int` if supplying an ID. `str` is only valid
for "default".
guild_id : int
The guild ID to deny this cog or command in. For global
rules, use ``0``.
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id) cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule is PermState.PASSIVE_ALLOW: if cur_rule is PermState.PASSIVE_ALLOW:
self.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id) self.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
else: else:
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id) self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]: def clear_rule_for(
"""Clear the rule which is currently set for this model.""" self, model_id: Union[int, str], guild_id: int
) -> Tuple[PermState, PermState]:
"""Clear the rule which is currently set for this model.
Parameters
----------
model_id : Union[int, str]
Must be an `int` if supplying an ID. `str` is only valid
for "default".
guild_id : int
The guild ID. For global rules, use ``0``.
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id) cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule is PermState.ACTIVE_ALLOW: if cur_rule is PermState.ACTIVE_ALLOW:
new_rule = PermState.NORMAL new_rule = PermState.NORMAL
@@ -84,15 +118,17 @@ class CogCommandMixin:
rule : Optional[bool] rule : Optional[bool]
The rule to set as default. If ``True`` for allow, The rule to set as default. If ``True`` for allow,
``False`` for deny and ``None`` for normal. ``False`` for deny and ``None`` for normal.
guild_id : Optional[int] guild_id : int
Specify to set the default rule for a specific guild. The guild to set the default rule in. When ``0``, this will
When ``None``, this will set the global default rule. set the global default rule.
""" """
if guild_id: if rule is None:
self.requires.set_default_guild_rule(guild_id, PermState.from_bool(rule)) self.clear_rule_for(Requires.DEFAULT, guild_id=guild_id)
else: elif rule is True:
self.requires.default_global_rule = PermState.from_bool(rule) self.allow_for(Requires.DEFAULT, guild_id=guild_id)
elif rule is False:
self.deny_to(Requires.DEFAULT, guild_id=guild_id)
class Command(CogCommandMixin, commands.Command): class Command(CogCommandMixin, commands.Command):
@@ -335,7 +371,7 @@ class Command(CogCommandMixin, commands.Command):
else: else:
return True return True
def allow_for(self, model_id: int, guild_id: int) -> None: def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
super().allow_for(model_id, guild_id=guild_id) super().allow_for(model_id, guild_id=guild_id)
parents = self.parents parents = self.parents
if self.instance is not None: if self.instance is not None:
@@ -347,7 +383,9 @@ class Command(CogCommandMixin, commands.Command):
elif cur_rule is PermState.ACTIVE_DENY: elif cur_rule is PermState.ACTIVE_DENY:
parent.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id) parent.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]: def clear_rule_for(
self, model_id: Union[int, str], guild_id: int
) -> Tuple[PermState, PermState]:
old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id) old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id)
if old_rule is PermState.ACTIVE_ALLOW: if old_rule is PermState.ACTIVE_ALLOW:
parents = self.parents parents = self.parents
@@ -396,8 +434,28 @@ class CogGroupMixin:
all_commands: Dict[str, Command] all_commands: Dict[str, Command]
def reevaluate_rules_for( def reevaluate_rules_for(
self, model_id: int, guild_id: Optional[int] self, model_id: Union[str, int], guild_id: Optional[int]
) -> Tuple[PermState, bool]: ) -> Tuple[PermState, bool]:
"""Re-evaluate a rule by checking subcommand rules.
This is called when a subcommand is no longer actively allowed.
Parameters
----------
model_id : Union[int, str]
Must be an `int` if supplying an ID. `str` is only valid
for "default".
guild_id : int
The guild ID. For global rules, use ``0``.
Returns
-------
Tuple[PermState, bool]
A 2-tuple containing the new rule and a bool indicating
whether or not the rule was changed as a result of this
call.
"""
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id) cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY): if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
# These three states are unaffected by subcommand rules # These three states are unaffected by subcommand rules

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import contextlib
from typing import Iterable, List from typing import Iterable, List
import discord import discord
from discord.ext import commands from discord.ext import commands
@@ -167,6 +168,7 @@ class Context(commands.Context):
timeout=timeout, timeout=timeout,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
with contextlib.suppress(discord.HTTPException):
await query.delete() await query.delete()
break break
else: else:
@@ -176,6 +178,7 @@ class Context(commands.Context):
# In case the bot can't delete other users' messages, # In case the bot can't delete other users' messages,
# or is not a bot account # or is not a bot account
# or channel is a DM # or channel is a DM
with contextlib.suppress(discord.HTTPException):
await query.delete() await query.delete()
return ret return ret

View File

@@ -19,6 +19,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
TypeVar, TypeVar,
Tuple, Tuple,
ClassVar,
) )
import discord import discord
@@ -284,6 +285,14 @@ class Requires:
""" """
DEFAULT: ClassVar[str] = "default"
"""The key for the default rule in a rules dict."""
GLOBAL: ClassVar[int] = 0
"""Should be used in place of a guild ID when setting/getting
global rules.
"""
def __init__( def __init__(
self, self,
privilege_level: Optional[PrivilegeLevel], privilege_level: Optional[PrivilegeLevel],
@@ -307,10 +316,8 @@ class Requires:
self.bot_perms.update(**bot_perms) self.bot_perms.update(**bot_perms)
else: else:
self.bot_perms = bot_perms self.bot_perms = bot_perms
self.default_global_rule: PermState = PermState.NORMAL self._global_rules: _RulesDict = _RulesDict()
self._global_rules: _IntKeyDict[PermState] = _IntKeyDict() self._guild_rules: _IntKeyDict[_RulesDict] = _IntKeyDict[_RulesDict]()
self._default_guild_rules: _IntKeyDict[PermState] = _IntKeyDict()
self._guild_rules: _IntKeyDict[_IntKeyDict[PermState]] = _IntKeyDict()
@staticmethod @staticmethod
def get_decorator( def get_decorator(
@@ -334,16 +341,17 @@ class Requires:
return decorator return decorator
def get_rule(self, model: Union[int, PermissionModel], guild_id: int) -> PermState: def get_rule(self, model: Union[int, str, PermissionModel], guild_id: int) -> PermState:
"""Get the rule for a particular model. """Get the rule for a particular model.
Parameters Parameters
---------- ----------
model : PermissionModel model : Union[int, str, PermissionModel]
The model to get the rule for. The model to get the rule for. `str` is only valid for
`Requires.DEFAULT`.
guild_id : int guild_id : int
The ID of the guild for the rule's scope. Set to ``0`` The ID of the guild for the rule's scope. Set to
for a global rule. `Requires.GLOBAL` for a global rule.
Returns Returns
------- -------
@@ -352,31 +360,32 @@ class Requires:
for an explanation. for an explanation.
""" """
if not isinstance(model, int): if not isinstance(model, (str, int)):
model = model.id model = model.id
if guild_id: if guild_id:
rules = self._guild_rules.get(guild_id, _IntKeyDict()) rules = self._guild_rules.get(guild_id, _RulesDict())
else: else:
rules = self._global_rules rules = self._global_rules
return rules.get(model, PermState.NORMAL) return rules.get(model, PermState.NORMAL)
def set_rule(self, model_id: int, rule: PermState, guild_id: int) -> None: def set_rule(self, model_id: Union[str, int], rule: PermState, guild_id: int) -> None:
"""Set the rule for a particular model. """Set the rule for a particular model.
Parameters Parameters
---------- ----------
model_id : PermissionModel model_id : Union[str, int]
The model to add a rule for. The model to add a rule for. `str` is only valid for
`Requires.DEFAULT`.
rule : PermState rule : PermState
Which state this rule should be set as. See the `PermState` Which state this rule should be set as. See the `PermState`
class for an explanation. class for an explanation.
guild_id : int guild_id : int
The ID of the guild for the rule's scope. Set to ``0`` The ID of the guild for the rule's scope. Set to
for a global rule. `Requires.GLOBAL` for a global rule.
""" """
if guild_id: if guild_id:
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict()) rules = self._guild_rules.setdefault(guild_id, _RulesDict())
else: else:
rules = self._global_rules rules = self._global_rules
if rule is PermState.NORMAL: if rule is PermState.NORMAL:
@@ -387,27 +396,24 @@ class Requires:
def clear_all_rules(self, guild_id: int) -> None: def clear_all_rules(self, guild_id: int) -> None:
"""Clear all rules of a particular scope. """Clear all rules of a particular scope.
This will preserve the default rule, if set.
Parameters Parameters
---------- ----------
guild_id : int guild_id : int
The guild ID to clear rules for. If ``0``, this will The guild ID to clear rules for. If set to
clear all global rules and leave all guild rules `Requires.GLOBAL`, this will clear all global rules and
untouched. leave all guild rules untouched.
""" """
if guild_id: if guild_id:
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict()) rules = self._guild_rules.setdefault(guild_id, _RulesDict())
else: else:
rules = self._global_rules rules = self._global_rules
default = rules.get(self.DEFAULT, None)
rules.clear() rules.clear()
if default is not None:
def get_default_guild_rule(self, guild_id: int) -> PermState: rules[self.DEFAULT] = default
"""Get the default rule for a guild."""
return self._default_guild_rules.get(guild_id, PermState.NORMAL)
def set_default_guild_rule(self, guild_id: int, rule: PermState) -> None:
"""Set the default rule for a guild."""
self._default_guild_rules[guild_id] = rule
async def verify(self, ctx: "Context") -> bool: async def verify(self, ctx: "Context") -> bool:
"""Check if the given context passes the requirements. """Check if the given context passes the requirements.
@@ -470,9 +476,9 @@ class Requires:
# We must check what would happen normally, if no explicit rules were set. # We must check what would happen normally, if no explicit rules were set.
default_rule = PermState.NORMAL default_rule = PermState.NORMAL
if ctx.guild is not None: if ctx.guild is not None:
default_rule = self.get_default_guild_rule(guild_id=ctx.guild.id) default_rule = self.get_rule(self.DEFAULT, guild_id=ctx.guild.id)
if default_rule is PermState.NORMAL: if default_rule is PermState.NORMAL:
default_rule = self.default_global_rule default_rule = self.get_rule(self.DEFAULT, self.GLOBAL)
if default_rule == PermState.ACTIVE_DENY: if default_rule == PermState.ACTIVE_DENY:
would_invoke = False would_invoke = False
@@ -510,7 +516,7 @@ class Requires:
rule = self._global_rules.get(author.id) rule = self._global_rules.get(author.id)
if rule is not None: if rule is not None:
return rule return rule
return self.default_global_rule return self.get_rule(self.DEFAULT, self.GLOBAL)
rules_chain = [self._global_rules] rules_chain = [self._global_rules]
guild_rules = self._guild_rules.get(ctx.guild.id) guild_rules = self._guild_rules.get(ctx.guild.id)
@@ -534,9 +540,9 @@ class Requires:
return rule return rule
del model_chain[-1] # We don't check for the guild in guild rules del model_chain[-1] # We don't check for the guild in guild rules
default_rule = self.get_default_guild_rule(guild.id) default_rule = self.get_rule(self.DEFAULT, guild.id)
if default_rule is PermState.NORMAL: if default_rule is PermState.NORMAL:
default_rule = self.default_global_rule default_rule = self.get_rule(self.DEFAULT, self.GLOBAL)
return default_rule return default_rule
async def _verify_checks(self, ctx: "Context") -> bool: async def _verify_checks(self, ctx: "Context") -> bool:
@@ -706,6 +712,20 @@ class _IntKeyDict(Dict[int, _T]):
return super().__setitem__(key, value) return super().__setitem__(key, value)
class _RulesDict(Dict[Union[int, str], PermState]):
"""Dict subclass which throws a KeyError when an invalid key is used."""
def __getitem__(self, key: Any) -> PermState:
if key != Requires.DEFAULT and not isinstance(key, int):
raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"')
return super().__getitem__(key)
def __setitem__(self, key: Any, value: PermState) -> None:
if key != Requires.DEFAULT and not isinstance(key, int):
raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"')
return super().__setitem__(key, value)
def _validate_perms_dict(perms: Dict[str, bool]) -> None: def _validate_perms_dict(perms: Dict[str, bool]) -> None:
for perm, value in perms.items(): for perm, value in perms.items():
try: try:

View File

@@ -489,7 +489,10 @@ class Core(commands.Cog, CoreLogic):
try: try:
await self.bot.wait_for("message", check=pred, timeout=15) await self.bot.wait_for("message", check=pred, timeout=15)
except asyncio.TimeoutError: except asyncio.TimeoutError:
try:
await query.delete() await query.delete()
except discord.errors.NotFound:
pass
else: else:
await self.leave_confirmation(guilds[pred.result], ctx) await self.leave_confirmation(guilds[pred.result], ctx)
@@ -515,6 +518,8 @@ class Core(commands.Cog, CoreLogic):
@checks.is_owner() @checks.is_owner()
async def load(self, ctx: commands.Context, *cogs: str): async def load(self, ctx: commands.Context, *cogs: str):
"""Loads packages""" """Loads packages"""
if not cogs:
return await ctx.send_help()
async with ctx.typing(): async with ctx.typing():
loaded, failed, not_found, already_loaded = await self._load(cogs) loaded, failed, not_found, already_loaded = await self._load(cogs)
@@ -545,6 +550,8 @@ class Core(commands.Cog, CoreLogic):
@checks.is_owner() @checks.is_owner()
async def unload(self, ctx: commands.Context, *cogs: str): async def unload(self, ctx: commands.Context, *cogs: str):
"""Unloads packages""" """Unloads packages"""
if not cogs:
return await ctx.send_help()
unloaded, failed = await self._unload(cogs) unloaded, failed = await self._unload(cogs)
if unloaded: if unloaded:
@@ -561,6 +568,8 @@ class Core(commands.Cog, CoreLogic):
@checks.is_owner() @checks.is_owner()
async def reload(self, ctx: commands.Context, *cogs: str): async def reload(self, ctx: commands.Context, *cogs: str):
"""Reloads packages""" """Reloads packages"""
if not cogs:
return await ctx.send_help()
async with ctx.typing(): async with ctx.typing():
loaded, failed, not_found, already_loaded = await self._reload(cogs) loaded, failed, not_found, already_loaded = await self._reload(cogs)
@@ -1078,7 +1087,7 @@ class Core(commands.Cog, CoreLogic):
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def backup(self, ctx: commands.Context, 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
@@ -1321,7 +1330,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: commands.Context, user: discord.User): async def whitelist_remove(self, ctx: commands.Context, *, user: discord.User):
""" """
Removes user from whitelist. Removes user from whitelist.
""" """
@@ -1354,7 +1363,7 @@ class Core(commands.Cog, CoreLogic):
pass pass
@blacklist.command(name="add") @blacklist.command(name="add")
async def blacklist_add(self, ctx: commands.Context, 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.
""" """
@@ -1383,7 +1392,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: commands.Context, user: discord.User): async def blacklist_remove(self, ctx: commands.Context, *, user: discord.User):
""" """
Removes user from blacklist. Removes user from blacklist.
""" """
@@ -1417,17 +1426,13 @@ class Core(commands.Cog, CoreLogic):
pass pass
@localwhitelist.command(name="add") @localwhitelist.command(name="add")
async def localwhitelist_add(self, ctx: commands.Context, *, user_or_role: str): async def localwhitelist_add(
self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
):
""" """
Adds a user or role to the whitelist. Adds a user or role to the whitelist.
""" """
try: user = isinstance(user_or_role, discord.Member)
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list: async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
if obj.id not in curr_list: if obj.id not in curr_list:
curr_list.append(obj.id) curr_list.append(obj.id)
@@ -1452,17 +1457,13 @@ 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: commands.Context, *, user_or_role: str): async def localwhitelist_remove(
self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
):
""" """
Removes user or role from whitelist. Removes user or role from whitelist.
""" """
try: user = isinstance(user_or_role, discord.Member)
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
removed = False removed = False
async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list: async with ctx.bot.db.guild(ctx.guild).whitelist() as curr_list:
@@ -1499,17 +1500,13 @@ class Core(commands.Cog, CoreLogic):
pass pass
@localblacklist.command(name="add") @localblacklist.command(name="add")
async def localblacklist_add(self, ctx: commands.Context, *, user_or_role: str): async def localblacklist_add(
self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
):
""" """
Adds a user or role to the blacklist. Adds a user or role to the blacklist.
""" """
try: user = isinstance(user_or_role, discord.Member)
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
if user and await ctx.bot.is_owner(obj): if user and await ctx.bot.is_owner(obj):
await ctx.send(_("You cannot blacklist an owner!")) await ctx.send(_("You cannot blacklist an owner!"))
@@ -1539,18 +1536,14 @@ 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: commands.Context, *, user_or_role: str): async def localblacklist_remove(
self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
):
""" """
Removes user or role from blacklist. Removes user or role from blacklist.
""" """
removed = False removed = False
try: user = isinstance(user_or_role, discord.Member)
obj = await commands.MemberConverter().convert(ctx, user_or_role)
except commands.BadArgument:
obj = await commands.RoleConverter().convert(ctx, user_or_role)
user = False
else:
user = True
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list: async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
if obj.id in curr_list: if obj.id in curr_list:
@@ -1747,7 +1740,7 @@ class Core(commands.Cog, CoreLogic):
@autoimmune_group.command(name="add") @autoimmune_group.command(name="add")
async def autoimmune_add( async def autoimmune_add(
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
): ):
""" """
Makes a user or roles immune from automated moderation actions Makes a user or roles immune from automated moderation actions
@@ -1760,7 +1753,7 @@ class Core(commands.Cog, CoreLogic):
@autoimmune_group.command(name="remove") @autoimmune_group.command(name="remove")
async def autoimmune_remove( async def autoimmune_remove(
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
): ):
""" """
Makes a user or roles immune from automated moderation actions Makes a user or roles immune from automated moderation actions
@@ -1773,7 +1766,7 @@ class Core(commands.Cog, CoreLogic):
@autoimmune_group.command(name="isimmune") @autoimmune_group.command(name="isimmune")
async def autoimmune_checkimmune( async def autoimmune_checkimmune(
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] self, ctx: commands.Context, *, user_or_role: Union[discord.Member, discord.Role]
): ):
""" """
Checks if a user or role would be considered immune from automated actions Checks if a user or role would be considered immune from automated actions

View File

@@ -144,7 +144,7 @@ def init_events(bot, cli_flags):
sentry = await bot.db.enable_sentry() sentry = await bot.db.enable_sentry()
mongo_enabled = storage_type() != "JSON" mongo_enabled = storage_type() != "JSON"
reqs_installed = {"voice": None, "docs": None, "test": None} reqs_installed = {"docs": None, "test": None}
for key in reqs_installed.keys(): for key in reqs_installed.keys():
reqs = [x.name for x in red_pkg._dep_map[key]] reqs = [x.name for x in red_pkg._dep_map[key]]
try: try:
@@ -157,7 +157,7 @@ def init_events(bot, cli_flags):
options = ( options = (
("Error Reporting", sentry), ("Error Reporting", sentry),
("MongoDB", mongo_enabled), ("MongoDB", mongo_enabled),
("Voice", reqs_installed["voice"]), ("Voice", True),
("Docs", reqs_installed["docs"]), ("Docs", reqs_installed["docs"]),
("Tests", reqs_installed["test"]), ("Tests", reqs_installed["test"]),
) )
@@ -176,12 +176,6 @@ def init_events(bot, cli_flags):
print("\nInvite URL: {}\n".format(invite_url)) print("\nInvite URL: {}\n".format(invite_url))
bot.color = discord.Colour(await bot.db.color()) bot.color = discord.Colour(await bot.db.color())
try:
import Levenshtein
except ImportError:
log.info(
"python-Levenshtein is not installed, fuzzy string matching will be a bit slower."
)
@bot.event @bot.event
async def on_error(event_method, *args, **kwargs): async def on_error(event_method, *args, **kwargs):
@@ -204,19 +198,17 @@ def init_events(bot, cli_flags):
await ctx.send(disabled_message.replace("{command}", ctx.invoked_with)) await ctx.send(disabled_message.replace("{command}", ctx.invoked_with))
elif isinstance(error, commands.CommandInvokeError): elif isinstance(error, commands.CommandInvokeError):
log.exception( log.exception(
"Exception in command '{}'" "".format(ctx.command.qualified_name), "Exception in command '{}'".format(ctx.command.qualified_name),
exc_info=error.original, exc_info=error.original,
) )
if should_log_sentry(error): if should_log_sentry(error):
sentry_log.exception( sentry_log.exception(
"Exception in command '{}'" "".format(ctx.command.qualified_name), "Exception in command '{}'".format(ctx.command.qualified_name),
exc_info=error.original, exc_info=error.original,
) )
message = ( message = "Error in command '{}'. Check your console or logs for details.".format(
"Error in command '{}'. Check your console or " ctx.command.qualified_name
"logs for details."
"".format(ctx.command.qualified_name)
) )
exception_log = "Exception in command '{}'\n" "".format(ctx.command.qualified_name) exception_log = "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
exception_log += "".join( exception_log += "".join(
@@ -273,9 +265,9 @@ def init_events(bot, cli_flags):
system_now = datetime.datetime.utcnow() system_now = datetime.datetime.utcnow()
diff = abs((discord_now - system_now).total_seconds()) diff = abs((discord_now - system_now).total_seconds())
if diff > 60: if diff > 60:
log.warn( log.warning(
"Detected significant difference (%d seconds) in system clock to discord's clock." "Detected significant difference (%d seconds) in system clock to discord's "
" Any time sensitive code may fail.", "clock. Any time sensitive code may fail.",
diff, diff,
) )
bot.checked_time_accuracy = discord_now bot.checked_time_accuracy = discord_now

View File

@@ -115,7 +115,7 @@ class Help(dpy_formatter.HelpFormatter):
def get_ending_note(self): def get_ending_note(self):
# command_name = self.context.invoked_with # command_name = self.context.invoked_with
return ( return (
"Type {0}help <command> for more info on a command.\n" "Type {0}help <command> for more info on a command. "
"You can also type {0}help <category> for more info on a category.".format( "You can also type {0}help <category> for more info on a category.".format(
self.context.clean_prefix self.context.clean_prefix
) )

View File

@@ -7,7 +7,12 @@ import discord
from redbot.core import Config from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
from .utils.common_filters import filter_invites, filter_mass_mentions, filter_urls from .utils.common_filters import (
filter_invites,
filter_mass_mentions,
filter_urls,
escape_spoilers,
)
__all__ = [ __all__ = [
"Case", "Case",
@@ -115,9 +120,11 @@ class Case:
reason = "**Reason:** Use `[p]reason {} <reason>` to add it".format(self.case_number) reason = "**Reason:** Use `[p]reason {} <reason>` to add it".format(self.case_number)
if self.moderator is not None: if self.moderator is not None:
moderator = "{}#{} ({})\n".format( moderator = escape_spoilers(
"{}#{} ({})\n".format(
self.moderator.name, self.moderator.discriminator, self.moderator.id self.moderator.name, self.moderator.discriminator, self.moderator.id
) )
)
else: else:
moderator = "Unknown" moderator = "Unknown"
until = None until = None
@@ -133,9 +140,11 @@ class Case:
amended_by = None amended_by = None
if self.amended_by: if self.amended_by:
amended_by = "{}#{} ({})".format( amended_by = escape_spoilers(
"{}#{} ({})".format(
self.amended_by.name, self.amended_by.discriminator, self.amended_by.id self.amended_by.name, self.amended_by.discriminator, self.amended_by.id
) )
)
last_modified = None last_modified = None
if self.modified_at: if self.modified_at:
@@ -143,9 +152,11 @@ class Case:
datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S") datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S")
) )
user = filter_invites( user = escape_spoilers(
filter_invites(
"{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id) "{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
) # Invites get rendered even in embeds. )
) # Invites and spoilers get rendered even in embeds.
if embed: if embed:
emb = discord.Embed(title=title, description=reason) emb = discord.Embed(title=title, description=reason)

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from typing import Optional
from aiohttp import web from aiohttp import web
from aiohttp_json_rpc import JsonRpc from aiohttp_json_rpc import JsonRpc
@@ -63,25 +64,26 @@ class RPC:
def __init__(self): def __init__(self):
self.app = web.Application() self.app = web.Application()
self._rpc = RedRpc() self._rpc = RedRpc()
self.app.router.add_route("*", "/", self._rpc) self.app.router.add_route("*", "/", self._rpc.handle_request)
self.app_handler = self.app.make_handler() self._runner = web.AppRunner(self.app)
self._site: Optional[web.TCPSite] = None
self.server = None
async def initialize(self): async def initialize(self):
""" """
Finalizes the initialization of the RPC server and allows it to begin Finalizes the initialization of the RPC server and allows it to begin
accepting queries. accepting queries.
""" """
self.server = await self.app.loop.create_server(self.app_handler, "127.0.0.1", 6133) await self._runner.setup()
self._site = web.TCPSite(self._runner, host="127.0.0.1", port=6133)
await self._site.start()
log.debug("Created RPC server listener.") log.debug("Created RPC server listener.")
def close(self): async def close(self):
""" """
Closes the RPC server. Closes the RPC server.
""" """
self.server.close() await self._runner.cleanup()
def add_method(self, method, prefix: str = None): def add_method(self, method, prefix: str = None):
if prefix is None: if prefix is None:
@@ -117,7 +119,8 @@ class RPCMixin:
""" """
Registers a method to act as an RPC handler if the internal RPC server is active. Registers a method to act as an RPC handler if the internal RPC server is active.
When calling this method through the RPC server, use the naming scheme "cogname__methodname". When calling this method through the RPC server, use the naming scheme
"cogname__methodname".
.. important:: .. important::

View File

@@ -9,6 +9,8 @@ __all__ = [
"filter_mass_mentions", "filter_mass_mentions",
"filter_various_mentions", "filter_various_mentions",
"normalize_smartquotes", "normalize_smartquotes",
"escape_spoilers",
"escape_spoilers_and_mass_mentions",
] ]
# regexes # regexes
@@ -29,6 +31,10 @@ SMART_QUOTE_REPLACEMENT_DICT = {
SMART_QUOTE_REPLACE_RE = re.compile("|".join(SMART_QUOTE_REPLACEMENT_DICT.keys())) SMART_QUOTE_REPLACE_RE = re.compile("|".join(SMART_QUOTE_REPLACEMENT_DICT.keys()))
SPOILER_CONTENT_RE = re.compile(
r"(?s)(?<!\\)(?P<OPEN>\|{2})(?P<SPOILERED>.*?)(?<!\\)(?P<CLOSE>\|{2})"
)
# convenience wrappers # convenience wrappers
def filter_urls(to_filter: str) -> str: def filter_urls(to_filter: str) -> str:
@@ -133,3 +139,37 @@ def normalize_smartquotes(to_normalize: str) -> str:
return SMART_QUOTE_REPLACEMENT_DICT.get(obj.group(0), "") return SMART_QUOTE_REPLACEMENT_DICT.get(obj.group(0), "")
return SMART_QUOTE_REPLACE_RE.sub(replacement_for, to_normalize) return SMART_QUOTE_REPLACE_RE.sub(replacement_for, to_normalize)
def escape_spoilers(content: str) -> str:
"""
Get a string with spoiler syntax escaped.
Parameters
----------
content : str
The string to escape.
Returns
-------
str
The escaped string.
"""
return SPOILER_CONTENT_RE.sub(r"\\\g<OPEN>\g<SPOILERED>\\\g<CLOSE>", content)
def escape_spoilers_and_mass_mentions(content: str) -> str:
"""
Get a string with spoiler syntax and mass mentions escaped
Parameters
----------
content : str
The string to escape.
Returns
-------
str
The escaped string.
"""
return escape_spoilers(filter_mass_mentions(content))

View File

@@ -744,7 +744,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
if not same_context(m): if not same_context(m):
return False return False
try: try:
self.result = collection.index(m.content) self.result = collection.index(m.content.lower())
except ValueError: except ValueError:
return False return False
else: else:

View File

@@ -452,6 +452,7 @@ def main_menu():
def main(): def main():
args, flags_to_pass = parse_cli_args()
if not PYTHON_OK: if not PYTHON_OK:
print( print(
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you " f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you "
@@ -478,8 +479,6 @@ def main():
run_red(args.instancename, autorestart=args.auto_restart, cliflags=flags_to_pass) run_red(args.instancename, autorestart=args.auto_restart, cliflags=flags_to_pass)
args, flags_to_pass = parse_cli_args()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() main()

View File

@@ -371,6 +371,7 @@ async def remove_instance_interaction():
def main(): def main():
args, _ = parse_cli_args()
if args.delete: if args.delete:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(remove_instance_interaction()) loop.run_until_complete(remove_instance_interaction())
@@ -381,8 +382,6 @@ def main():
basic_setup() basic_setup()
args, _ = parse_cli_args()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() main()

View File

@@ -1,40 +1,37 @@
import distutils.ccompiler as ccompiler
import os import os
import re import re
import tempfile
from distutils.errors import CCompilerError, DistutilsPlatformError
from setuptools import setup, find_packages from setuptools import setup, find_packages
install_requires = [ install_requires = [
"aiohttp-json-rpc==0.11.2", "aiohttp-json-rpc==0.12",
"aiohttp==3.4.4", "aiohttp==3.5.4",
"appdirs==1.4.3", "appdirs==1.4.3",
"async-timeout==3.0.1", "async-timeout==3.0.1",
"attrs==18.2.0", "attrs==18.2.0",
"chardet==3.0.4", "chardet==3.0.4",
"colorama==0.4.1", "colorama==0.4.1",
"distro==1.3.0; sys_platform == 'linux'", "distro==1.4.0; sys_platform == 'linux'",
"fuzzywuzzy==0.17.0", "fuzzywuzzy==0.17.0",
"idna-ssl==1.1.0", "idna-ssl==1.1.0",
"idna==2.8", "idna==2.8",
"multidict==4.5.2", "multidict==4.5.2",
"python-levenshtein==0.12.0", "python-levenshtein-wheels==0.13.1",
"pyyaml==3.13", "pyyaml==3.13",
"raven==6.10.0", "raven==6.10.0",
"raven-aiohttp==0.7.0", "raven-aiohttp==0.7.0",
"red-lavalink==0.2.0", "red-lavalink==0.2.3",
"schema==0.6.8", "schema==0.6.8",
"websockets==6.0", "websockets==7.0",
"yarl==1.3.0", "yarl==1.3.0",
] ]
extras_require = { extras_require = {
"test": [ "test": [
"atomicwrites==1.2.1", "atomicwrites==1.3.0",
"more-itertools==5.0.0", "more-itertools==6.0.0",
"pluggy==0.8.1", "pluggy==0.8.1",
"py==1.7.0", "py==1.7.0",
"pytest==4.1.0", "pytest==4.2.0",
"pytest-asyncio==0.10.0", "pytest-asyncio==0.10.0",
"six==1.12.0", "six==1.12.0",
], ],
@@ -47,15 +44,15 @@ extras_require = {
"imagesize==1.1.0", "imagesize==1.1.0",
"Jinja2==2.10", "Jinja2==2.10",
"MarkupSafe==1.1.0", "MarkupSafe==1.1.0",
"packaging==18.0", "packaging==19.0",
"pyparsing==2.3.0", "pyparsing==2.3.1",
"Pygments==2.3.1", "Pygments==2.3.1",
"pytz==2018.9", "pytz==2018.9",
"requests==2.21.0", "requests==2.21.0",
"six==1.12.0", "six==1.12.0",
"snowballstemmer==1.2.1", "snowballstemmer==1.2.1",
"sphinx==1.8.3", "sphinx==1.8.4",
"sphinx_rtd_theme==0.4.2", "sphinx_rtd_theme==0.4.3",
"sphinxcontrib-asyncio==0.2.0", "sphinxcontrib-asyncio==0.2.0",
"sphinxcontrib-websupport==1.1.0", "sphinxcontrib-websupport==1.1.0",
"urllib3==1.24.1", "urllib3==1.24.1",
@@ -70,19 +67,6 @@ if os.name == "nt":
python_requires = ">=3.6.6,<3.8" python_requires = ">=3.6.6,<3.8"
def check_compiler_available():
m = ccompiler.new_compiler()
with tempfile.TemporaryDirectory() as tdir:
with open(os.path.join(tdir, "dummy.c"), "w") as tfile:
tfile.write("int main(int argc, char** argv) {return 0;}")
try:
m.compile([tfile.name], output_dir=tdir)
except (CCompilerError, DistutilsPlatformError):
return False
return True
def get_version(): def get_version():
with open("redbot/core/__init__.py") as f: with open("redbot/core/__init__.py") as f:
version = re.search( version = re.search(
@@ -92,11 +76,6 @@ def get_version():
if __name__ == "__main__": if __name__ == "__main__":
if not check_compiler_available():
install_requires.remove(
next(r for r in install_requires if r.lower().startswith("python-levenshtein"))
)
setup( setup(
name="Red-DiscordBot", name="Red-DiscordBot",
version=get_version(), version=get_version(),