diff options
author | 2019-03-07 17:06:05 +1000 | |
---|---|---|
committer | 2019-03-07 17:06:05 +1000 | |
commit | 4555d90b60eedbb6a3032512277f307d69034f6f (patch) | |
tree | f00244b5dc91369a418ac7b52512f7d554216c4c | |
parent | Update config-default.yml (diff) | |
parent | Fix linting issues (diff) |
Merge pull request #328 from python-discord/remove-snake-cog
Remove the "Snakes" cog
24 files changed, 234 insertions, 2385 deletions
@@ -14,7 +14,6 @@ markdownify = "*" lxml = "*" pyyaml = "*" fuzzywuzzy = "*" -pillow = "*" aio-pika = "*" python-dateutil = "*" deepdiff = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 92566c3ed..20900c918 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d53f89c6d3b32ccbc2dadaff1a7e9ee1bdcbd1df9cddab35def36bcceec98b27" + "sha256": "0cda63882f4a5ad3d210a90e76bde86fb18c32f2231023496c532a78b2a2c0cb" }, "pipfile-spec": 6, "requires": { @@ -18,19 +18,19 @@ "default": { "aio-pika": { "hashes": [ - "sha256:c3eb639f7fc5c96355e7a227380989c9e0f342bb6612e6671ea76d188813ba45", - "sha256:ea26efd262d7c4cd4ac00fb968ede89e82c00ad331b47415e3c2353a4b91cbe0" + "sha256:22e722c7202d85abfbfbe9a9ee96fdf064d621489f45d4fd383306de5d765e12", + "sha256:308b439822648c61634117d900678d71ef4a7afea7ce5b921826ce07b7f60290" ], "index": "pypi", - "version": "==4.9.1" + "version": "==5.3.2" }, "aiodns": { "hashes": [ - "sha256:99d0652f2c02f73bfa646bf44af82705260a523014576647d7959e664830b26b", - "sha256:d8677adc679ce8d0ef706c14d9c3d2f27a0e0cc11d59730cdbaf218ad52dd9ea" + "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d", + "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de" ], "index": "pypi", - "version": "==1.1.1" + "version": "==2.0.0" }, "aiohttp": { "hashes": [ @@ -60,6 +60,13 @@ "index": "pypi", "version": "==3.4.4" }, + "aiormq": { + "hashes": [ + "sha256:40832b67a96b63d0dc70292daf094ec5a0cab8649120333543bd07ccafc2b502", + "sha256:7d0146ea803699a5d4b7c1459c1065d110e52afcb785b2b9e70591a7af9ddbb2" + ], + "version": "==2.0.1" + }, "alabaster": { "hashes": [ "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", @@ -76,10 +83,10 @@ }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==18.2.0" + "version": "==19.1.0" }, "babel": { "hashes": [ @@ -90,11 +97,11 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:1ed70a0e99742653953d68462378a1a8eb65dca5f7c8fa44a05a2a0b3545df67", - "sha256:6a7f5e0efc563cd1ffeefba6d528b97aa0d313c02dd126ba6c455e5fe5bd48eb", - "sha256:e394827904cc4923f443e8dd2e9968343669c8e1ad7a8d62d7541e780884acb8" + "sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858", + "sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348", + "sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718" ], - "version": "==4.7.0" + "version": "==4.7.1" }, "certifi": { "hashes": [ @@ -105,40 +112,36 @@ }, "cffi": { "hashes": [ - "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", - "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", - "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", - "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", - "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", - "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", - "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", - "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", - "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", - "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", - "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", - "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", - "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", - "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", - "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", - "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", - "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", - "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", - "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", - "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", - "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", - "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", - "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", - "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", - "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", - "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", - "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", - "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", - "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", - "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", - "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", - "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" - ], - "version": "==1.11.5" + "sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f", + "sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11", + "sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d", + "sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891", + "sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf", + "sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c", + "sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed", + "sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b", + "sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a", + "sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585", + "sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea", + "sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f", + "sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33", + "sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145", + "sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a", + "sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3", + "sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f", + "sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd", + "sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804", + "sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d", + "sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92", + "sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f", + "sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84", + "sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb", + "sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7", + "sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7", + "sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35", + "sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889" + ], + "version": "==1.12.2" }, "chardet": { "hashes": [ @@ -174,10 +177,10 @@ }, "dulwich": { "hashes": [ - "sha256:5e1e39555f594939a8aff1ca08b3bdf6c7efd4b941c2850760983a0197240974" + "sha256:afbe070f6899357e33f63f3f3696e601731fef66c64a489dea1bc9f539f4a725" ], "index": "pypi", - "version": "==0.19.9" + "version": "==0.19.11" }, "fuzzywuzzy": { "hashes": [ @@ -194,6 +197,13 @@ ], "version": "==2.8" }, + "idna-ssl": { + "hashes": [ + "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "markers": "python_version < '3.7'", + "version": "==1.1.0" + }, "imagesize": { "hashes": [ "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", @@ -210,11 +220,10 @@ }, "jsonpickle": { "hashes": [ - "sha256:8b6212f1155f43ce67fa945efae6d010ed059f3ca5ed377aa070e5903d45b722", - "sha256:d43ede55b3d9b5524a8e11566ea0b11c9c8109116ef6a509a1b619d2041e7397", - "sha256:ed4adf0d14564c56023862eabfac211cf01211a20c5271896c8ab6f80c68086c" + "sha256:0231d6f7ebc4723169310141352d9c9b7bbbd6f3be110cf634575d2bf2af91f0", + "sha256:625098cc8e5854b8c23b587aec33bc8e33e0e597636bfaca76152249c78fe5c1" ], - "version": "==1.0" + "version": "==1.1" }, "logmatic-python": { "hashes": [ @@ -225,39 +234,35 @@ }, "lxml": { "hashes": [ - "sha256:16cf8bac33ec17049617186d63006ba49da7c5be417042877a49f0ef6d7a195d", - "sha256:18f2d8f14cc61e66e8a45f740d15b6fc683c096f733db1f8d0ee15bcac9843de", - "sha256:260868f69d14a64dd1de9cf92e133d2f71514d288de4906f109bdf48ca9b756a", - "sha256:29b8acd8ecdf772266dbac491f203c71664b0b07ad4309ba2c3bb131306332fc", - "sha256:2b05e5e06f8e8c63595472dc887d0d6e0250af754a35ba690f6a6abf2ef85691", - "sha256:30d6ec05fb607a5b7345549f642c7c7a5b747b634f6d5e935596b910f243f96f", - "sha256:3bf683f0237449ebc1851098f664410e3c99ba3faa8c9cc82c6acfe857df1767", - "sha256:3ce5488121eb15513c4b239dadd67f9e7959511bd766aac6be0c35e80274f298", - "sha256:48be0c375350a5519bb9474b42a9c0e7ab709fb45f11bfcd33de876791137896", - "sha256:49bc343ca3b30cd860845433bb9f62448a54ff87b632175108bacbc5dc63e49e", - "sha256:4cc7531e86a43ea66601763c5914c3d3adb297f32e4284957609b90d41825fca", - "sha256:4e9822fad564d82035f0b6d701a890444560210f8a8648b8f15850f8fe883cd9", - "sha256:51a9a441aefc8c93512bad5efe867d2ff086e7249ce0fc3b47c310644b352936", - "sha256:5bbed9efc8aeb69929140f71a30e655bf496b45b766861513960e1b11168d475", - "sha256:60a5323b2bc893ca1059d283d6695a172d51cc95a70c25b3e587e1aad5459c38", - "sha256:7035d9361f3ceec9ccc1dd3482094d1174580e7e1bf6870b77ea758f7cad15d2", - "sha256:76d62cc048bda0ebf476689ad3eb8e65e6827e43a7521be3b163071020667b8c", - "sha256:78163b578e6d1836012febaa1865e095ccc7fc826964dd69a2dbfe401618a1f7", - "sha256:83b58b2b5904d50de03a47e2f56d24e9da4cf7e3b0d66fb4510b18fca0faf910", - "sha256:a07447e46fffa5bb4d7a0af0a6505c8517e9bd197cfd2aec79e499b6e86cde49", - "sha256:a17d808b3edca4aaf6b295b5a388c844a0b7f79aca2d79eec5acc1461db739e3", - "sha256:a378fd61022cf4d3b492134c3bc48204ac2ff19e0813b23e07c3dd95ae8df0bc", - "sha256:aa7d096a44ae3d475c5ed763e24cf302d32462e78b61bba73ce1ad0efb8f522a", - "sha256:ade8785c93a985956ba6499d5ea6d0a362e24b4a9ba07dd18920fd67cccf63ea", - "sha256:cc039668f91d8af8c4094cfb5a67c7ae733967fdc84c0507fe271db81480d367", - "sha256:d89f1ffe98744c4b5c11f00fb843a4e72f68a6279b5e38168167f1b3c0fdd84c", - "sha256:e691b6ef6e27437860016bd6c32e481bdc2ed3af03289707a38b9ca422105f40", - "sha256:e750da6ac3ca624ae3303df448664012f9b6f9dfbc5d50048ea8a12ce2f8bc29", - "sha256:eca305b200549906ea25648463aeb1b3b220b716415183eaa99c998a846936d9", - "sha256:f52fe795e08858192eea167290033b5ff24f50f51781cb78d989e8d63cfe73d1" + "sha256:0358b9e9642bc7d39aac5cffe9884a99a5ca68e5e2c1b89e570ed60da9139908", + "sha256:091a359c4dafebbecd3959d9013f1b896b5371859165e4e50b01607a98d9e3e2", + "sha256:1998e4e60603c64bcc35af61b4331ab3af087457900d3980e18d190e17c3a697", + "sha256:2000b4088dee9a41f459fddaf6609bba48a435ce6374bb254c5ccdaa8928c5ba", + "sha256:2afb0064780d8aaf165875be5898c1866766e56175714fa5f9d055433e92d41d", + "sha256:2d8f1d9334a4e3ff176d096c14ded3100547d73440683567d85b8842a53180bb", + "sha256:2e38db22f6a3199fd63675e1b4bd795d676d906869047398f29f38ca55cb453a", + "sha256:3181f84649c1a1ca62b19ddf28436b1b2cb05ae6c7d2628f33872e713994c364", + "sha256:37462170dfd88af8431d04de6b236e6e9c06cda71e2ca26d88ef2332fd2a5237", + "sha256:3a9d8521c89bf6f2a929c3d12ad3ad7392c774c327ea809fd08a13be6b3bc05f", + "sha256:3d0bbd2e1a28b4429f24fd63a122a450ce9edb7a8063d070790092d7343a1aa4", + "sha256:483d60585ce3ee71929cea70949059f83850fa5e12deb9c094ed1c8c2ec73cbd", + "sha256:4888be27d5cba55ce94209baef5bcd7bbd7314a3d17021a5fc10000b3a5f737d", + "sha256:64b0d62e4209170a2a0c404c446ab83b941a0003e96604d2e4f4cb735f8a2254", + "sha256:68010900898fdf139ac08549c4dba8206c584070a960ffc530aebf0c6f2794ef", + "sha256:872ecb066de602a0099db98bd9e57f4cfc1d62f6093d94460c787737aa08f39e", + "sha256:88a32b03f2e4cd0e63f154cac76724709f40b3fc2f30139eb5d6f900521b44ed", + "sha256:b1dc7683da4e67ab2bebf266afa68098d681ae02ce570f0d1117312273d2b2ac", + "sha256:b29e27ce9371810250cb1528a771d047a9c7b0f79630dc7dc5815ff828f4273b", + "sha256:ce197559596370d985f1ce6b7051b52126849d8159040293bf8b98cb2b3e1f78", + "sha256:d45cf6daaf22584eff2175f48f82c4aa24d8e72a44913c5aff801819bb73d11f", + "sha256:e2ff9496322b2ce947ba4a7a5eb048158de9d6f3fe9efce29f1e8dd6878561e6", + "sha256:f7b979518ec1f294a41a707c007d54d0f3b3e1fd15d5b26b7e99b62b10d9a72e", + "sha256:f9c7268e9d16e34e50f8246c4f24cf7353764affd2bc971f0379514c246e3f6b", + "sha256:f9c839806089d79de588ee1dde2dae05dc1156d3355dfeb2b51fde84d9c960ad", + "sha256:ff962953e2389226adc4d355e34a98b0b800984399153c6678f2367b11b4d4b8" ], "index": "pypi", - "version": "==4.2.6" + "version": "==4.3.2" }, "markdownify": { "hashes": [ @@ -268,36 +273,36 @@ }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" ], - "version": "==1.1.0" + "version": "==1.1.1" }, "multidict": { "hashes": [ @@ -335,79 +340,40 @@ }, "packaging": { "hashes": [ - "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", - "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" - ], - "version": "==18.0" - }, - "pillow": { - "hashes": [ - "sha256:0cd42fe2d99ec6ce23aaf00947a7b7956ad2ed4b1695fd37545c3b8eae06d95a", - "sha256:137bed8972089d65da63fb79b4949b0f2b99e9a58f1b494e82be43ba8b0f4226", - "sha256:14eb2b2e4f2a14f5c89fd0edf55c5af0bf1a40fdf3838d81867f26f131cd557d", - "sha256:1fc43ce8c4fa3754222cd6831d599ad17ca2fc9868d2fb52f4e5362dfbfaf379", - "sha256:26dfeee23a86dad6277a63d18f61f53b957cb2cd3506dbbd74b88ba2fa65b3b1", - "sha256:2e0e582942e025cc58f669499a8e0bffde5bcc8d42b65729f294c1dac54e4672", - "sha256:3bb8dd3ce101dd8b0b37eaae924a5bb93abb6ffdd034bf68a066a808e11768ab", - "sha256:3f07da3874f0b085421f1d4f979785131aa9d497501d8610d82f7378b33858f8", - "sha256:429b2b5ae5f57f8fd9ec2e012c1e7b342ff10f1a8977dc291976b9a3b4c096e1", - "sha256:4a000fdd89d77b6b675de27e1ab91c6fba517c08f19ee83e6716b78930634e04", - "sha256:4ccbe7cce6156391a3ecf447c79a7d4a1a0ecd3de79bdec9ca5e4f7242a306d1", - "sha256:4d08034196db41acb7392e4fccfc0448e7a87192c41d3011ad4093eac2c31ffd", - "sha256:6b202b1cb524bc76ed52a7eb0314f4b0a0497c7cceb9a93539b5a25800e1f2b6", - "sha256:8563b56fa7c34f1606848c2143ea67d27cf225b9726a1b041c3d27cf85e46edc", - "sha256:86d7421e8803d7bae2e594765c378a867b629d46b32fbfe5ed9fd95b30989feb", - "sha256:8d4bddedcb4ab99131d9705a75720efc48b3d006122dae1a4cc329496ac47c9a", - "sha256:a4929c6de9590635c34533609402c9da12b22bfc2feb8c0c4f38c39bab48a9ad", - "sha256:b0736e21798448cee3e663c0df7a6dfa83d805b3f3a45e67f7457a2f019e5fca", - "sha256:b669acba91d47395de84c9ca52a7ad393b487e5ae2e20b9b2790b22a57d479fa", - "sha256:bba993443921f2d077195b425a3283357f52b07807d53704610c1249d20b183a", - "sha256:bdf706a93d00547c9443b2654ae424fd54d5dece4bc4333e7035740aeb7a7cea", - "sha256:c5aa93e55175b9cde95279ccd03c93d218976b376480222d37be41d2c9c54510", - "sha256:cc11fd997d8ad71bb0412e983b711e49639c2ddba9b9dce04d4bdab575fe5f84", - "sha256:d584f1c33995c3dc16a35e30ef43e0881fa0d085f0fef29cebf154ffb5643363", - "sha256:d88f54bdefb7ddccb68efdd710d689aa6a09b875cc3e44b7e81ef54e0751e3a7", - "sha256:de0d323072be72fa4d74f4e013cd594e3f8ee03b2e0eac5876a3249fa076ef7b", - "sha256:f139c963c6679d236b2c45369524338eabd36a853fe23abd39ba246ab0a75aec", - "sha256:f41c0bf667c4c1c30b873eaa8d6bb894f6d721b3e38e9c993bddd1263c02fb1f", - "sha256:fbd0ea468b4ec04270533bf5206f1cd57746fcf226520bb133318fa276de2644", - "sha256:fe2d2850521c467c915ff0a6e27dc64c3c04c2f66612e0072672bd1bd4854b61" + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" ], - "index": "pypi", - "version": "==5.4.0" + "version": "==19.0" + }, + "pamqp": { + "hashes": [ + "sha256:24370038a22583b8036fe92f94e5d04347e2b0f8e2502513dc5741472387b13e", + "sha256:e1fa1107a195993fca6e04f1eb7286b60e223c958944d7808a501258ccc0ef8c" + ], + "version": "==2.1.0" }, "pycares": { "hashes": [ - "sha256:080ae0f1b1b754be60b6ef31b9ab2915364c210eb1cb4d8e089357c89d7b9819", - "sha256:0eccb76dff0155ddf793a589c6270e1bdbf6975b2824d18d1d23db2075d7fc96", - "sha256:223a03d69e864a18d7bb2e0108bca5ba069ef91e5b048b953ed90ea9f50eb77f", - "sha256:289e49f98adfd7a2ae3656df26e1d62cf49a06bbc03ced63f243c22cd8919adf", - "sha256:292ac442a1d4ff27d41be748ec19f0c4ff47efebfb715064ba336564ea0f2071", - "sha256:34771095123da0e54597fe3c5585a28d3799945257e51b378a20778bf33573b6", - "sha256:34c8865f2d047be4c301ce90a916c7748be597e271c5c7932e8b9a6de85840f4", - "sha256:36af260b215f86ebfe4a5e4aea82fd6036168a5710cbf8aad77019ab52156dda", - "sha256:5e8e2a461717da40482b5fecf1119116234922d29660b3c3e01cbc5ba2cbf4bd", - "sha256:61e77bd75542c56dff49434fedbafb25604997bc57dc0ebf791a5732503cb1bb", - "sha256:691740c332f38a9035b4c6d1f0e6c8af239466ef2373a894d4393f0ea65c815d", - "sha256:6bc0e0fdcb4cdc4ca06aa0b07e6e3560d62b2af79ef0ea4589835fcd2059012b", - "sha256:96db5c93e2fe2e39f519efb7bb9d86aef56f5813fa0b032e47aba329fa925d57", - "sha256:af701b22c91b3e36f65ee9f4b1bc2fe4800c8ed486eb6ef203624acbe53d026d", - "sha256:b25bd21bba9c43d44320b719118c2ce35e4a78031f61d906caeb01316d49dafb", - "sha256:c42f68319f8ea2322ed81c31a86c4e60547e6e90f3ebef479a7a7540bddbf268", - "sha256:cc9a8d35af12bc5f484f3496f9cb3ab5bedfa4dcf3dfff953099453d88b659a7", - "sha256:dfee9d198ba6d6f29aa5bf510bfb2c28a60c3f308116f114c9fd311980d3e870", - "sha256:e1dd02e110a7a97582097ebba6713d9da28583b538c08e8a14bc82169c5d3e10", - "sha256:e48c586c80a139c6c7fb0298b944d1c40752cf839bc8584cc793e42a8971ba6c", - "sha256:f509762dec1a70eac32b86c098f37ac9c5d3d4a8a9098983328377c9e71543b2", - "sha256:f8e0d61733843844f9019c911d5676818d99c4cd2c54b91de58384c7d962862b", - "sha256:fe20280fed496deba60e0f6437b7672bdc83bf45e243bb546af47c60c85bcfbc" - ], - "version": "==2.4.0" + "sha256:2ca080db265ea238dc45f997f94effb62b979a617569889e265c26a839ed6305", + "sha256:6f79c6afb6ce603009db2042fddc2e348ad093ece9784cbe2daa809499871a23", + "sha256:70918d06eb0603016d37092a5f2c0228509eb4e6c5a3faacb4184f6ab7be7650", + "sha256:755187d28d24a9ea63aa2b4c0638be31d65fbf7f0ce16d41261b9f8cb55a1b99", + "sha256:7baa4b1f2146eb8423ff8303ebde3a20fb444a60db761fba0430d104fe35ddbf", + "sha256:90b27d4df86395f465a171386bc341098d6d47b65944df46518814ae298f6cc6", + "sha256:9e090dd6b2afa65cb51c133883b2bf2240fd0f717b130b0048714b33fb0f47ce", + "sha256:a11b7d63c3718775f6e805d6464cb10943780395ab042c7e5a0a7a9f612735dd", + "sha256:b253f5dcaa0ac7076b79388a3ac80dd8f3bd979108f813baade40d3a9b8bf0bd", + "sha256:c7f4f65e44ba35e35ad3febc844270665bba21cfb0fb7d749434e705b556e087", + "sha256:cdb342e6a254f035bd976d95807a2184038fc088d957a5104dcaab8be602c093", + "sha256:cf08e164f8bfb83b9fe633feb56f2754fae6baefcea663593794fa0518f8f98c", + "sha256:df9bc694cf03673878ea8ce674082c5acd134991d64d6c306d4bd61c0c1df98f" + ], + "version": "==3.0.0" }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", - "sha256:db32bd592ba104f8fbb8047c18cd897f0f20d0909ba0ec5dc72a3221f6a82e15" + "sha256:2d4f907c7667f7a264d65e5f48a516c70ea07e6ecb6f121588c1c97cd536a6b5", + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" ], "version": "==2.19" }, @@ -456,18 +422,18 @@ }, "pyparsing": { "hashes": [ - "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", - "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" + "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", + "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" ], - "version": "==2.3.0" + "version": "==2.3.1" }, "python-dateutil": { "hashes": [ - "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", - "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" ], "index": "pypi", - "version": "==2.7.5" + "version": "==2.8.0" }, "python-json-logger": { "hashes": [ @@ -478,10 +444,10 @@ }, "pytz": { "hashes": [ - "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", - "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" + "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", + "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" ], - "version": "==2018.7" + "version": "==2018.9" }, "pyyaml": { "hashes": [ @@ -508,12 +474,6 @@ "index": "pypi", "version": "==2.21.0" }, - "shortuuid": { - "hashes": [ - "sha256:d08fd398f40f8baf87e15eef8355e92fa541bca4eb8465fefab7ee22f92711b9" - ], - "version": "==0.5.0" - }, "six": { "hashes": [ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", @@ -530,18 +490,18 @@ }, "soupsieve": { "hashes": [ - "sha256:057e08f362a255b457a5781675211556799ed3bb8807506eaac3809390bc304b", - "sha256:f7d99b41637be2f249dfcc06ae93c13fcbbdfa7bb68b15308cdd0734e58146f1" + "sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b", + "sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26" ], - "version": "==1.6.1" + "version": "==1.8" }, "sphinx": { "hashes": [ - "sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", - "sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8" + "sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287", + "sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c" ], "index": "pypi", - "version": "==1.8.3" + "version": "==1.8.4" }, "sphinxcontrib-websupport": { "hashes": [ @@ -550,6 +510,15 @@ ], "version": "==1.1.0" }, + "typing": { + "hashes": [ + "sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", + "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", + "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" + ], + "markers": "python_version < '3.7'", + "version": "==3.6.6" + }, "urllib3": { "hashes": [ "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", @@ -603,24 +572,17 @@ "develop": { "aspy.yaml": { "hashes": [ - "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", - "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" + "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", + "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" ], - "version": "==1.1.1" + "version": "==1.2.0" }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" - ], - "version": "==18.2.0" - }, - "cached-property": { - "hashes": [ - "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", - "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==1.5.1" + "version": "==19.1.0" }, "certifi": { "hashes": [ @@ -631,10 +593,10 @@ }, "cfgv": { "hashes": [ - "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", - "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" + "sha256:39f8475d8eca48639f900daffa3f8bd2f60a31d989df41a9f81c5ad1779a66eb", + "sha256:a6a4366d32799a6bfb6f577ebe113b27ba8d1bae43cb57133b1472c1c3dae227" ], - "version": "==1.1.0" + "version": "==1.5.0" }, "chardet": { "hashes": [ @@ -664,13 +626,20 @@ ], "version": "==0.4.1" }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, "flake8": { "hashes": [ - "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", - "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.6.0" + "version": "==3.7.7" }, "flake8-bugbear": { "hashes": [ @@ -682,11 +651,11 @@ }, "flake8-import-order": { "hashes": [ - "sha256:9be5ca10d791d458eaa833dd6890ab2db37be80384707b0f76286ddd13c16cbf", - "sha256:feca2fd0a17611b33b7fa84449939196c2c82764e262486d5c3e143ed77d387b" + "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", + "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92" ], "index": "pypi", - "version": "==0.18" + "version": "==0.18.1" }, "flake8-string-format": { "hashes": [ @@ -698,11 +667,11 @@ }, "flake8-tidy-imports": { "hashes": [ - "sha256:5fc28c82bba16abb4f1154dc59a90487f5491fbdb27e658cbee241e8fddc1b91", - "sha256:c05c9f7dadb5748a04b6fa1c47cb6ae5a8170f03cfb1dca8b37aec58c1ee6d15" + "sha256:1c476aabc6e8db26dc75278464a3a392dba0ea80562777c5f13fd5cdf2646154", + "sha256:b3f5b96affd0f57cacb6621ed28286ce67edaca807757b51227043ebf7b136a1" ], "index": "pypi", - "version": "==1.1.0" + "version": "==2.0.0" }, "flake8-todo": { "hashes": [ @@ -713,10 +682,10 @@ }, "identify": { "hashes": [ - "sha256:08826e68e39e7de53cc2ddd8f6228a4e463b4bacb20565e5301c3ec690e68d27", - "sha256:2364e24a7699fea0dc910e90740adbab43eef3746eeea4e016029c34123ce66d" + "sha256:407cbb36e8b72b45cfa96a97ae13ccabca4c36557e03616958bd895dfcd3f77d", + "sha256:721abbbb1269fa1172799119981c22c5ace022544ce82eedc29b1b0d753baaa5" ], - "version": "==1.1.8" + "version": "==1.4.0" }, "idna": { "hashes": [ @@ -732,6 +701,14 @@ ], "version": "==0.8" }, + "importlib-resources": { + "hashes": [ + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + ], + "markers": "python_version < '3.7'", + "version": "==1.0.2" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -747,39 +724,39 @@ }, "packaging": { "hashes": [ - "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", - "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" ], - "version": "==18.0" + "version": "==19.0" }, "pre-commit": { "hashes": [ - "sha256:33bb9bf599c334d458fa9e311bde54e0c306a651473b6a36fdb36a61c8605c89", - "sha256:e233f5cf3230ae9ed9ada132e9cf6890e18cc937adc669353fb64394f6e80c17" + "sha256:d3d69c63ae7b7584c4b51446b0b583d454548f9df92575b2fe93a68ec800c4d3", + "sha256:fc512f129b9526e35e80d656a16a31c198f584c4fce3a5c739045b5140584917" ], "index": "pypi", - "version": "==1.13.0" + "version": "==1.14.4" }, "pycodestyle": { "hashes": [ - "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", - "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" ], - "version": "==2.4.0" + "version": "==2.5.0" }, "pyflakes": { "hashes": [ - "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", - "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" ], - "version": "==2.0.0" + "version": "==2.1.1" }, "pyparsing": { "hashes": [ - "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", - "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" + "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", + "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" ], - "version": "==2.3.0" + "version": "==2.3.1" }, "pyyaml": { "hashes": [ @@ -808,11 +785,11 @@ }, "safety": { "hashes": [ - "sha256:399511524f47230d5867f1eb75548f9feefb7a2711a4985cb5be0e034f87040f", - "sha256:69b970918324865dcd7b92337e07152a0ea1ceecaf92f4d3b38529ee0ca83441" + "sha256:0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59", + "sha256:5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78" ], "index": "pypi", - "version": "==1.8.4" + "version": "==1.8.5" }, "six": { "hashes": [ @@ -837,9 +814,10 @@ }, "virtualenv": { "hashes": [ - "sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c" + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" ], - "version": "==16.2.0" + "version": "==16.4.3" }, "zipp": { "hashes": [ diff --git a/bot/__init__.py b/bot/__init__.py index 54550842e..a088138a0 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -91,5 +91,4 @@ for key, value in logging.Logger.manager.loggerDict.items(): # Silence irrelevant loggers logging.getLogger("aio_pika").setLevel(logging.ERROR) logging.getLogger("discord").setLevel(logging.ERROR) -logging.getLogger("PIL").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) diff --git a/bot/__main__.py b/bot/__main__.py index 6928a3960..ead6d287a 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -72,7 +72,6 @@ bot.load_extension("bot.cogs.off_topic_names") bot.load_extension("bot.cogs.reddit") bot.load_extension("bot.cogs.reminders") bot.load_extension("bot.cogs.site") -bot.load_extension("bot.cogs.snakes") bot.load_extension("bot.cogs.snekbox") bot.load_extension("bot.cogs.superstarify") bot.load_extension("bot.cogs.tags") diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 8dac83d9b..2819b7dcc 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -1,5 +1,6 @@ import logging from functools import partial +from typing import List from discord import Colour, Embed, Member, Object from discord.ext.commands import ( @@ -175,7 +176,7 @@ class Events: users = [] for member in self.bot.get_guild(Guild.id).members: # type: Member - roles = [str(r.id) for r in member.roles] # type: List[int] + roles: List[int] = [str(r.id) for r in member.roles] users.append({ "avatar": member.avatar_url_as(format="png"), @@ -225,9 +226,9 @@ class Events: and before.avatar == after.avatar): return - before_role_names = [role.name for role in before.roles] # type: List[str] - after_role_names = [role.name for role in after.roles] # type: List[str] - role_ids = [str(r.id) for r in after.roles] # type: List[str] + before_role_names: List[str] = [role.name for role in before.roles] + after_role_names: List[str] = [role.name for role in after.roles] + role_ids: List[str] = [str(r.id) for r in after.roles] log.debug(f"{before.display_name} roles changing from {before_role_names} to {after_role_names}") @@ -242,7 +243,7 @@ class Events: log.debug(f"User {after.id} updated; changes: {changes}") async def on_member_join(self, member: Member): - role_ids = [str(r.id) for r in member.roles] # type: List[str] + role_ids: List[str] = [str(r.id) for r in member.roles] new_roles = [] try: diff --git a/bot/cogs/snakes.py b/bot/cogs/snakes.py deleted file mode 100644 index d74380259..000000000 --- a/bot/cogs/snakes.py +++ /dev/null @@ -1,1216 +0,0 @@ -import asyncio -import colorsys -import logging -import os -import random -import re -import string -import textwrap -import urllib -from functools import partial -from io import BytesIO -from typing import Any, Dict - -import aiohttp -import async_timeout -from discord import Colour, Embed, File, Member, Message, Reaction -from discord.ext.commands import BadArgument, Bot, Context, bot_has_permissions, group -from PIL import Image, ImageDraw, ImageFont - -from bot.constants import ERROR_REPLIES, Keys, URLs -from bot.converters import Snake -from bot.decorators import locked -from bot.utils.snakes import hatching, perlin, perlinsneks, sal - - -log = logging.getLogger(__name__) - - -# region: Constants -# Color -SNAKE_COLOR = 0x399600 - -# Antidote constants -SYRINGE_EMOJI = "\U0001F489" # :syringe: -PILL_EMOJI = "\U0001F48A" # :pill: -HOURGLASS_EMOJI = "\u231B" # :hourglass: -CROSSBONES_EMOJI = "\u2620" # :skull_crossbones: -ALEMBIC_EMOJI = "\u2697" # :alembic: -TICK_EMOJI = "\u2705" # :white_check_mark: - Correct peg, correct hole -CROSS_EMOJI = "\u274C" # :x: - Wrong peg, wrong hole -BLANK_EMOJI = "\u26AA" # :white_circle: - Correct peg, wrong hole -HOLE_EMOJI = "\u2B1C" # :white_square: - Used in guesses -EMPTY_UNICODE = "\u200b" # literally just an empty space - -ANTIDOTE_EMOJI = ( - SYRINGE_EMOJI, - PILL_EMOJI, - HOURGLASS_EMOJI, - CROSSBONES_EMOJI, - ALEMBIC_EMOJI, -) - -# Quiz constants -ANSWERS_EMOJI = { - "a": "\U0001F1E6", # :regional_indicator_a: 🇦 - "b": "\U0001F1E7", # :regional_indicator_b: 🇧 - "c": "\U0001F1E8", # :regional_indicator_c: 🇨 - "d": "\U0001F1E9", # :regional_indicator_d: 🇩 -} - -ANSWERS_EMOJI_REVERSE = { - "\U0001F1E6": "A", # :regional_indicator_a: 🇦 - "\U0001F1E7": "B", # :regional_indicator_b: 🇧 - "\U0001F1E8": "C", # :regional_indicator_c: 🇨 - "\U0001F1E9": "D", # :regional_indicator_d: 🇩 -} - -# Zzzen of pythhhon constant -ZEN = """ -Beautiful is better than ugly. -Explicit is better than implicit. -Simple is better than complex. -Complex is better than complicated. -Flat is better than nested. -Sparse is better than dense. -Readability counts. -Special cases aren't special enough to break the rules. -Although practicality beats purity. -Errors should never pass silently. -Unless explicitly silenced. -In the face of ambiguity, refuse the temptation to guess. -There should be one-- and preferably only one --obvious way to do it. -Now is better than never. -Although never is often better than *right* now. -If the implementation is hard to explain, it's a bad idea. -If the implementation is easy to explain, it may be a good idea. -""" - -# Max messages to train snake_chat on -MSG_MAX = 100 - -# get_snek constants -URL = "https://en.wikipedia.org/w/api.php?" - -# snake guess responses -INCORRECT_GUESS = ( - "Nope, that's not what it is.", - "Not quite.", - "Not even close.", - "Terrible guess.", - "Nnnno.", - "Dude. No.", - "I thought everyone knew this one.", - "Guess you suck at snakes.", - "Bet you feel stupid now.", - "Hahahaha, no.", - "Did you hit the wrong key?" -) - -CORRECT_GUESS = ( - "**WRONG**. Wait, no, actually you're right.", - "Yeah, you got it!", - "Yep, that's exactly what it is.", - "Uh-huh. Yep yep yep.", - "Yeah that's right.", - "Yup. How did you know that?", - "Are you a herpetologist?", - "Sure, okay, but I bet you can't pronounce it.", - "Are you cheating?" -) - -# snake card consts -CARD = { - "top": Image.open("bot/resources/snake_cards/card_top.png"), - "frame": Image.open("bot/resources/snake_cards/card_frame.png"), - "bottom": Image.open("bot/resources/snake_cards/card_bottom.png"), - "backs": [ - Image.open(f"bot/resources/snake_cards/backs/{file}") - for file in os.listdir("bot/resources/snake_cards/backs") - ], - "font": ImageFont.truetype("bot/resources/snake_cards/expressway.ttf", 20) -} -# endregion - - -class Snakes: - """ - Commands related to snakes. These were created by our - community during the first code jam. - - More information can be found in the code-jam-1 repo. - - https://gitlab_bot_repo.com/discord-python/code-jams/code-jam-1 - """ - - wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL) - valid_image_extensions = ('gif', 'png', 'jpeg', 'jpg', 'webp') - - def __init__(self, bot: Bot): - self.active_sal = {} - self.bot = bot - self.headers = {"X-API-KEY": Keys.site_api} - - # region: Helper methods - @staticmethod - def _beautiful_pastel(hue): - """ - Returns random bright pastels. - """ - - light = random.uniform(0.7, 0.85) - saturation = 1 - - rgb = colorsys.hls_to_rgb(hue, light, saturation) - hex_rgb = "" - - for part in rgb: - value = int(part * 0xFF) - hex_rgb += f"{value:02x}" - - return int(hex_rgb, 16) - - @staticmethod - def _generate_card(buffer: BytesIO, content: dict) -> BytesIO: - """ - Generate a card from snake information. - - Written by juan and Someone during the first code jam. - """ - - snake = Image.open(buffer) - - # Get the size of the snake icon, configure the height of the image box (yes, it changes) - icon_width = 347 # Hardcoded, not much i can do about that - icon_height = int((icon_width / snake.width) * snake.height) - frame_copies = icon_height // CARD['frame'].height + 1 - snake.thumbnail((icon_width, icon_height)) - - # Get the dimensions of the final image - main_height = icon_height + CARD['top'].height + CARD['bottom'].height - main_width = CARD['frame'].width - - # Start creating the foreground - foreground = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0)) - foreground.paste(CARD['top'], (0, 0)) - - # Generate the frame borders to the correct height - for offset in range(frame_copies): - position = (0, CARD['top'].height + offset * CARD['frame'].height) - foreground.paste(CARD['frame'], position) - - # Add the image and bottom part of the image - foreground.paste(snake, (36, CARD['top'].height)) # Also hardcoded :( - foreground.paste(CARD['bottom'], (0, CARD['top'].height + icon_height)) - - # Setup the background - back = random.choice(CARD['backs']) - back_copies = main_height // back.height + 1 - full_image = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0)) - - # Generate the tiled background - for offset in range(back_copies): - full_image.paste(back, (16, 16 + offset * back.height)) - - # Place the foreground onto the final image - full_image.paste(foreground, (0, 0), foreground) - - # Get the first two sentences of the info - description = '.'.join(content['info'].split(".")[:2]) + '.' - - # Setup positioning variables - margin = 36 - offset = CARD['top'].height + icon_height + margin - - # Create blank rectangle image which will be behind the text - rectangle = Image.new( - "RGBA", - (main_width, main_height), - (0, 0, 0, 0) - ) - - # Draw a semi-transparent rectangle on it - rect = ImageDraw.Draw(rectangle) - rect.rectangle( - (margin, offset, main_width - margin, main_height - margin), - fill=(63, 63, 63, 128) - ) - - # Paste it onto the final image - full_image.paste(rectangle, (0, 0), mask=rectangle) - - # Draw the text onto the final image - draw = ImageDraw.Draw(full_image) - for line in textwrap.wrap(description, 36): - draw.text([margin + 4, offset], line, font=CARD['font']) - offset += CARD['font'].getsize(line)[1] - - # Get the image contents as a BufferIO object - buffer = BytesIO() - full_image.save(buffer, 'PNG') - buffer.seek(0) - - return buffer - - @staticmethod - def _snakify(message): - """ - Sssnakifffiesss a sstring. - """ - - # Replace fricatives with exaggerated snake fricatives. - simple_fricatives = [ - "f", "s", "z", "h", - "F", "S", "Z", "H", - ] - complex_fricatives = [ - "th", "sh", "Th", "Sh" - ] - - for letter in simple_fricatives: - if letter.islower(): - message = message.replace(letter, letter * random.randint(2, 4)) - else: - message = message.replace(letter, (letter * random.randint(2, 4)).title()) - - for fricative in complex_fricatives: - message = message.replace(fricative, fricative[0] + fricative[1] * random.randint(2, 4)) - - return message - - async def _fetch(self, session, url, params=None): - """ - Asyncronous web request helper method. - """ - - if params is None: - params = {} - - async with async_timeout.timeout(10): - async with session.get(url, params=params) as response: - return await response.json() - - def _get_random_long_message(self, messages, retries=10): - """ - Fetch a message that's at least 3 words long, - but only if it is possible to do so in retries - attempts. Else, just return whatever the last - message is. - """ - - long_message = random.choice(messages) - if len(long_message.split()) < 3 and retries > 0: - return self._get_random_long_message( - messages, - retries=retries - 1 - ) - - return long_message - - async def _get_snek(self, name: str) -> Dict[str, Any]: - """ - Goes online and fetches all the data from a wikipedia article - about a snake. Builds a dict that the .get() method can use. - - Created by Ava and eivl. - - :param name: The name of the snake to get information for - omit for a random snake - :return: A dict containing information on a snake - """ - - snake_info = {} - - async with aiohttp.ClientSession() as session: - params = { - 'format': 'json', - 'action': 'query', - 'list': 'search', - 'srsearch': name, - 'utf8': '', - 'srlimit': '1', - } - - json = await self._fetch(session, URL, params=params) - - # wikipedia does have a error page - try: - pageid = json["query"]["search"][0]["pageid"] - except KeyError: - # Wikipedia error page ID(?) - pageid = 41118 - except IndexError: - return None - - params = { - 'format': 'json', - 'action': 'query', - 'prop': 'extracts|images|info', - 'exlimit': 'max', - 'explaintext': '', - 'inprop': 'url', - 'pageids': pageid - } - - json = await self._fetch(session, URL, params=params) - - # constructing dict - handle exceptions later - try: - snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"] - snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"] - snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"] - snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"] - snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"] - except KeyError: - snake_info["error"] = True - - if snake_info["images"]: - i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/' - image_list = [] - map_list = [] - thumb_list = [] - - # Wikipedia has arbitrary images that are not snakes - banned = [ - 'Commons-logo.svg', - 'Red%20Pencil%20Icon.png', - 'distribution', - 'The%20Death%20of%20Cleopatra%20arthur.jpg', - 'Head%20of%20holotype', - 'locator', - 'Woma.png', - '-map.', - '.svg', - 'ange.', - 'Adder%20(PSF).png' - ] - - for image in snake_info["images"]: - # images come in the format of `File:filename.extension` - file, sep, filename = image["title"].partition(':') - filename = filename.replace(" ", "%20") # Wikipedia returns good data! - - if not filename.startswith('Map'): - if any(ban in filename for ban in banned): - pass - else: - image_list.append(f"{i_url}{filename}") - thumb_list.append(f"{i_url}{filename}?width=100") - else: - map_list.append(f"{i_url}{filename}") - - snake_info["image_list"] = image_list - snake_info["map_list"] = map_list - snake_info["thumb_list"] = thumb_list - snake_info["name"] = name - - match = self.wiki_brief.match(snake_info['extract']) - info = match.group(1) if match else None - - if info: - info = info.replace("\n", "\n\n") # Give us some proper paragraphs. - - snake_info["info"] = info - - return snake_info - - async def _get_snake_name(self) -> Dict[str, str]: - """ - Gets a random snake name. - :return: A random snake name, as a string. - """ - - response = await self.bot.http_session.get(URLs.site_names_api, headers=self.headers) - name_data = await response.json() - - return name_data - - async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list): - """ - Validate the answer using a reaction event loop - :return: - """ - - def predicate(reaction, user): - """ - Test if the the answer is valid and can be evaluated. - """ - return ( - reaction.message.id == message.id # The reaction is attached to the question we asked. - and user == ctx.author # It's the user who triggered the quiz. - and str(reaction.emoji) in ANSWERS_EMOJI.values() # The reaction is one of the options. - ) - - for emoji in ANSWERS_EMOJI.values(): - await message.add_reaction(emoji) - - # Validate the answer - try: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=45.0, check=predicate) - except asyncio.TimeoutError: - await ctx.channel.send(f"You took too long. The correct answer was **{options[answer]}**.") - await message.clear_reactions() - return - - if str(reaction.emoji) == ANSWERS_EMOJI[answer]: - await ctx.send(f"{random.choice(CORRECT_GUESS)} The correct answer was **{options[answer]}**.") - else: - await ctx.send( - f"{random.choice(INCORRECT_GUESS)} The correct answer was **{options[answer]}**." - ) - - await message.clear_reactions() - # endregion - - # region: Commands - @group(name='snakes', aliases=('snake',), invoke_without_command=True) - async def snakes_group(self, ctx: Context): - """Commands from our first code jam.""" - - await ctx.invoke(self.bot.get_command("help"), "snake") - - @bot_has_permissions(manage_messages=True) - @snakes_group.command(name='antidote') - @locked() - async def antidote_command(self, ctx: Context): - """ - Antidote - Can you create the antivenom before the patient dies? - - Rules: You have 4 ingredients for each antidote, you only have 10 attempts - Once you synthesize the antidote, you will be presented with 4 markers - Tick: This means you have a CORRECT ingredient in the CORRECT position - Circle: This means you have a CORRECT ingredient in the WRONG position - Cross: This means you have a WRONG ingredient in the WRONG position - - Info: The game automatically ends after 5 minutes inactivity. - You should only use each ingredient once. - - This game was created by Lord Bisk and Runew0lf. - """ - - def predicate(reaction_: Reaction, user_: Member): - """ - Make sure that this reaction is what we want to operate on - """ - - return ( - all(( - reaction_.message.id == board_id.id, # Reaction is on this message - reaction_.emoji in ANTIDOTE_EMOJI, # Reaction is one of the pagination emotes - user_.id != self.bot.user.id, # Reaction was not made by the Bot - user_.id == ctx.author.id # Reaction was made by author - )) - ) - - # Initialize variables - antidote_tries = 0 - antidote_guess_count = 0 - antidote_guess_list = [] - guess_result = [] - board = [] - page_guess_list = [] - page_result_list = [] - win = False - - antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote") - antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url) - - # Generate answer - antidote_answer = list(ANTIDOTE_EMOJI) # Duplicate list, not reference it - random.shuffle(antidote_answer) - antidote_answer.pop() - - # Begin initial board building - for i in range(0, 10): - page_guess_list.append(f"{HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI}") - page_result_list.append(f"{CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI}") - board.append(f"`{i+1:02d}` " - f"{page_guess_list[i]} - " - f"{page_result_list[i]}") - board.append(EMPTY_UNICODE) - antidote_embed.add_field(name="10 guesses remaining", value="\n".join(board)) - board_id = await ctx.send(embed=antidote_embed) # Display board - - # Add our player reactions - for emoji in ANTIDOTE_EMOJI: - await board_id.add_reaction(emoji) - - # Begin main game loop - while not win and antidote_tries < 10: - try: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=300, check=predicate) - except asyncio.TimeoutError: - log.debug("Antidote timed out waiting for a reaction") - break # We're done, no reactions for the last 5 minutes - - if antidote_tries < 10: - if antidote_guess_count < 4: - if reaction.emoji in ANTIDOTE_EMOJI: - antidote_guess_list.append(reaction.emoji) - antidote_guess_count += 1 - - if antidote_guess_count == 4: # Guesses complete - antidote_guess_count = 0 - page_guess_list[antidote_tries] = " ".join(antidote_guess_list) - - # Now check guess - for i in range(0, len(antidote_answer)): - if antidote_guess_list[i] == antidote_answer[i]: - guess_result.append(TICK_EMOJI) - elif antidote_guess_list[i] in antidote_answer: - guess_result.append(BLANK_EMOJI) - else: - guess_result.append(CROSS_EMOJI) - guess_result.sort() - page_result_list[antidote_tries] = " ".join(guess_result) - - # Rebuild the board - board = [] - for i in range(0, 10): - board.append(f"`{i+1:02d}` " - f"{page_guess_list[i]} - " - f"{page_result_list[i]}") - board.append(EMPTY_UNICODE) - - # Remove Reactions - for emoji in antidote_guess_list: - await board_id.remove_reaction(emoji, user) - - if antidote_guess_list == antidote_answer: - win = True - - antidote_tries += 1 - guess_result = [] - antidote_guess_list = [] - - antidote_embed.clear_fields() - antidote_embed.add_field(name=f"{10 - antidote_tries} " - f"guesses remaining", - value="\n".join(board)) - # Redisplay the board - await board_id.edit(embed=antidote_embed) - - # Winning / Ending Screen - if win is True: - antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote") - antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url) - antidote_embed.set_image(url="https://i.makeagif.com/media/7-12-2015/Cj1pts.gif") - antidote_embed.add_field(name=f"You have created the snake antidote!", - value=f"The solution was: {' '.join(antidote_answer)}\n" - f"You had {10 - antidote_tries} tries remaining.") - await board_id.edit(embed=antidote_embed) - else: - antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote") - antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url) - antidote_embed.set_image(url="https://media.giphy.com/media/ceeN6U57leAhi/giphy.gif") - antidote_embed.add_field(name=EMPTY_UNICODE, - value=f"Sorry you didnt make the antidote in time.\n" - f"The formula was {' '.join(antidote_answer)}") - await board_id.edit(embed=antidote_embed) - - log.debug("Ending pagination and removing all reactions...") - await board_id.clear_reactions() - - @snakes_group.command(name='draw') - async def draw_command(self, ctx: Context): - """ - Draws a random snek using Perlin noise - - Written by Momo and kel. - Modified by juan and lemon. - """ - - with ctx.typing(): - - # Generate random snake attributes - width = random.randint(6, 10) - length = random.randint(15, 22) - random_hue = random.random() - snek_color = self._beautiful_pastel(random_hue) - text_color = self._beautiful_pastel((random_hue + 0.5) % 1) - bg_color = ( - random.randint(32, 50), - random.randint(32, 50), - random.randint(50, 70), - ) - - # Get a snake idiom from the API - response = await self.bot.http_session.get(URLs.site_idioms_api, headers=self.headers) - text = await response.json() - - # Build and send the snek - factory = perlin.PerlinNoiseFactory(dimension=1, octaves=2) - image_frame = perlinsneks.create_snek_frame( - factory, - snake_width=width, - snake_length=length, - snake_color=snek_color, - text=text, - text_color=text_color, - bg_color=bg_color - ) - png_bytes = perlinsneks.frame_to_png_bytes(image_frame) - - file = File(png_bytes, filename='snek.png') - - await ctx.send(file=file) - - @snakes_group.command(name='get') - @bot_has_permissions(manage_messages=True) - @locked() - async def get_command(self, ctx: Context, *, name: Snake = None): - """ - Fetches information about a snake from Wikipedia. - :param ctx: Context object passed from discord.py - :param name: Optional, the name of the snake to get information for - omit for a random snake - - Created by Ava and eivl. - """ - - with ctx.typing(): - if name is None: - name = await Snake.random() - - if isinstance(name, dict): - data = name - else: - data = await self._get_snek(name) - - if data.get('error'): - return await ctx.send('Could not fetch data from Wikipedia.') - - description = data["info"] - - # Shorten the description if needed - if len(description) > 1000: - description = description[:1000] - last_newline = description.rfind("\n") - if last_newline > 0: - description = description[:last_newline] - - # Strip and add the Wiki link. - if "fullurl" in data: - description = description.strip("\n") - description += f"\n\nRead more on [Wikipedia]({data['fullurl']})" - - # Build and send the embed. - embed = Embed( - title=data.get("title", data.get('name')), - description=description, - colour=0x59982F, - ) - - emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png' - image = next((url for url in data['image_list'] if url.endswith(self.valid_image_extensions)), emoji) - embed.set_image(url=image) - - await ctx.send(embed=embed) - - @snakes_group.command(name='guess', aliases=('identify',)) - @locked() - async def guess_command(self, ctx): - """ - Snake identifying game! - - Made by Ava and eivl. - Modified by lemon. - """ - - with ctx.typing(): - - image = None - - while image is None: - snakes = [await Snake.random() for _ in range(4)] - snake = random.choice(snakes) - answer = "abcd"[snakes.index(snake)] - - data = await self._get_snek(snake) - - image = next((url for url in data['image_list'] if url.endswith(self.valid_image_extensions)), None) - - embed = Embed( - title='Which of the following is the snake in the image?', - description="\n".join(f"{'ABCD'[snakes.index(snake)]}: {snake}" for snake in snakes), - colour=SNAKE_COLOR - ) - embed.set_image(url=image) - - guess = await ctx.send(embed=embed) - options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes} - await self._validate_answer(ctx, guess, answer, options) - - @snakes_group.command(name='hatch') - async def hatch_command(self, ctx: Context): - """ - Hatches your personal snake - - Written by Momo and kel. - """ - - # Pick a random snake to hatch. - snake_name = random.choice(list(hatching.snakes.keys())) - snake_image = hatching.snakes[snake_name] - - # Hatch the snake - message = await ctx.channel.send(embed=Embed(description="Hatching your snake :snake:...")) - await asyncio.sleep(1) - - for stage in hatching.stages: - hatch_embed = Embed(description=stage) - await message.edit(embed=hatch_embed) - await asyncio.sleep(1) - await asyncio.sleep(1) - await message.delete() - - # Build and send the embed. - my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name)) - my_snake_embed.set_thumbnail(url=snake_image) - my_snake_embed.set_footer( - text=" Owner: {0}#{1}".format(ctx.message.author.name, ctx.message.author.discriminator) - ) - - await ctx.channel.send(embed=my_snake_embed) - - @snakes_group.command(name='movie') - async def movie_command(self, ctx: Context): - """ - Gets a random snake-related movie from OMDB. - - Written by Samuel. - Modified by gdude. - """ - - url = "http://www.omdbapi.com/" - page = random.randint(1, 27) - - response = await self.bot.http_session.get( - url, - params={ - "s": "snake", - "page": page, - "type": "movie", - "apikey": Keys.omdb - } - ) - data = await response.json() - movie = random.choice(data["Search"])["imdbID"] - - response = await self.bot.http_session.get( - url, - params={ - "i": movie, - "apikey": Keys.omdb - } - ) - data = await response.json() - - embed = Embed( - title=data["Title"], - color=SNAKE_COLOR - ) - - del data["Response"], data["imdbID"], data["Title"] - - for key, value in data.items(): - if not value or value == "N/A" or key in ("Response", "imdbID", "Title", "Type"): - continue - - if key == "Ratings": # [{'Source': 'Internet Movie Database', 'Value': '7.6/10'}] - rating = random.choice(value) - - if rating["Source"] != "Internet Movie Database": - embed.add_field(name=f"Rating: {rating['Source']}", value=rating["Value"]) - - continue - - if key == "Poster": - embed.set_image(url=value) - continue - - elif key == "imdbRating": - key = "IMDB Rating" - - elif key == "imdbVotes": - key = "IMDB Votes" - - embed.add_field(name=key, value=value, inline=True) - - embed.set_footer(text="Data provided by the OMDB API") - - await ctx.channel.send( - embed=embed - ) - - @snakes_group.command(name='quiz') - @locked() - async def quiz_command(self, ctx: Context): - """ - Asks a snake-related question in the chat and validates the user's guess. - - This was created by Mushy and Cardium, - and modified by Urthas and lemon. - """ - - # Prepare a question. - response = await self.bot.http_session.get(URLs.site_quiz_api, headers=self.headers) - question = await response.json() - answer = question["answerkey"] - options = {key: question["options"][key] for key in ANSWERS_EMOJI.keys()} - - # Build and send the embed. - embed = Embed( - color=SNAKE_COLOR, - title=question["question"], - description="\n".join( - [f"**{key.upper()}**: {answer}" for key, answer in options.items()] - ) - ) - - quiz = await ctx.channel.send("", embed=embed) - await self._validate_answer(ctx, quiz, answer, options) - - @snakes_group.command(name='name', aliases=('name_gen',)) - async def name_command(self, ctx: Context, *, name: str = None): - """ - Slices the users name at the last vowel (or second last if the name - ends with a vowel), and then combines it with a random snake name, - which is sliced at the first vowel (or second if the name starts with - a vowel). - - If the name contains no vowels, it just appends the snakename - to the end of the name. - - Examples: - lemon + anaconda = lemoconda - krzsn + anaconda = krzsnconda - gdude + anaconda = gduconda - aperture + anaconda = apertuconda - lucy + python = luthon - joseph + taipan = joseipan - - This was written by Iceman, and modified for inclusion into the bot by lemon. - """ - - snake_name = await self._get_snake_name() - snake_name = snake_name['name'] - snake_prefix = "" - - # Set aside every word in the snake name except the last. - if " " in snake_name: - snake_prefix = " ".join(snake_name.split()[:-1]) - snake_name = snake_name.split()[-1] - - # If no name is provided, use whoever called the command. - if name: - user_name = name - else: - user_name = ctx.author.display_name - - # Get the index of the vowel to slice the username at - user_slice_index = len(user_name) - for index, char in enumerate(reversed(user_name)): - if index == 0: - continue - if char.lower() in "aeiouy": - user_slice_index -= index - break - - # Now, get the index of the vowel to slice the snake_name at - snake_slice_index = 0 - for index, char in enumerate(snake_name): - if index == 0: - continue - if char.lower() in "aeiouy": - snake_slice_index = index + 1 - break - - # Combine! - snake_name = snake_name[snake_slice_index:] - user_name = user_name[:user_slice_index] - result = f"{snake_prefix} {user_name}{snake_name}" - result = string.capwords(result) - - # Embed and send - embed = Embed( - title="Snake name", - description=f"Your snake-name is **{result}**", - color=SNAKE_COLOR - ) - - return await ctx.send(embed=embed) - - @snakes_group.command(name='sal') - @locked() - async def sal_command(self, ctx: Context): - """ - Play a game of Snakes and Ladders! - - Written by Momo and kel. - Modified by lemon. - """ - - # check if there is already a game in this channel - if ctx.channel in self.active_sal: - await ctx.send(f"{ctx.author.mention} A game is already in progress in this channel.") - return - - game = sal.SnakeAndLaddersGame(snakes=self, context=ctx) - self.active_sal[ctx.channel] = game - - await game.open_game() - - @snakes_group.command(name='about') - async def about_command(self, ctx: Context): - """ - A command that shows an embed with information about the event, - it's participants, and its winners. - """ - - contributors = [ - "<@!245270749919576066>", - "<@!396290259907903491>", - "<@!172395097705414656>", - "<@!361708843425726474>", - "<@!300302216663793665>", - "<@!210248051430916096>", - "<@!174588005745557505>", - "<@!87793066227822592>", - "<@!211619754039967744>", - "<@!97347867923976192>", - "<@!136081839474343936>", - "<@!263560579770220554>", - "<@!104749643715387392>", - "<@!303940835005825024>", - ] - - embed = Embed( - title="About the snake cog", - description=( - "The features in this cog were created by members of the community " - "during our first ever [code jam event](https://gitlab.com/discord-python/code-jams/code-jam-1). \n\n" - "The event saw over 50 participants, who competed to write a discord bot cog with a snake theme over " - "48 hours. The staff then selected the best features from all the best teams, and made modifications " - "to ensure they would all work together before integrating them into the community bot.\n\n" - "It was a tight race, but in the end, <@!104749643715387392> and <@!303940835005825024> " - "walked away as grand champions. Make sure you check out `!snakes sal`, `!snakes draw` " - "and `!snakes hatch` to see what they came up with." - ) - ) - - embed.add_field( - name="Contributors", - value=( - ", ".join(contributors) - ) - ) - - await ctx.channel.send(embed=embed) - - @snakes_group.command(name='card') - async def card_command(self, ctx: Context, *, name: Snake = None): - """ - Create an interesting little card from a snake! - - Created by juan and Someone during the first code jam. - """ - - # Get the snake data we need - if not name: - name_obj = await self._get_snake_name() - name = name_obj['scientific'] - content = await self._get_snek(name) - - elif isinstance(name, dict): - content = name - - else: - content = await self._get_snek(name) - - # Make the card - async with ctx.typing(): - - stream = BytesIO() - async with async_timeout.timeout(10): - async with self.bot.http_session.get(content['image_list'][0]) as response: - stream.write(await response.read()) - - stream.seek(0) - - func = partial(self._generate_card, stream, content) - final_buffer = await self.bot.loop.run_in_executor(None, func) - - # Send it! - await ctx.send( - f"A wild {content['name'].title()} appears!", - file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png") - ) - - @snakes_group.command(name='fact') - async def fact_command(self, ctx: Context): - """ - Gets a snake-related fact - - Written by Andrew and Prithaj. - Modified by lemon. - """ - - # Get a fact from the API. - response = await self.bot.http_session.get(URLs.site_facts_api, headers=self.headers) - question = await response.json() - - # Build and send the embed. - embed = Embed( - title="Snake fact", - color=SNAKE_COLOR, - description=question - ) - await ctx.channel.send(embed=embed) - - @snakes_group.command(name='help') - async def help_command(self, ctx: Context): - """ - This just invokes the help command on this cog. - """ - - log.debug(f"{ctx.author} requested info about the snakes cog") - return await ctx.invoke(self.bot.get_command("help"), "Snakes") - - @snakes_group.command(name='snakify') - async def snakify_command(self, ctx: Context, *, message: str = None): - """ - How would I talk if I were a snake? - :param ctx: context - :param message: If this is passed, it will snakify the message. - If not, it will snakify a random message from - the users history. - - Written by Momo and kel. - Modified by lemon. - """ - - with ctx.typing(): - embed = Embed() - user = ctx.message.author - - if not message: - - # Get a random message from the users history - messages = [] - async for message in ctx.channel.history(limit=500).filter( - lambda msg: msg.author == ctx.message.author # Message was sent by author. - ): - messages.append(message.content) - - message = self._get_random_long_message(messages) - - # Set the avatar - if user.avatar is not None: - avatar = f"https://cdn.discordapp.com/avatars/{user.id}/{user.avatar}" - else: - avatar = ctx.author.default_avatar_url - - # Build and send the embed - embed.set_author( - name=f"{user.name}#{user.discriminator}", - icon_url=avatar, - ) - embed.description = f"*{self._snakify(message)}*" - - await ctx.channel.send(embed=embed) - - @snakes_group.command(name='video', aliases=('get_video',)) - async def video_command(self, ctx: Context, *, search: str = None): - """ - Gets a YouTube video about snakes - :param name: Optional, a name of a snake. Used to search for videos with that name - :param ctx: Context object passed from discord.py - - Written by Andrew and Prithaj. - """ - - # Are we searching for anything specific? - if search: - query = search + ' snake' - else: - snake = await self._get_snake_name() - query = snake['name'] - - # Build the URL and make the request - url = f'https://www.googleapis.com/youtube/v3/search' - response = await self.bot.http_session.get( - url, - params={ - "part": "snippet", - "q": urllib.parse.quote(query), - "type": "video", - "key": Keys.youtube - } - ) - response = await response.json() - data = response['items'] - - # Send the user a video - if len(data) > 0: - num = random.randint(0, len(data) - 1) - youtube_base_url = 'https://www.youtube.com/watch?v=' - await ctx.channel.send( - content=f"{youtube_base_url}{data[num]['id']['videoId']}" - ) - else: - log.warning(f"YouTube API error. Full response looks like {response}") - - @snakes_group.command(name='zen') - async def zen_command(self, ctx: Context): - """ - Gets a random quote from the Zen of Python, - except as if spoken by a snake. - - Written by Prithaj and Andrew. - Modified by lemon. - """ - - embed = Embed( - title="Zzzen of Pythhon", - color=SNAKE_COLOR - ) - - # Get the zen quote and snakify it - zen_quote = random.choice(ZEN.splitlines()) - zen_quote = self._snakify(zen_quote) - - # Embed and send - embed.description = zen_quote - await ctx.channel.send( - embed=embed - ) - # endregion - - # region: Error handlers - @get_command.error - @card_command.error - @video_command.error - async def command_error(self, ctx, error): - - embed = Embed() - embed.colour = Colour.red() - - if isinstance(error, BadArgument): - embed.description = str(error) - embed.title = random.choice(ERROR_REPLIES) - - elif isinstance(error, OSError): - log.error(f"snake_card encountered an OSError: {error} ({error.original})") - embed.description = "Could not generate the snake card! Please try again." - embed.title = random.choice(ERROR_REPLIES) - - else: - log.error(f"Unhandled tag command error: {error} ({error.original})") - return - - await ctx.send(embed=embed) - # endregion - - -def setup(bot): - bot.add_cog(Snakes(bot)) - log.info("Cog loaded: Snakes") diff --git a/bot/constants.py b/bot/constants.py index 99afc1e6e..93f8a231b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -387,9 +387,7 @@ class Keys(metaclass=YAMLGetter): deploy_bot: str deploy_site: str - omdb: str site_api: str - youtube: str class RabbitMQ(metaclass=YAMLGetter): @@ -412,25 +410,19 @@ class URLs(metaclass=YAMLGetter): bot_avatar: str deploy: str gitlab_bot_repo: str - omdb: str status: str # Site endpoints site: str site_api: str - site_facts_api: str site_clean_api: str site_superstarify_api: str - site_idioms_api: str site_logs_api: str site_logs_view: str - site_names_api: str - site_quiz_api: str site_reminders_api: str site_reminders_user_api: str site_schema: str site_settings_api: str - site_special_api: str site_tags_api: str site_user_api: str site_user_complete_api: str diff --git a/bot/converters.py b/bot/converters.py index 069e841f9..91f30ac5e 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -1,117 +1,14 @@ import logging -import random -import socket from ssl import CertificateError import discord -from aiohttp import AsyncResolver, ClientConnectorError, ClientSession, TCPConnector +from aiohttp import ClientConnectorError from discord.ext.commands import BadArgument, Context, Converter -from fuzzywuzzy import fuzz - -from bot.constants import DEBUG_MODE, Keys, URLs -from bot.utils import disambiguate log = logging.getLogger(__name__) -class Snake(Converter): - snakes = None - special_cases = None - - async def convert(self, ctx, name): - await self.build_list() - name = name.lower() - - if name == 'python': - return 'Python (programming language)' - - def get_potential(iterable, *, threshold=80): - nonlocal name - potential = [] - - for item in iterable: - original, item = item, item.lower() - - if name == item: - return [original] - - a, b = fuzz.ratio(name, item), fuzz.partial_ratio(name, item) - if a >= threshold or b >= threshold: - potential.append(original) - - return potential - - # Handle special cases - if name.lower() in self.special_cases: - return self.special_cases.get(name.lower(), name.lower()) - - names = {snake['name']: snake['scientific'] for snake in self.snakes} - all_names = names.keys() | names.values() - timeout = len(all_names) * (3 / 4) - - embed = discord.Embed(title='Found multiple choices. Please choose the correct one.', colour=0x59982F) - embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url) - - name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed) - return names.get(name, name) - - @classmethod - async def build_list(cls): - - headers = {"X-API-KEY": Keys.site_api} - - # Set up the session - if DEBUG_MODE: - http_session = ClientSession( - connector=TCPConnector( - resolver=AsyncResolver(), - family=socket.AF_INET, - verify_ssl=False, - ) - ) - else: - http_session = ClientSession( - connector=TCPConnector( - resolver=AsyncResolver() - ) - ) - - # Get all the snakes - if cls.snakes is None: - response = await http_session.get( - URLs.site_names_api, - params={"get_all": "true"}, - headers=headers - ) - cls.snakes = await response.json() - - # Get the special cases - if cls.special_cases is None: - response = await http_session.get( - URLs.site_special_api, - headers=headers - ) - special_cases = await response.json() - cls.special_cases = {snake['name'].lower(): snake for snake in special_cases} - - # Close the session - http_session.close() - - @classmethod - async def random(cls): - """ - This is stupid. We should find a way to - somehow get the global session into a - global context, so I can get it from here. - :return: - """ - - await cls.build_list() - names = [snake['scientific'] for snake in cls.snakes] - return random.choice(names) - - class ValidPythonIdentifier(Converter): """ A converter that checks whether the given string is a valid Python identifier. diff --git a/bot/resources/snake_cards/backs/card_back1.jpg b/bot/resources/snake_cards/backs/card_back1.jpg Binary files differdeleted file mode 100644 index 22959fa73..000000000 --- a/bot/resources/snake_cards/backs/card_back1.jpg +++ /dev/null diff --git a/bot/resources/snake_cards/backs/card_back2.jpg b/bot/resources/snake_cards/backs/card_back2.jpg Binary files differdeleted file mode 100644 index d56edc320..000000000 --- a/bot/resources/snake_cards/backs/card_back2.jpg +++ /dev/null diff --git a/bot/resources/snake_cards/card_bottom.png b/bot/resources/snake_cards/card_bottom.png Binary files differdeleted file mode 100644 index 8b2b91c5c..000000000 --- a/bot/resources/snake_cards/card_bottom.png +++ /dev/null diff --git a/bot/resources/snake_cards/card_frame.png b/bot/resources/snake_cards/card_frame.png Binary files differdeleted file mode 100644 index 149a0a5f6..000000000 --- a/bot/resources/snake_cards/card_frame.png +++ /dev/null diff --git a/bot/resources/snake_cards/card_top.png b/bot/resources/snake_cards/card_top.png Binary files differdeleted file mode 100644 index e329c873a..000000000 --- a/bot/resources/snake_cards/card_top.png +++ /dev/null diff --git a/bot/resources/snake_cards/expressway.ttf b/bot/resources/snake_cards/expressway.ttf Binary files differdeleted file mode 100644 index 39e157947..000000000 --- a/bot/resources/snake_cards/expressway.ttf +++ /dev/null diff --git a/bot/resources/snakes_and_ladders/banner.jpg b/bot/resources/snakes_and_ladders/banner.jpg Binary files differdeleted file mode 100644 index 69eaaf129..000000000 --- a/bot/resources/snakes_and_ladders/banner.jpg +++ /dev/null diff --git a/bot/resources/snakes_and_ladders/board.jpg b/bot/resources/snakes_and_ladders/board.jpg Binary files differdeleted file mode 100644 index 20032e391..000000000 --- a/bot/resources/snakes_and_ladders/board.jpg +++ /dev/null diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 87351eaf3..4c99d50e8 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,84 +1,3 @@ -import asyncio -from typing import List - -import discord -from discord.ext.commands import BadArgument, Context - -from bot.pagination import LinePaginator - - -async def disambiguate( - ctx: Context, entries: List[str], *, timeout: float = 30, - per_page: int = 20, empty: bool = False, embed: discord.Embed = None -): - """ - Has the user choose between multiple entries in case one could not be chosen automatically. - - This will raise a BadArgument if entries is empty, if the disambiguation event times out, - or if the user makes an invalid choice. - - :param ctx: Context object from discord.py - :param entries: List of items for user to choose from - :param timeout: Number of seconds to wait before canceling disambiguation - :param per_page: Entries per embed page - :param empty: Whether the paginator should have an extra line between items - :param embed: The embed that the paginator will use. - :return: Users choice for correct entry. - """ - - if len(entries) == 0: - raise BadArgument('No matches found.') - - if len(entries) == 1: - return entries[0] - - choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1)) - - def check(message): - return (message.content.isdigit() - and message.author == ctx.author - and message.channel == ctx.channel) - - try: - if embed is None: - embed = discord.Embed() - - coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout) - coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=per_page, - empty=empty, max_size=6000, timeout=9000) - - # wait_for timeout will go to except instead of the wait_for thing as I expected - futures = [asyncio.ensure_future(coro1), asyncio.ensure_future(coro2)] - done, pending = await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED, loop=ctx.bot.loop) - - # :yert: - result = list(done)[0].result() - - # Pagination was canceled - result is None - if result is None: - for coro in pending: - coro.cancel() - raise BadArgument('Canceled.') - - # Pagination was not initiated, only one page - if result.author == ctx.bot.user: - # Continue the wait_for - result = await list(pending)[0] - - # Love that duplicate code - for coro in pending: - coro.cancel() - except asyncio.TimeoutError: - raise BadArgument('Timed out.') - - # Guaranteed to not error because of isdigit() in check - index = int(result.content) - - try: - return entries[index - 1] - except IndexError: - raise BadArgument('Invalid choice.') - class CaseInsensitiveDict(dict): """ diff --git a/bot/utils/snakes/__init__.py b/bot/utils/snakes/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/bot/utils/snakes/__init__.py +++ /dev/null diff --git a/bot/utils/snakes/hatching.py b/bot/utils/snakes/hatching.py deleted file mode 100644 index b9d29583f..000000000 --- a/bot/utils/snakes/hatching.py +++ /dev/null @@ -1,44 +0,0 @@ -h1 = r'''``` - ---- - ------ - /--------\ - |--------| - |--------| - \------/ - ----```''' - -h2 = r'''``` - ---- - ------ - /---\-/--\ - |-----\--| - |--------| - \------/ - ----```''' - -h3 = r'''``` - ---- - ------ - /---\-/--\ - |-----\--| - |-----/--| - \----\-/ - ----```''' - -h4 = r'''``` - ----- - ----- \ - /--| /---\ - |--\ -\---| - |--\--/-- / - \------- / - ------```''' - -stages = [h1, h2, h3, h4] -snakes = { - "Baby Python": "https://i.imgur.com/SYOcmSa.png", - "Baby Rattle Snake": "https://i.imgur.com/i5jYA8f.png", - "Baby Dragon Snake": "https://i.imgur.com/SuMKM4m.png", - "Baby Garden Snake": "https://i.imgur.com/5vYx3ah.png", - "Baby Cobra": "https://i.imgur.com/jk14ryt.png" -} diff --git a/bot/utils/snakes/perlin.py b/bot/utils/snakes/perlin.py deleted file mode 100644 index 0401787ef..000000000 --- a/bot/utils/snakes/perlin.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Perlin noise implementation. -Taken from: https://gist.github.com/eevee/26f547457522755cb1fb8739d0ea89a1 -Licensed under ISC -""" - -import math -import random -from itertools import product - - -def smoothstep(t): - """Smooth curve with a zero derivative at 0 and 1, making it useful for - interpolating. - """ - return t * t * (3. - 2. * t) - - -def lerp(t, a, b): - """Linear interpolation between a and b, given a fraction t.""" - return a + t * (b - a) - - -class PerlinNoiseFactory(object): - """Callable that produces Perlin noise for an arbitrary point in an - arbitrary number of dimensions. The underlying grid is aligned with the - integers. - There is no limit to the coordinates used; new gradients are generated on - the fly as necessary. - """ - - def __init__(self, dimension, octaves=1, tile=(), unbias=False): - """Create a new Perlin noise factory in the given number of dimensions, - which should be an integer and at least 1. - More octaves create a foggier and more-detailed noise pattern. More - than 4 octaves is rather excessive. - ``tile`` can be used to make a seamlessly tiling pattern. For example: - pnf = PerlinNoiseFactory(2, tile=(0, 3)) - This will produce noise that tiles every 3 units vertically, but never - tiles horizontally. - If ``unbias`` is true, the smoothstep function will be applied to the - output before returning it, to counteract some of Perlin noise's - significant bias towards the center of its output range. - """ - self.dimension = dimension - self.octaves = octaves - self.tile = tile + (0,) * dimension - self.unbias = unbias - - # For n dimensions, the range of Perlin noise is ±sqrt(n)/2; multiply - # by this to scale to ±1 - self.scale_factor = 2 * dimension ** -0.5 - - self.gradient = {} - - def _generate_gradient(self): - # Generate a random unit vector at each grid point -- this is the - # "gradient" vector, in that the grid tile slopes towards it - - # 1 dimension is special, since the only unit vector is trivial; - # instead, use a slope between -1 and 1 - if self.dimension == 1: - return (random.uniform(-1, 1),) - - # Generate a random point on the surface of the unit n-hypersphere; - # this is the same as a random unit vector in n dimensions. Thanks - # to: http://mathworld.wolfram.com/SpherePointPicking.html - # Pick n normal random variables with stddev 1 - random_point = [random.gauss(0, 1) for _ in range(self.dimension)] - # Then scale the result to a unit vector - scale = sum(n * n for n in random_point) ** -0.5 - return tuple(coord * scale for coord in random_point) - - def get_plain_noise(self, *point): - """Get plain noise for a single point, without taking into account - either octaves or tiling. - """ - if len(point) != self.dimension: - raise ValueError("Expected {0} values, got {1}".format( - self.dimension, len(point))) - - # Build a list of the (min, max) bounds in each dimension - grid_coords = [] - for coord in point: - min_coord = math.floor(coord) - max_coord = min_coord + 1 - grid_coords.append((min_coord, max_coord)) - - # Compute the dot product of each gradient vector and the point's - # distance from the corresponding grid point. This gives you each - # gradient's "influence" on the chosen point. - dots = [] - for grid_point in product(*grid_coords): - if grid_point not in self.gradient: - self.gradient[grid_point] = self._generate_gradient() - gradient = self.gradient[grid_point] - - dot = 0 - for i in range(self.dimension): - dot += gradient[i] * (point[i] - grid_point[i]) - dots.append(dot) - - # Interpolate all those dot products together. The interpolation is - # done with smoothstep to smooth out the slope as you pass from one - # grid cell into the next. - # Due to the way product() works, dot products are ordered such that - # the last dimension alternates: (..., min), (..., max), etc. So we - # can interpolate adjacent pairs to "collapse" that last dimension. Then - # the results will alternate in their second-to-last dimension, and so - # forth, until we only have a single value left. - dim = self.dimension - while len(dots) > 1: - dim -= 1 - s = smoothstep(point[dim] - grid_coords[dim][0]) - - next_dots = [] - while dots: - next_dots.append(lerp(s, dots.pop(0), dots.pop(0))) - - dots = next_dots - - return dots[0] * self.scale_factor - - def __call__(self, *point): - """Get the value of this Perlin noise function at the given point. The - number of values given should match the number of dimensions. - """ - ret = 0 - for o in range(self.octaves): - o2 = 1 << o - new_point = [] - for i, coord in enumerate(point): - coord *= o2 - if self.tile[i]: - coord %= self.tile[i] * o2 - new_point.append(coord) - ret += self.get_plain_noise(*new_point) / o2 - - # Need to scale n back down since adding all those extra octaves has - # probably expanded it beyond ±1 - # 1 octave: ±1 - # 2 octaves: ±1½ - # 3 octaves: ±1¾ - ret /= 2 - 2 ** (1 - self.octaves) - - if self.unbias: - # The output of the plain Perlin noise algorithm has a fairly - # strong bias towards the center due to the central limit theorem - # -- in fact the top and bottom 1/8 virtually never happen. That's - # a quarter of our entire output range! If only we had a function - # in [0..1] that could introduce a bias towards the endpoints... - r = (ret + 1) / 2 - # Doing it this many times is a completely made-up heuristic. - for _ in range(int(self.octaves / 2 + 0.5)): - r = smoothstep(r) - ret = r * 2 - 1 - - return ret diff --git a/bot/utils/snakes/perlinsneks.py b/bot/utils/snakes/perlinsneks.py deleted file mode 100644 index 662281775..000000000 --- a/bot/utils/snakes/perlinsneks.py +++ /dev/null @@ -1,111 +0,0 @@ -# perlin sneks! -import io -import math -import random -from typing import Tuple - -from PIL.ImageDraw import Image, ImageDraw - -from bot.utils.snakes import perlin - -DEFAULT_SNAKE_COLOR: int = 0x15c7ea -DEFAULT_BACKGROUND_COLOR: int = 0 -DEFAULT_IMAGE_DIMENSIONS: Tuple[int] = (200, 200) -DEFAULT_SNAKE_LENGTH: int = 22 -DEFAULT_SNAKE_WIDTH: int = 8 -DEFAULT_SEGMENT_LENGTH_RANGE: Tuple[int] = (7, 10) -DEFAULT_IMAGE_MARGINS: Tuple[int] = (50, 50) -DEFAULT_TEXT: str = "snek\nit\nup" -DEFAULT_TEXT_POSITION: Tuple[int] = ( - 10, - 10 -) -DEFAULT_TEXT_COLOR: int = 0xf2ea15 - -X = 0 -Y = 1 -ANGLE_RANGE = math.pi * 2 - - -def create_snek_frame( - perlin_factory: perlin.PerlinNoiseFactory, perlin_lookup_vertical_shift: float = 0, - image_dimensions: Tuple[int] = DEFAULT_IMAGE_DIMENSIONS, image_margins: Tuple[int] = DEFAULT_IMAGE_MARGINS, - snake_length: int = DEFAULT_SNAKE_LENGTH, - snake_color: int = DEFAULT_SNAKE_COLOR, bg_color: int = DEFAULT_BACKGROUND_COLOR, - segment_length_range: Tuple[int] = DEFAULT_SEGMENT_LENGTH_RANGE, snake_width: int = DEFAULT_SNAKE_WIDTH, - text: str = DEFAULT_TEXT, text_position: Tuple[int] = DEFAULT_TEXT_POSITION, - text_color: Tuple[int] = DEFAULT_TEXT_COLOR -) -> Image: - """ - Creates a single random snek frame using Perlin noise. - :param perlin_factory: the perlin noise factory used. Required. - :param perlin_lookup_vertical_shift: the Perlin noise shift in the Y-dimension for this frame - :param image_dimensions: the size of the output image. - :param image_margins: the margins to respect inside of the image. - :param snake_length: the length of the snake, in segments. - :param snake_color: the color of the snake. - :param bg_color: the background color. - :param segment_length_range: the range of the segment length. Values will be generated inside this range, including - the bounds. - :param snake_width: the width of the snek, in pixels. - :param text: the text to display with the snek. Set to None for no text. - :param text_position: the position of the text. - :param text_color: the color of the text. - :return: a PIL image, representing a single frame. - """ - start_x = random.randint(image_margins[X], image_dimensions[X] - image_margins[X]) - start_y = random.randint(image_margins[Y], image_dimensions[Y] - image_margins[Y]) - points = [(start_x, start_y)] - - for index in range(0, snake_length): - angle = perlin_factory.get_plain_noise( - ((1 / (snake_length + 1)) * (index + 1)) + perlin_lookup_vertical_shift - ) * ANGLE_RANGE - current_point = points[index] - segment_length = random.randint(segment_length_range[0], segment_length_range[1]) - points.append(( - current_point[X] + segment_length * math.cos(angle), - current_point[Y] + segment_length * math.sin(angle) - )) - - # normalize bounds - min_dimensions = [start_x, start_y] - max_dimensions = [start_x, start_y] - for point in points: - min_dimensions[X] = min(point[X], min_dimensions[X]) - min_dimensions[Y] = min(point[Y], min_dimensions[Y]) - max_dimensions[X] = max(point[X], max_dimensions[X]) - max_dimensions[Y] = max(point[Y], max_dimensions[Y]) - - # shift towards middle - dimension_range = (max_dimensions[X] - min_dimensions[X], max_dimensions[Y] - min_dimensions[Y]) - shift = ( - image_dimensions[X] / 2 - (dimension_range[X] / 2 + min_dimensions[X]), - image_dimensions[Y] / 2 - (dimension_range[Y] / 2 + min_dimensions[Y]) - ) - - image = Image.new(mode='RGB', size=image_dimensions, color=bg_color) - draw = ImageDraw(image) - for index in range(1, len(points)): - point = points[index] - previous = points[index - 1] - draw.line( - ( - shift[X] + previous[X], - shift[Y] + previous[Y], - shift[X] + point[X], - shift[Y] + point[Y] - ), - width=snake_width, - fill=snake_color - ) - if text is not None: - draw.multiline_text(text_position, text, fill=text_color) - del draw - return image - - -def frame_to_png_bytes(image: Image): - stream = io.BytesIO() - image.save(stream, format='PNG') - return stream.getvalue() diff --git a/bot/utils/snakes/sal.py b/bot/utils/snakes/sal.py deleted file mode 100644 index 8530d8a0f..000000000 --- a/bot/utils/snakes/sal.py +++ /dev/null @@ -1,365 +0,0 @@ -import asyncio -import io -import logging -import math -import os -import random - -import aiohttp -from discord import File, Member, Reaction -from discord.ext.commands import Context -from PIL import Image - -from bot.utils.snakes.sal_board import ( - BOARD, BOARD_MARGIN, BOARD_PLAYER_SIZE, - BOARD_TILE_SIZE, MAX_PLAYERS, PLAYER_ICON_IMAGE_SIZE -) - -log = logging.getLogger(__name__) - -# Emoji constants -START_EMOJI = "\u2611" # :ballot_box_with_check: - Start the game -CANCEL_EMOJI = "\u274C" # :x: - Cancel or leave the game -ROLL_EMOJI = "\U0001F3B2" # :game_die: - Roll the die! -JOIN_EMOJI = "\U0001F64B" # :raising_hand: - Join the game. - -STARTUP_SCREEN_EMOJI = [ - JOIN_EMOJI, - START_EMOJI, - CANCEL_EMOJI -] - -GAME_SCREEN_EMOJI = [ - ROLL_EMOJI, - CANCEL_EMOJI -] - - -class SnakeAndLaddersGame: - def __init__(self, snakes, context: Context): - self.snakes = snakes - self.ctx = context - self.channel = self.ctx.channel - self.state = 'booting' - self.started = False - self.author = self.ctx.author - self.players = [] - self.player_tiles = {} - self.round_has_rolled = {} - self.avatar_images = {} - self.board = None - self.positions = None - self.rolls = [] - - async def open_game(self): - """ - Create a new Snakes and Ladders game. - - Listen for reactions until players have joined, - and the game has been started. - """ - - def startup_event_check(reaction_: Reaction, user_: Member): - """ - Make sure that this reaction is what we want to operate on - """ - return ( - all(( - reaction_.message.id == startup.id, # Reaction is on startup message - reaction_.emoji in STARTUP_SCREEN_EMOJI, # Reaction is one of the startup emotes - user_.id != self.ctx.bot.user.id, # Reaction was not made by the bot - )) - ) - - # Check to see if the bot can remove reactions - if not self.channel.permissions_for(self.ctx.guild.me).manage_messages: - log.warning( - "Unable to start Snakes and Ladders - " - f"Missing manage_messages permissions in {self.channel}" - ) - return - - await self._add_player(self.author) - await self.channel.send( - "**Snakes and Ladders**: A new game is about to start!", - file=File( - os.path.join("bot", "resources", "snakes_and_ladders", "banner.jpg"), - filename='Snakes and Ladders.jpg' - ) - ) - startup = await self.channel.send( - f"Press {JOIN_EMOJI} to participate, and press " - f"{START_EMOJI} to start the game" - ) - for emoji in STARTUP_SCREEN_EMOJI: - await startup.add_reaction(emoji) - - self.state = 'waiting' - - while not self.started: - try: - reaction, user = await self.ctx.bot.wait_for( - "reaction_add", - timeout=300, - check=startup_event_check - ) - if reaction.emoji == JOIN_EMOJI: - await self.player_join(user) - elif reaction.emoji == CANCEL_EMOJI: - if self.ctx.author == user: - await self.cancel_game(user) - return - else: - await self.player_leave(user) - elif reaction.emoji == START_EMOJI: - if self.ctx.author == user: - self.started = True - await self.start_game(user) - await startup.delete() - break - - await startup.remove_reaction(reaction.emoji, user) - - except asyncio.TimeoutError: - log.debug("Snakes and Ladders timed out waiting for a reaction") - self.cancel_game(self.author) - return # We're done, no reactions for the last 5 minutes - - async def _add_player(self, user: Member): - self.players.append(user) - self.player_tiles[user.id] = 1 - avatar_url = user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE) - async with aiohttp.ClientSession() as session: - async with session.get(avatar_url) as res: - avatar_bytes = await res.read() - im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE)) - self.avatar_images[user.id] = im - - async def player_join(self, user: Member): - for p in self.players: - if user == p: - await self.channel.send(user.mention + " You are already in the game.", delete_after=10) - return - if self.state != 'waiting': - await self.channel.send(user.mention + " You cannot join at this time.", delete_after=10) - return - if len(self.players) is MAX_PLAYERS: - await self.channel.send(user.mention + " The game is full!", delete_after=10) - return - - await self._add_player(user) - - await self.channel.send( - f"**Snakes and Ladders**: {user.mention} has joined the game.\n" - f"There are now {str(len(self.players))} players in the game.", - delete_after=10 - ) - - async def player_leave(self, user: Member): - if user == self.author: - await self.channel.send( - user.mention + " You are the author, and cannot leave the game. Execute " - "`sal cancel` to cancel the game.", - delete_after=10 - ) - return - for p in self.players: - if user == p: - self.players.remove(p) - self.player_tiles.pop(p.id, None) - self.round_has_rolled.pop(p.id, None) - await self.channel.send( - "**Snakes and Ladders**: " + user.mention + " has left the game.", - delete_after=10 - ) - - if self.state != 'waiting' and len(self.players) == 1: - await self.channel.send("**Snakes and Ladders**: The game has been surrendered!") - self._destruct() - return - await self.channel.send(user.mention + " You are not in the match.", delete_after=10) - - async def cancel_game(self, user: Member): - if not user == self.author: - await self.channel.send(user.mention + " Only the author of the game can cancel it.", delete_after=10) - return - await self.channel.send("**Snakes and Ladders**: Game has been canceled.") - self._destruct() - - async def start_game(self, user: Member): - if not user == self.author: - await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10) - return - if len(self.players) < 1: - await self.channel.send( - user.mention + " A minimum of 2 players is required to start the game.", - delete_after=10 - ) - return - if not self.state == 'waiting': - await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10) - return - self.state = 'starting' - player_list = ', '.join(user.mention for user in self.players) - await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list) - await self.start_round() - - async def start_round(self): - - def game_event_check(reaction_: Reaction, user_: Member): - """ - Make sure that this reaction is what we want to operate on - """ - return ( - all(( - reaction_.message.id == self.positions.id, # Reaction is on positions message - reaction_.emoji in GAME_SCREEN_EMOJI, # Reaction is one of the game emotes - user_.id != self.ctx.bot.user.id, # Reaction was not made by the bot - )) - ) - - self.state = 'roll' - for user in self.players: - self.round_has_rolled[user.id] = False - board_img = Image.open(os.path.join("bot", "resources", "snakes_and_ladders", "board.jpg")) - player_row_size = math.ceil(MAX_PLAYERS / 2) - - for i, player in enumerate(self.players): - tile = self.player_tiles[player.id] - tile_coordinates = self._board_coordinate_from_index(tile) - x_offset = BOARD_MARGIN[0] + tile_coordinates[0] * BOARD_TILE_SIZE - y_offset = \ - BOARD_MARGIN[1] + ( - (10 * BOARD_TILE_SIZE) - (9 - tile_coordinates[1]) * BOARD_TILE_SIZE - BOARD_PLAYER_SIZE) - x_offset += BOARD_PLAYER_SIZE * (i % player_row_size) - y_offset -= BOARD_PLAYER_SIZE * math.floor(i / player_row_size) - board_img.paste(self.avatar_images[player.id], - box=(x_offset, y_offset)) - stream = io.BytesIO() - board_img.save(stream, format='JPEG') - board_file = File(stream.getvalue(), filename='Board.jpg') - player_list = '\n'.join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players) - - # Store and send new messages - temp_board = await self.channel.send( - "**Snakes and Ladders**: A new round has started! Current board:", - file=board_file - ) - temp_positions = await self.channel.send( - f"**Current positions**:\n{player_list}\n\nUse {ROLL_EMOJI} to roll the dice!" - ) - - # Delete the previous messages - if self.board and self.positions: - await self.board.delete() - await self.positions.delete() - - # remove the roll messages - for roll in self.rolls: - await roll.delete() - self.rolls = [] - - # Save new messages - self.board = temp_board - self.positions = temp_positions - - # Wait for rolls - for emoji in GAME_SCREEN_EMOJI: - await self.positions.add_reaction(emoji) - - while True: - try: - reaction, user = await self.ctx.bot.wait_for( - "reaction_add", - timeout=300, - check=game_event_check - ) - - if reaction.emoji == ROLL_EMOJI: - await self.player_roll(user) - elif reaction.emoji == CANCEL_EMOJI: - if self.ctx.author == user: - await self.cancel_game(user) - return - else: - await self.player_leave(user) - - await self.positions.remove_reaction(reaction.emoji, user) - - if self._check_all_rolled(): - break - - except asyncio.TimeoutError: - log.debug("Snakes and Ladders timed out waiting for a reaction") - await self.cancel_game(self.author) - return # We're done, no reactions for the last 5 minutes - - # Round completed - await self._complete_round() - - async def player_roll(self, user: Member): - if user.id not in self.player_tiles: - await self.channel.send(user.mention + " You are not in the match.", delete_after=10) - return - if self.state != 'roll': - await self.channel.send(user.mention + " You may not roll at this time.", delete_after=10) - return - if self.round_has_rolled[user.id]: - return - roll = random.randint(1, 6) - self.rolls.append(await self.channel.send(f"{user.mention} rolled a **{roll}**!")) - next_tile = self.player_tiles[user.id] + roll - - # apply snakes and ladders - if next_tile in BOARD: - target = BOARD[next_tile] - if target < next_tile: - await self.channel.send( - f"{user.mention} slips on a snake and falls back to **{target}**", - delete_after=15 - ) - else: - await self.channel.send( - f"{user.mention} climbs a ladder to **{target}**", - delete_after=15 - ) - next_tile = target - - self.player_tiles[user.id] = min(100, next_tile) - self.round_has_rolled[user.id] = True - - async def _complete_round(self): - - self.state = 'post_round' - - # check for winner - winner = self._check_winner() - if winner is None: - # there is no winner, start the next round - await self.start_round() - return - - # announce winner and exit - await self.channel.send("**Snakes and Ladders**: " + winner.mention + " has won the game! :tada:") - self._destruct() - - def _check_winner(self) -> Member: - if self.state != 'post_round': - return None - return next((player for player in self.players if self.player_tiles[player.id] == 100), - None) - - def _check_all_rolled(self): - return all(rolled for rolled in self.round_has_rolled.values()) - - def _destruct(self): - del self.snakes.active_sal[self.channel] - - def _board_coordinate_from_index(self, index: int): - # converts the tile number to the x/y coordinates for graphical purposes - y_level = 9 - math.floor((index - 1) / 10) - is_reversed = math.floor((index - 1) / 10) % 2 != 0 - x_level = (index - 1) % 10 - if is_reversed: - x_level = 9 - x_level - return x_level, y_level diff --git a/bot/utils/snakes/sal_board.py b/bot/utils/snakes/sal_board.py deleted file mode 100644 index 1b8eab451..000000000 --- a/bot/utils/snakes/sal_board.py +++ /dev/null @@ -1,33 +0,0 @@ -BOARD_TILE_SIZE = 56 # the size of each board tile -BOARD_PLAYER_SIZE = 20 # the size of each player icon -BOARD_MARGIN = (10, 0) # margins, in pixels (for player icons) -PLAYER_ICON_IMAGE_SIZE = 32 # the size of the image to download, should a power of 2 and higher than BOARD_PLAYER_SIZE -MAX_PLAYERS = 4 # depends on the board size/quality, 4 is for the default board - -# board definition (from, to) -BOARD = { - # ladders - 2: 38, - 7: 14, - 8: 31, - 15: 26, - 21: 42, - 28: 84, - 36: 44, - 51: 67, - 71: 91, - 78: 98, - 87: 94, - - # snakes - 99: 80, - 95: 75, - 92: 88, - 89: 68, - 74: 53, - 64: 60, - 62: 19, - 49: 11, - 46: 25, - 16: 6 -} diff --git a/config-default.yml b/config-default.yml index 50bea189b..e5681dc3d 100644 --- a/config-default.yml +++ b/config-default.yml @@ -222,9 +222,7 @@ filter: keys: deploy_bot: !ENV "DEPLOY_BOT_KEY" deploy_site: !ENV "DEPLOY_SITE" - omdb: !ENV "OMDB_API_KEY" site_api: !ENV "BOT_API_KEY" - youtube: !ENV "YOUTUBE_API_KEY" rabbitmq: @@ -243,9 +241,7 @@ urls: site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] - site_facts_api: !JOIN [*SCHEMA, *API, "/bot/snake_facts"] site_superstarify_api: !JOIN [*SCHEMA, *API, "/bot/superstarify"] - site_idioms_api: !JOIN [*SCHEMA, *API, "/bot/snake_idioms"] site_infractions: !JOIN [*SCHEMA, *API, "/bot/infractions"] site_infractions_user: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}"] site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"] @@ -254,13 +250,10 @@ urls: site_infractions_user_type: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"] site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"] site_logs_view: !JOIN [*SCHEMA, *DOMAIN, "/bot/logs"] - site_names_api: !JOIN [*SCHEMA, *API, "/bot/snake_names"] site_off_topic_names_api: !JOIN [*SCHEMA, *API, "/bot/off-topic-names"] - site_quiz_api: !JOIN [*SCHEMA, *API, "/bot/snake_quiz"] site_reminders_api: !JOIN [*SCHEMA, *API, "/bot/reminders"] site_reminders_user_api: !JOIN [*SCHEMA, *API, "/bot/reminders/user"] site_settings_api: !JOIN [*SCHEMA, *API, "/bot/settings"] - site_special_api: !JOIN [*SCHEMA, *API, "/bot/special_snakes"] site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] @@ -277,7 +270,6 @@ urls: # Misc URLs bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot" - omdb: "http://omdbapi.com" anti_spam: # Clean messages that violate a rule. |