diff options
Diffstat (limited to '')
22 files changed, 724 insertions, 264 deletions
| @@ -9,7 +9,7 @@ ENV PIP_NO_CACHE_DIR=false \  # Install git to be able to dowload git dependencies in the Pipfile  RUN apt-get -y update \      && apt-get install -y \ -        git \ +        ffmpeg \      && rm -rf /var/lib/apt/lists/*  # Install pipenv @@ -7,12 +7,12 @@ name = "pypi"  aiodns = "~=2.0"  arrow = "~=0.14"  beautifulsoup4 = "~=4.8" -discord-py = {git = "https://github.com/Rapptz/discord.py.git",ref = "0bc15fa130b8f01fe2d67446a2184d474b0d0ba7"}  fuzzywuzzy = "~=0.17" -pillow = "~=6.2" +pillow = "~=7.2"  pytz = "~=2019.2"  sentry-sdk = "~=0.14.2"  PyYAML = "~=5.3.1" +"discord.py" = {extras = ["voice"], version = "~=1.4.1"}  [dev-packages]  flake8 = "~=3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 74931967..6e6a3c2e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "dfd24795dcdab1a05fd774b5034b195b69d6d6ed52fd6810db9c89247f8e0a43" +            "sha256": "1077d14c4a0456f57062e91e403a107d6321a385ea2bc2e8c833e0b6c22801e4"          },          "pipfile-spec": 6,          "requires": { @@ -44,11 +44,11 @@          },          "arrow": {              "hashes": [ -                "sha256:271b8e05174d48e50324ed0dc5d74796c839c7e579a4f21cf1a7394665f9e94f", -                "sha256:edc31dc051db12c95da9bac0271cd1027b8e36912daf6d4580af53b23e62721a" +                "sha256:92aac856ea5175c804f7ccb96aca4d714d936f1c867ba59d747a8096ec30e90a", +                "sha256:98184d8dd3e5d30b96c2df4596526f7de679ccb467f358b82b0f686436f3a6b8"              ],              "index": "pypi", -            "version": "==0.15.8" +            "version": "==0.16.0"          },          "async-timeout": {              "hashes": [ @@ -60,11 +60,11 @@          },          "attrs": {              "hashes": [ -                "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", -                "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" +                "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", +                "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==19.3.0" +            "version": "==20.2.0"          },          "beautifulsoup4": {              "hashes": [ @@ -84,36 +84,44 @@          },          "cffi": {              "hashes": [ -                "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", -                "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", -                "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", -                "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", -                "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", -                "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", -                "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", -                "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", -                "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", -                "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", -                "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", -                "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", -                "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", -                "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", -                "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", -                "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", -                "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", -                "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", -                "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", -                "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", -                "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", -                "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", -                "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", -                "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", -                "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", -                "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", -                "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", -                "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" -            ], -            "version": "==1.14.2" +                "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", +                "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", +                "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", +                "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", +                "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", +                "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", +                "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", +                "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", +                "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", +                "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", +                "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", +                "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", +                "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", +                "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", +                "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", +                "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", +                "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", +                "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", +                "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", +                "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", +                "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", +                "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", +                "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", +                "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", +                "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", +                "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", +                "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", +                "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", +                "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", +                "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", +                "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", +                "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", +                "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", +                "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", +                "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", +                "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" +            ], +            "version": "==1.14.3"          },          "chardet": {              "hashes": [ @@ -122,9 +130,16 @@              ],              "version": "==3.0.4"          }, -        "discord-py": { -            "git": "https://github.com/Rapptz/discord.py.git", -            "ref": "0bc15fa130b8f01fe2d67446a2184d474b0d0ba7" +        "discord.py": { +            "extras": [ +                "voice" +            ], +            "hashes": [ +                "sha256:98ea3096a3585c9c379209926f530808f5fcf4930928d8cfb579d2562d119570", +                "sha256:f9decb3bfa94613d922376288617e6a6f969260923643e2897f4540c34793442" +            ], +            "index": "pypi", +            "version": "==1.4.1"          },          "fuzzywuzzy": {              "hashes": [ @@ -167,39 +182,37 @@          },          "pillow": {              "hashes": [ -                "sha256:00e0bbe9923adc5cc38a8da7d87d4ce16cde53b8d3bba8886cb928e84522d963", -                "sha256:03457e439d073770d88afdd90318382084732a5b98b0eb6f49454746dbaae701", -                "sha256:0d5c99f80068f13231ac206bd9b2e80ea357f5cf9ae0fa97fab21e32d5b61065", -                "sha256:1a3bc8e1db5af40a81535a62a591fafdb30a8a1b319798ea8052aa65ef8f06d2", -                "sha256:2b4a94be53dff02af90760c10a2e3634c3c7703410f38c98154d5ce71fe63d20", -                "sha256:3ba7d8f1d962780f86aa747fef0baf3211b80cb13310fff0c375da879c0656d4", -                "sha256:3e81485cec47c24f5fb27acb485a4fc97376b2b332ed633867dc68ac3077998c", -                "sha256:43ef1cff7ee57f9c8c8e6fa02a62eae9fa23a7e34418c7ce88c0e3fe09d1fb38", -                "sha256:4adc3302df4faf77c63ab3a83e1a3e34b94a6a992084f4aa1cb236d1deaf4b39", -                "sha256:535e8e0e02c9f1fc2e307256149d6ee8ad3aa9a6e24144b7b6e6fb6126cb0e99", -                "sha256:5ccfcb0a34ad9b77ad247c231edb781763198f405a5c8dc1b642449af821fb7f", -                "sha256:5dcbbaa3a24d091a64560d3c439a8962866a79a033d40eb1a75f1b3413bfc2bc", -                "sha256:6e2a7e74d1a626b817ecb7a28c433b471a395c010b2a1f511f976e9ea4363e64", -                "sha256:82859575005408af81b3e9171ae326ff56a69af5439d3fc20e8cb76cd51c8246", -                "sha256:834dd023b7f987d6b700ad93dc818098d7eb046bd445e9992b3093c6f9d7a95f", -                "sha256:87ef0eca169f7f0bc050b22f05c7e174a65c36d584428431e802c0165c5856ea", -                "sha256:900de1fdc93764be13f6b39dc0dd0207d9ff441d87ad7c6e97e49b81987dc0f3", -                "sha256:92b83b380f9181cacc994f4c983d95a9c8b00b50bf786c66d235716b526a3332", -                "sha256:aa1b0297e352007ec781a33f026afbb062a9a9895bb103c8f49af434b1666880", -                "sha256:aa4792ab056f51b49e7d59ce5733155e10a918baf8ce50f64405db23d5627fa2", -                "sha256:b72c39585f1837d946bd1a829a4820ccf86e361f28cbf60f5d646f06318b61e2", -                "sha256:bb7861e4618a0c06c40a2e509c1bea207eea5fd4320d486e314e00745a402ca5", -                "sha256:bc149dab804291a18e1186536519e5e122a2ac1316cb80f506e855a500b1cdd4", -                "sha256:c424d35a5259be559b64490d0fd9e03fba81f1ce8e5b66e0a59de97547351d80", -                "sha256:cbd5647097dc55e501f459dbac7f1d0402225636deeb9e0a98a8d2df649fc19d", -                "sha256:ccf16fe444cc43800eeacd4f4769971200982200a71b1368f49410d0eb769543", -                "sha256:d3a98444a00b4643b22b0685dbf9e0ddcaf4ebfd4ea23f84f228adf5a0765bb2", -                "sha256:d6b4dc325170bee04ca8292bbd556c6f5398d52c6149ca881e67daf62215426f", -                "sha256:db9ff0c251ed066d367f53b64827cc9e18ccea001b986d08c265e53625dab950", -                "sha256:e3a797a079ce289e59dbd7eac9ca3bf682d52687f718686857281475b7ca8e6a" +                "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", +                "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", +                "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", +                "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", +                "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", +                "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", +                "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", +                "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", +                "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", +                "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", +                "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", +                "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", +                "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", +                "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", +                "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", +                "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", +                "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", +                "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", +                "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", +                "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", +                "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", +                "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", +                "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", +                "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", +                "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", +                "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", +                "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", +                "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"              ],              "index": "pypi", -            "version": "==6.2.2" +            "version": "==7.2.0"          },          "pycares": {              "hashes": [ @@ -243,12 +256,38 @@              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",              "version": "==2.20"          }, +        "pynacl": { +            "hashes": [ +                "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", +                "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", +                "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", +                "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", +                "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", +                "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", +                "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", +                "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", +                "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", +                "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", +                "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", +                "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", +                "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", +                "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", +                "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", +                "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", +                "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", +                "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", +                "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", +                "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", +                "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" +            ], +            "version": "==1.3.0" +        },          "python-dateutil": {              "hashes": [                  "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",                  "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"              ], -            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",              "version": "==2.8.1"          },          "pytz": { @@ -289,7 +328,7 @@                  "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",                  "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"              ], -            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",              "version": "==1.15.0"          },          "soupsieve": { @@ -308,56 +347,28 @@              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",              "version": "==1.25.10"          }, -        "websockets": { -            "hashes": [ -                "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", -                "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", -                "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", -                "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", -                "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", -                "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", -                "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", -                "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", -                "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", -                "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", -                "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", -                "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", -                "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", -                "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", -                "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", -                "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", -                "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", -                "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", -                "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", -                "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", -                "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", -                "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" -            ], -            "markers": "python_full_version >= '3.6.1'", -            "version": "==8.1" -        },          "yarl": {              "hashes": [ -                "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", -                "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", -                "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", -                "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", -                "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", -                "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", -                "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", -                "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", -                "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", -                "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", -                "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", -                "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", -                "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", -                "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", -                "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", -                "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", -                "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" +                "sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e", +                "sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5", +                "sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580", +                "sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc", +                "sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b", +                "sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2", +                "sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a", +                "sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921", +                "sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e", +                "sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1", +                "sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d", +                "sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131", +                "sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a", +                "sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1", +                "sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188", +                "sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020", +                "sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"              ],              "markers": "python_version >= '3.5'", -            "version": "==1.5.1" +            "version": "==1.6.0"          }      },      "develop": { @@ -370,11 +381,11 @@          },          "attrs": {              "hashes": [ -                "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", -                "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" +                "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", +                "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==19.3.0" +            "version": "==20.2.0"          },          "cfgv": {              "hashes": [ @@ -408,11 +419,11 @@          },          "flake8-annotations": {              "hashes": [ -                "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26", -                "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414" +                "sha256:09fe1aa3f40cb8fef632a0ab3614050a7584bb884b6134e70cf1fc9eeee642fa", +                "sha256:5bda552f074fd6e34276c7761756fa07d824ffac91ce9c0a8555eb2bc5b92d7a"              ],              "index": "pypi", -            "version": "==2.3.0" +            "version": "==2.4.0"          },          "flake8-bugbear": {              "hashes": [ @@ -470,11 +481,11 @@          },          "identify": {              "hashes": [ -                "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", -                "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7" +                "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4", +                "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==1.4.28" +            "version": "==1.5.5"          },          "mccabe": {              "hashes": [ @@ -485,9 +496,10 @@          },          "nodeenv": {              "hashes": [ -                "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" +                "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", +                "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"              ], -            "version": "==1.4.0" +            "version": "==1.5.0"          },          "pep8-naming": {              "hashes": [ @@ -499,11 +511,11 @@          },          "pre-commit": {              "hashes": [ -                "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", -                "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" +                "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", +                "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"              ],              "index": "pypi", -            "version": "==2.6.0" +            "version": "==2.7.1"          },          "pycodestyle": {              "hashes": [ @@ -515,11 +527,11 @@          },          "pydocstyle": {              "hashes": [ -                "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", -                "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" +                "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", +                "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"              ],              "markers": "python_version >= '3.5'", -            "version": "==5.0.2" +            "version": "==5.1.1"          },          "pyflakes": {              "hashes": [ @@ -551,7 +563,7 @@                  "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",                  "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"              ], -            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",              "version": "==1.15.0"          },          "snowballstemmer": { @@ -570,11 +582,11 @@          },          "virtualenv": {              "hashes": [ -                "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5", -                "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e" +                "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", +                "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.0.30" +            "version": "==20.0.31"          }      }  } diff --git a/bot/__main__.py b/bot/__main__.py index 0ffd6143..cd2d43a9 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -5,8 +5,9 @@ from sentry_sdk.integrations.logging import LoggingIntegration  from bot.bot import bot  from bot.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS -from bot.exts import walk_extensions  from bot.utils.decorators import in_channel_check +from bot.utils.extensions import walk_extensions +  sentry_logging = LoggingIntegration(      level=logging.DEBUG, diff --git a/bot/constants.py b/bot/constants.py index 7c8f72cb..7ec8ac27 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -11,7 +11,6 @@ __all__ = (      "Client",      "Colours",      "Emojis", -    "Hacktoberfest",      "Icons",      "Lovefest",      "Month", @@ -75,7 +74,7 @@ class Channels(NamedTuple):      python_discussion = 267624335836053506      show_your_projects = int(environ.get("CHANNEL_SHOW_YOUR_PROJECTS", 303934982764625920))      show_your_projects_discussion = 360148304664723466 -    hacktoberfest_2019 = 628184417646411776 +    hacktoberfest_2020 = 760857070781071431  class Client(NamedTuple): @@ -84,6 +83,7 @@ class Client(NamedTuple):      token = environ.get("SEASONALBOT_TOKEN")      sentry_dsn = environ.get("SEASONALBOT_SENTRY_DSN")      debug = environ.get("SEASONALBOT_DEBUG", "").lower() == "true" +    github_bot_repo = "https://github.com/python-discord/seasonalbot"      # Override seasonal locks: 1 (January) to 12 (December)      month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None @@ -122,9 +122,10 @@ class Emojis:      pull_request_closed = "<:PRClosed:629695470519713818>"      merge = "<:PRMerged:629695470570176522>" - -class Hacktoberfest(NamedTuple): -    voice_id = 514420006474219521 +    status_online = "<:status_online:470326272351010816>" +    status_idle = "<:status_idle:470326266625785866>" +    status_dnd = "<:status_dnd:470326272082313216>" +    status_offline = "<:status_offline:470326266537705472>"  class Icons: @@ -177,6 +178,7 @@ class Roles(NamedTuple):      verified = 352427296948486144      helpers = 267630620367257601      rockstars = 458226413825294336 +    core_developers = 587606783669829632  class Tokens(NamedTuple): diff --git a/bot/exts/__init__.py b/bot/exts/__init__.py index 25deb9af..13f484ac 100644 --- a/bot/exts/__init__.py +++ b/bot/exts/__init__.py @@ -1,9 +1,8 @@  import logging  import pkgutil -from pathlib import Path  from typing import Iterator -__all__ = ("get_package_names", "walk_extensions") +__all__ = ("get_package_names",)  log = logging.getLogger(__name__) @@ -13,23 +12,3 @@ def get_package_names() -> Iterator[str]:      for package in pkgutil.iter_modules(__path__):          if package.ispkg:              yield package.name - - -def walk_extensions() -> Iterator[str]: -    """ -    Iterate dot-separated paths to all extensions. - -    The strings are formatted in a way such that the bot's `load_extension` -    method can take them. Use this to load all available extensions. - -    This intentionally doesn't make use of pkgutil's `walk_packages`, as we only -    want to build paths to extensions - not recursively all modules. For some -    extensions, the `setup` function is in the package's __init__ file, while -    modules nested under the package are only helpers. Constructing the paths -    ourselves serves our purpose better. -    """ -    base_path = Path(__path__[0]) - -    for package in get_package_names(): -        for extension in pkgutil.iter_modules([base_path.joinpath(package)]): -            yield f"bot.exts.{package}.{extension.name}" diff --git a/bot/exts/evergreen/8bitify.py b/bot/exts/evergreen/8bitify.py index 60062fc1..c048d9bf 100644 --- a/bot/exts/evergreen/8bitify.py +++ b/bot/exts/evergreen/8bitify.py @@ -14,7 +14,7 @@ class EightBitify(commands.Cog):      @staticmethod      def pixelate(image: Image) -> Image:          """Takes an image and pixelates it.""" -        return image.resize((32, 32)).resize((1024, 1024)) +        return image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST)      @staticmethod      def quantize(image: Image) -> Image: diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py index 73908702..5fa05d2e 100644 --- a/bot/exts/evergreen/bookmark.py +++ b/bot/exts/evergreen/bookmark.py @@ -5,6 +5,7 @@ import discord  from discord.ext import commands  from bot.constants import Colours, ERROR_REPLIES, Emojis, Icons +from bot.utils.converters import WrappedMessageConverter  log = logging.getLogger(__name__) @@ -19,7 +20,7 @@ class Bookmark(commands.Cog):      async def bookmark(          self,          ctx: commands.Context, -        target_message: discord.Message, +        target_message: WrappedMessageConverter,          *,          title: str = "Bookmark"      ) -> None: diff --git a/bot/exts/evergreen/snakes/__init__.py b/bot/exts/evergreen/snakes/__init__.py index 2eae2751..bc42f0c2 100644 --- a/bot/exts/evergreen/snakes/__init__.py +++ b/bot/exts/evergreen/snakes/__init__.py @@ -2,7 +2,7 @@ import logging  from discord.ext import commands -from bot.exts.evergreen.snakes.snakes_cog import Snakes +from bot.exts.evergreen.snakes._snakes_cog import Snakes  log = logging.getLogger(__name__) diff --git a/bot/exts/evergreen/snakes/converter.py b/bot/exts/evergreen/snakes/_converter.py index 55609b8e..eee248cf 100644 --- a/bot/exts/evergreen/snakes/converter.py +++ b/bot/exts/evergreen/snakes/_converter.py @@ -7,7 +7,7 @@ import discord  from discord.ext.commands import Context, Converter  from fuzzywuzzy import fuzz -from bot.exts.evergreen.snakes.utils import SNAKE_RESOURCES +from bot.exts.evergreen.snakes._utils import SNAKE_RESOURCES  from bot.utils import disambiguate  log = logging.getLogger(__name__) diff --git a/bot/exts/evergreen/snakes/snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py index 9bbad9fe..a846274b 100644 --- a/bot/exts/evergreen/snakes/snakes_cog.py +++ b/bot/exts/evergreen/snakes/_snakes_cog.py @@ -18,8 +18,8 @@ from discord import Colour, Embed, File, Member, Message, Reaction  from discord.ext.commands import BadArgument, Bot, Cog, CommandError, Context, bot_has_permissions, group  from bot.constants import ERROR_REPLIES, Tokens -from bot.exts.evergreen.snakes import utils -from bot.exts.evergreen.snakes.converter import Snake +from bot.exts.evergreen.snakes import _utils as utils +from bot.exts.evergreen.snakes._converter import Snake  from bot.utils.decorators import locked  log = logging.getLogger(__name__) diff --git a/bot/exts/evergreen/snakes/utils.py b/bot/exts/evergreen/snakes/_utils.py index 7d6caf04..7d6caf04 100644 --- a/bot/exts/evergreen/snakes/utils.py +++ b/bot/exts/evergreen/snakes/_utils.py diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py index c1fff873..be36e2c4 100644 --- a/bot/exts/evergreen/wikipedia.py +++ b/bot/exts/evergreen/wikipedia.py @@ -1,8 +1,9 @@  import asyncio  import datetime  import logging -from typing import List +from typing import List, Optional +from aiohttp import client_exceptions  from discord import Color, Embed, Message  from discord.ext import commands @@ -14,7 +15,7 @@ SEARCH_API = "https://en.wikipedia.org/w/api.php?action=query&list=search&srsear  WIKIPEDIA_URL = "https://en.wikipedia.org/wiki/{title}" -class WikipediaCog(commands.Cog): +class WikipediaSearch(commands.Cog):      """Get info from wikipedia."""      def __init__(self, bot: commands.Bot): @@ -26,20 +27,22 @@ class WikipediaCog(commands.Cog):          """Formating wikipedia link with index and title."""          return f'`{index}` [{title}]({WIKIPEDIA_URL.format(title=title.replace(" ", "_"))})' -    async def search_wikipedia(self, search_term: str) -> List[str]: +    async def search_wikipedia(self, search_term: str) -> Optional[List[str]]:          """Search wikipedia and return the first 10 pages found.""" -        async with self.http_session.get(SEARCH_API.format(search_term=search_term)) as response: -            data = await response.json() -          pages = [] +        async with self.http_session.get(SEARCH_API.format(search_term=search_term)) as response: +            try: +                data = await response.json() -        search_results = data["query"]["search"] +                search_results = data["query"]["search"] -        # Ignore pages with "may refer to" -        for search_result in search_results: -            log.info("trying to append titles") -            if "may refer to" not in search_result["snippet"]: -                pages.append(search_result["title"]) +                # Ignore pages with "may refer to" +                for search_result in search_results: +                    log.info("trying to append titles") +                    if "may refer to" not in search_result["snippet"]: +                        pages.append(search_result["title"]) +            except client_exceptions.ContentTypeError: +                pages = None          log.info("Finished appending titles")          return pages @@ -108,4 +111,4 @@ class WikipediaCog(commands.Cog):  def setup(bot: commands.Bot) -> None:      """Wikipedia Cog load.""" -    bot.add_cog(WikipediaCog(bot)) +    bot.add_cog(WikipediaSearch(bot)) diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py index b5ad1c4f..78acf391 100644 --- a/bot/exts/halloween/hacktober-issue-finder.py +++ b/bot/exts/halloween/hacktober-issue-finder.py @@ -7,13 +7,19 @@ import aiohttp  import discord  from discord.ext import commands -from bot.constants import Month +from bot.constants import Month, Tokens  from bot.utils.decorators import in_month  log = logging.getLogger(__name__)  URL = "https://api.github.com/search/issues?per_page=100&q=is:issue+label:hacktoberfest+language:python+state:open" -HEADERS = {"Accept": "application / vnd.github.v3 + json"} + +REQUEST_HEADERS = { +    "User-Agent": "Python Discord Hacktoberbot", +    "Accept": "application / vnd.github.v3 + json" +} +if GITHUB_TOKEN := Tokens.github: +    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"  class HacktoberIssues(commands.Cog): @@ -66,7 +72,7 @@ class HacktoberIssues(commands.Cog):                      url += f"&page={page}"              log.debug(f"making api request to url: {url}") -            async with session.get(url, headers=HEADERS) as response: +            async with session.get(url, headers=REQUEST_HEADERS) as response:                  if response.status != 200:                      log.error(f"expected 200 status (got {response.status}) from the GitHub api.")                      await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.") diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index db5e37f2..ed1755e3 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -10,7 +10,7 @@ import aiohttp  import discord  from discord.ext import commands -from bot.constants import Channels, Month, WHITELISTED_CHANNELS +from bot.constants import Channels, Month, Tokens, WHITELISTED_CHANNELS  from bot.utils.decorators import in_month, override_in_channel  from bot.utils.persist import make_persistent @@ -18,7 +18,16 @@ log = logging.getLogger(__name__)  CURRENT_YEAR = datetime.now().year  # Used to construct GH API query  PRS_FOR_SHIRT = 4  # Minimum number of PRs before a shirt is awarded -HACKTOBER_WHITELIST = WHITELISTED_CHANNELS + (Channels.hacktoberfest_2019,) +HACKTOBER_WHITELIST = WHITELISTED_CHANNELS + (Channels.hacktoberfest_2020,) + +REQUEST_HEADERS = {"User-Agent": "Python Discord Hacktoberbot"} +if GITHUB_TOKEN := Tokens.github: +    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" + +GITHUB_NONEXISTENT_USER_MESSAGE = ( +    "The listed users cannot be searched either because the users do not exist " +    "or you do not have permission to view the users." +)  class HacktoberStats(commands.Cog): @@ -29,7 +38,7 @@ class HacktoberStats(commands.Cog):          self.link_json = make_persistent(Path("bot", "resources", "halloween", "github_links.json"))          self.linked_accounts = self.load_linked_users() -    @in_month(Month.OCTOBER) +    @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True)      @override_in_channel(HACKTOBER_WHITELIST)      async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None: @@ -57,7 +66,7 @@ class HacktoberStats(commands.Cog):          await self.get_stats(ctx, github_username) -    @in_month(Month.OCTOBER) +    @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="link")      @override_in_channel(HACKTOBER_WHITELIST)      async def link_user(self, ctx: commands.Context, github_username: str = None) -> None: @@ -92,7 +101,7 @@ class HacktoberStats(commands.Cog):              logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")              await ctx.send(f"{author_mention}, a GitHub username is required to link your account") -    @in_month(Month.OCTOBER) +    @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="unlink")      @override_in_channel(HACKTOBER_WHITELIST)      async def unlink_user(self, ctx: commands.Context) -> None: @@ -175,11 +184,11 @@ class HacktoberStats(commands.Cog):          n = pr_stats['n_prs']          if n >= PRS_FOR_SHIRT: -            shirtstr = f"**{github_username} has earned a tshirt!**" +            shirtstr = f"**{github_username} has earned a T-shirt or a tree!**"          elif n == PRS_FOR_SHIRT - 1: -            shirtstr = f"**{github_username} is 1 PR away from a tshirt!**" +            shirtstr = f"**{github_username} is 1 PR away from a T-shirt or a tree!**"          else: -            shirtstr = f"**{github_username} is {PRS_FOR_SHIRT - n} PRs away from a tshirt!**" +            shirtstr = f"**{github_username} is {PRS_FOR_SHIRT - n} PRs away from a T-shirt or a tree!**"          stats_embed = discord.Embed(              title=f"{github_username}'s Hacktoberfest", @@ -196,7 +205,7 @@ class HacktoberStats(commands.Cog):          stats_embed.set_author(              name="Hacktoberfest",              url="https://hacktoberfest.digitalocean.com", -            icon_url="https://hacktoberfest.digitalocean.com/pretty_logo.png" +            icon_url="https://avatars1.githubusercontent.com/u/35706162?s=200&v=4"          )          stats_embed.add_field(              name="Top 5 Repositories:", @@ -242,16 +251,22 @@ class HacktoberStats(commands.Cog):              f"&per_page={per_page}"          ) -        headers = {"user-agent": "Discord Python Hacktoberbot"}          async with aiohttp.ClientSession() as session: -            async with session.get(query_url, headers=headers) as resp: +            async with session.get(query_url, headers=REQUEST_HEADERS) as resp:                  jsonresp = await resp.json()          if "message" in jsonresp.keys():              # One of the parameters is invalid, short circuit for now              api_message = jsonresp["errors"][0]["message"] -            logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}") + +            # Ignore logging non-existent users or users we do not have permission to see +            if api_message == GITHUB_NONEXISTENT_USER_MESSAGE: +                logging.debug(f"No GitHub user found named '{github_username}'") +            else: +                logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}") +              return +          else:              if jsonresp["total_count"] == 0:                  # Short circuit if there aren't any PRs diff --git a/bot/exts/halloween/spookysound.py b/bot/exts/halloween/spookysound.py deleted file mode 100644 index 569a9153..00000000 --- a/bot/exts/halloween/spookysound.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import random -from pathlib import Path - -import discord -from discord.ext import commands - -from bot.bot import SeasonalBot -from bot.constants import Hacktoberfest - -log = logging.getLogger(__name__) - - -class SpookySound(commands.Cog): -    """A cog that plays a spooky sound in a voice channel on command.""" - -    def __init__(self, bot: SeasonalBot): -        self.bot = bot -        self.sound_files = list(Path("bot/resources/halloween/spookysounds").glob("*.mp3")) -        self.channel = None - -    @commands.cooldown(rate=1, per=1) -    @commands.command(brief="Play a spooky sound, restricted to once per 2 mins") -    async def spookysound(self, ctx: commands.Context) -> None: -        """ -        Connect to the Hacktoberbot voice channel, play a random spooky sound, then disconnect. - -        Cannot be used more than once in 2 minutes. -        """ -        if not self.channel: -            await self.bot.wait_until_guild_available() -            self.channel = self.bot.get_channel(Hacktoberfest.voice_id) - -        await ctx.send("Initiating spooky sound...") -        file_path = random.choice(self.sound_files) -        src = discord.FFmpegPCMAudio(str(file_path.resolve())) -        voice = await self.channel.connect() -        voice.play(src, after=lambda e: self.bot.loop.create_task(self.disconnect(voice))) - -    @staticmethod -    async def disconnect(voice: discord.VoiceClient) -> None: -        """Helper method to disconnect a given voice client.""" -        await voice.disconnect() - - -def setup(bot: SeasonalBot) -> None: -    """Spooky sound Cog load.""" -    bot.add_cog(SpookySound(bot)) diff --git a/bot/exts/halloween/timeleft.py b/bot/exts/halloween/timeleft.py index 295acc89..47adb09b 100644 --- a/bot/exts/halloween/timeleft.py +++ b/bot/exts/halloween/timeleft.py @@ -13,20 +13,23 @@ class TimeLeft(commands.Cog):      def __init__(self, bot: commands.Bot):          self.bot = bot -    @staticmethod -    def in_october() -> bool: -        """Return True if the current month is October.""" -        return datetime.utcnow().month == 10 +    def in_hacktober(self) -> bool: +        """Return True if the current time is within Hacktoberfest.""" +        _, end, start = self.load_date() + +        now = datetime.utcnow() + +        return start <= now <= end      @staticmethod -    def load_date() -> Tuple[int, datetime, datetime]: +    def load_date() -> Tuple[datetime, datetime, datetime]:          """Return of a tuple of the current time and the end and start times of the next October."""          now = datetime.utcnow()          year = now.year          if now.month > 10:              year += 1 -        end = datetime(year, 11, 1, 11, 59, 59) -        start = datetime(year, 10, 1) +        end = datetime(year, 11, 1, 12)  # November 1st 12:00 (UTC-12:00) +        start = datetime(year, 9, 30, 10)  # September 30th 10:00 (UTC+14:00)          return now, end, start      @commands.command() @@ -35,16 +38,23 @@ class TimeLeft(commands.Cog):          Calculates the time left until the end of Hacktober.          Whilst in October, displays the days, hours and minutes left. -        Only displays the days left until the beginning and end whilst in a different month +        Only displays the days left until the beginning and end whilst in a different month. + +        This factors in that Hacktoberfest starts when it is October anywhere in the world +        and ends with the same rules. It treats the start as UTC+14:00 and the end as +        UTC-12.          """          now, end, start = self.load_date()          diff = end - now          days, seconds = diff.days, diff.seconds -        if self.in_october(): +        if self.in_hacktober():              minutes = seconds // 60              hours, minutes = divmod(minutes, 60) -            await ctx.send(f"There is currently only {days} days, {hours} hours and {minutes}" -                           "minutes left until the end of Hacktober.") + +            await ctx.send( +                f"There are {days} days, {hours} hours and {minutes}" +                f" minutes left until the end of Hacktober." +            )          else:              start_diff = start - now              start_days = start_diff.days diff --git a/bot/exts/utils/__init__.py b/bot/exts/utils/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/bot/exts/utils/__init__.py diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py new file mode 100644 index 00000000..102a0416 --- /dev/null +++ b/bot/exts/utils/extensions.py @@ -0,0 +1,265 @@ +import functools +import logging +import typing as t +from enum import Enum + +from discord import Colour, Embed +from discord.ext import commands +from discord.ext.commands import Context, group + +from bot import exts +from bot.bot import SeasonalBot as Bot +from bot.constants import Client, Emojis, MODERATION_ROLES, Roles +from bot.utils.checks import with_role_check +from bot.utils.extensions import EXTENSIONS, unqualify +from bot.utils.pagination import LinePaginator + +log = logging.getLogger(__name__) + + +UNLOAD_BLACKLIST = {f"{exts.__name__}.utils.extensions"} +BASE_PATH_LEN = len(exts.__name__.split(".")) + + +class Action(Enum): +    """Represents an action to perform on an extension.""" + +    # Need to be partial otherwise they are considered to be function definitions. +    LOAD = functools.partial(Bot.load_extension) +    UNLOAD = functools.partial(Bot.unload_extension) +    RELOAD = functools.partial(Bot.reload_extension) + + +class Extension(commands.Converter): +    """ +    Fully qualify the name of an extension and ensure it exists. + +    The * and ** values bypass this when used with the reload command. +    """ + +    async def convert(self, ctx: Context, argument: str) -> str: +        """Fully qualify the name of an extension and ensure it exists.""" +        # Special values to reload all extensions +        if argument == "*" or argument == "**": +            return argument + +        argument = argument.lower() + +        if argument in EXTENSIONS: +            return argument +        elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: +            return qualified_arg + +        matches = [] +        for ext in EXTENSIONS: +            if argument == unqualify(ext): +                matches.append(ext) + +        if len(matches) > 1: +            matches.sort() +            names = "\n".join(matches) +            raise commands.BadArgument( +                f":x: `{argument}` is an ambiguous extension name. " +                f"Please use one of the following fully-qualified names.```\n{names}```" +            ) +        elif matches: +            return matches[0] +        else: +            raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + + +class Extensions(commands.Cog): +    """Extension management commands.""" + +    def __init__(self, bot: Bot): +        self.bot = bot + +    @group(name="extensions", aliases=("ext", "exts", "c", "cogs"), invoke_without_command=True) +    async def extensions_group(self, ctx: Context) -> None: +        """Load, unload, reload, and list loaded extensions.""" +        await ctx.send_help(ctx.command) + +    @extensions_group.command(name="load", aliases=("l",)) +    async def load_command(self, ctx: Context, *extensions: Extension) -> None: +        r""" +        Load extensions given their fully qualified or unqualified names. + +        If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. +        """  # noqa: W605 +        if not extensions: +            await ctx.send_help(ctx.command) +            return + +        if "*" in extensions or "**" in extensions: +            extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) + +        msg = self.batch_manage(Action.LOAD, *extensions) +        await ctx.send(msg) + +    @extensions_group.command(name="unload", aliases=("ul",)) +    async def unload_command(self, ctx: Context, *extensions: Extension) -> None: +        r""" +        Unload currently loaded extensions given their fully qualified or unqualified names. + +        If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. +        """  # noqa: W605 +        if not extensions: +            await ctx.send_help(ctx.command) +            return + +        blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) + +        if blacklisted: +            msg = f":x: The following extension(s) may not be unloaded:```{blacklisted}```" +        else: +            if "*" in extensions or "**" in extensions: +                extensions = set(self.bot.extensions.keys()) - UNLOAD_BLACKLIST + +            msg = self.batch_manage(Action.UNLOAD, *extensions) + +        await ctx.send(msg) + +    @extensions_group.command(name="reload", aliases=("r",), root_aliases=("reload",)) +    async def reload_command(self, ctx: Context, *extensions: Extension) -> None: +        r""" +        Reload extensions given their fully qualified or unqualified names. + +        If an extension fails to be reloaded, it will be rolled-back to the prior working state. + +        If '\*' is given as the name, all currently loaded extensions will be reloaded. +        If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. +        """  # noqa: W605 +        if not extensions: +            await ctx.send_help(ctx.command) +            return + +        if "**" in extensions: +            extensions = EXTENSIONS +        elif "*" in extensions: +            extensions = set(self.bot.extensions.keys()) | set(extensions) +            extensions.remove("*") + +        msg = self.batch_manage(Action.RELOAD, *extensions) + +        await ctx.send(msg) + +    @extensions_group.command(name="list", aliases=("all",)) +    async def list_command(self, ctx: Context) -> None: +        """ +        Get a list of all extensions, including their loaded status. + +        Grey indicates that the extension is unloaded. +        Green indicates that the extension is currently loaded. +        """ +        embed = Embed(colour=Colour.blurple()) +        embed.set_author( +            name="Extensions List", +            url=Client.github_bot_repo, +            icon_url=str(self.bot.user.avatar_url) +        ) + +        lines = [] +        categories = self.group_extension_statuses() +        for category, extensions in sorted(categories.items()): +            # Treat each category as a single line by concatenating everything. +            # This ensures the paginator will not cut off a page in the middle of a category. +            category = category.replace("_", " ").title() +            extensions = "\n".join(sorted(extensions)) +            lines.append(f"**{category}**\n{extensions}\n") + +        log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") +        await LinePaginator.paginate(lines, ctx, embed, max_size=1200, empty=False) + +    def group_extension_statuses(self) -> t.Mapping[str, str]: +        """Return a mapping of extension names and statuses to their categories.""" +        categories = {} + +        for ext in EXTENSIONS: +            if ext in self.bot.extensions: +                status = Emojis.status_online +            else: +                status = Emojis.status_offline + +            path = ext.split(".") +            if len(path) > BASE_PATH_LEN + 1: +                category = " - ".join(path[BASE_PATH_LEN:-1]) +            else: +                category = "uncategorised" + +            categories.setdefault(category, []).append(f"{status}  {path[-1]}") + +        return categories + +    def batch_manage(self, action: Action, *extensions: str) -> str: +        """ +        Apply an action to multiple extensions and return a message with the results. + +        If only one extension is given, it is deferred to `manage()`. +        """ +        if len(extensions) == 1: +            msg, _ = self.manage(action, extensions[0]) +            return msg + +        verb = action.name.lower() +        failures = {} + +        for extension in extensions: +            _, error = self.manage(action, extension) +            if error: +                failures[extension] = error + +        emoji = ":x:" if failures else ":ok_hand:" +        msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} extensions {verb}ed." + +        if failures: +            failures = "\n".join(f"{ext}\n    {err}" for ext, err in failures.items()) +            msg += f"\nFailures:```{failures}```" + +        log.debug(f"Batch {verb}ed extensions.") + +        return msg + +    def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: +        """Apply an action to an extension and return the status message and any error message.""" +        verb = action.name.lower() +        error_msg = None + +        try: +            action.value(self.bot, ext) +        except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): +            if action is Action.RELOAD: +                # When reloading, just load the extension if it was not loaded. +                return self.manage(Action.LOAD, ext) + +            msg = f":x: Extension `{ext}` is already {verb}ed." +            log.debug(msg[4:]) +        except Exception as e: +            if hasattr(e, "original"): +                e = e.original + +            log.exception(f"Extension '{ext}' failed to {verb}.") + +            error_msg = f"{e.__class__.__name__}: {e}" +            msg = f":x: Failed to {verb} extension `{ext}`:\n```{error_msg}```" +        else: +            msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." +            log.debug(msg[10:]) + +        return msg, error_msg + +    # This cannot be static (must have a __func__ attribute). +    def cog_check(self, ctx: Context) -> bool: +        """Only allow moderators and core developers to invoke the commands in this cog.""" +        return with_role_check(ctx, *MODERATION_ROLES, Roles.core_developers) + +    # This cannot be static (must have a __func__ attribute). +    async def cog_command_error(self, ctx: Context, error: Exception) -> None: +        """Handle BadArgument errors locally to prevent the help command from showing.""" +        if isinstance(error, commands.BadArgument): +            await ctx.send(str(error)) +            error.handled = True + + +def setup(bot: Bot) -> None: +    """Load the Extensions cog.""" +    bot.add_cog(Extensions(bot)) diff --git a/bot/utils/checks.py b/bot/utils/checks.py new file mode 100644 index 00000000..3031a271 --- /dev/null +++ b/bot/utils/checks.py @@ -0,0 +1,164 @@ +import datetime +import logging +from typing import Callable, Container, Iterable, Optional + +from discord.ext.commands import ( +    BucketType, +    CheckFailure, +    Cog, +    Command, +    CommandOnCooldown, +    Context, +    Cooldown, +    CooldownMapping, +) + +from bot import constants + +log = logging.getLogger(__name__) + + +class InWhitelistCheckFailure(CheckFailure): +    """Raised when the `in_whitelist` check fails.""" + +    def __init__(self, redirect_channel: Optional[int]) -> None: +        self.redirect_channel = redirect_channel + +        if redirect_channel: +            redirect_message = f" here. Please use the <#{redirect_channel}> channel instead" +        else: +            redirect_message = "" + +        error_message = f"You are not allowed to use that command{redirect_message}." + +        super().__init__(error_message) + + +def in_whitelist_check( +    ctx: Context, +    channels: Container[int] = (), +    categories: Container[int] = (), +    roles: Container[int] = (), +    redirect: Optional[int] = constants.Channels.seasonalbot_commands, +    fail_silently: bool = False, +) -> bool: +    """ +    Check if a command was issued in a whitelisted context. + +    The whitelists that can be provided are: + +    - `channels`: a container with channel ids for whitelisted channels +    - `categories`: a container with category ids for whitelisted categories +    - `roles`: a container with with role ids for whitelisted roles + +    If the command was invoked in a context that was not whitelisted, the member is either +    redirected to the `redirect` channel that was passed (default: #bot-commands) or simply +    told that they're not allowed to use this particular command (if `None` was passed). +    """ +    if redirect and redirect not in channels: +        # It does not make sense for the channel whitelist to not contain the redirection +        # channel (if applicable). That's why we add the redirection channel to the `channels` +        # container if it's not already in it. As we allow any container type to be passed, +        # we first create a tuple in order to safely add the redirection channel. +        # +        # Note: It's possible for the redirect channel to be in a whitelisted category, but +        # there's no easy way to check that and as a channel can easily be moved in and out of +        # categories, it's probably not wise to rely on its category in any case. +        channels = tuple(channels) + (redirect,) + +    if channels and ctx.channel.id in channels: +        log.trace(f"{ctx.author} may use the `{ctx.command.name}` command as they are in a whitelisted channel.") +        return True + +    # Only check the category id if we have a category whitelist and the channel has a `category_id` +    if categories and hasattr(ctx.channel, "category_id") and ctx.channel.category_id in categories: +        log.trace(f"{ctx.author} may use the `{ctx.command.name}` command as they are in a whitelisted category.") +        return True + +    # Only check the roles whitelist if we have one and ensure the author's roles attribute returns +    # an iterable to prevent breakage in DM channels (for if we ever decide to enable commands there). +    if roles and any(r.id in roles for r in getattr(ctx.author, "roles", ())): +        log.trace(f"{ctx.author} may use the `{ctx.command.name}` command as they have a whitelisted role.") +        return True + +    log.trace(f"{ctx.author} may not use the `{ctx.command.name}` command within this context.") + +    # Some commands are secret, and should produce no feedback at all. +    if not fail_silently: +        raise InWhitelistCheckFailure(redirect) +    return False + + +def with_role_check(ctx: Context, *role_ids: int) -> bool: +    """Returns True if the user has any one of the roles in role_ids.""" +    if not ctx.guild:  # Return False in a DM +        log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " +                  "This command is restricted by the with_role decorator. Rejecting request.") +        return False + +    for role in ctx.author.roles: +        if role.id in role_ids: +            log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.") +            return True + +    log.trace(f"{ctx.author} does not have the required role to use " +              f"the '{ctx.command.name}' command, so the request is rejected.") +    return False + + +def without_role_check(ctx: Context, *role_ids: int) -> bool: +    """Returns True if the user does not have any of the roles in role_ids.""" +    if not ctx.guild:  # Return False in a DM +        log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " +                  "This command is restricted by the without_role decorator. Rejecting request.") +        return False + +    author_roles = [role.id for role in ctx.author.roles] +    check = all(role not in author_roles for role in role_ids) +    log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +              f"The result of the without_role check was {check}.") +    return check + + +def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketType.default, *, +                              bypass_roles: Iterable[int]) -> Callable: +    """ +    Applies a cooldown to a command, but allows members with certain roles to be ignored. + +    NOTE: this replaces the `Command.before_invoke` callback, which *might* introduce problems in the future. +    """ +    # Make it a set so lookup is hash based. +    bypass = set(bypass_roles) + +    # This handles the actual cooldown logic. +    buckets = CooldownMapping(Cooldown(rate, per, type)) + +    # Will be called after the command has been parse but before it has been invoked, ensures that +    # the cooldown won't be updated if the user screws up their input to the command. +    async def predicate(cog: Cog, ctx: Context) -> None: +        nonlocal bypass, buckets + +        if any(role.id in bypass for role in ctx.author.roles): +            return + +        # Cooldown logic, taken from discord.py internals. +        current = ctx.message.created_at.replace(tzinfo=datetime.timezone.utc).timestamp() +        bucket = buckets.get_bucket(ctx.message) +        retry_after = bucket.update_rate_limit(current) +        if retry_after: +            raise CommandOnCooldown(bucket, retry_after) + +    def wrapper(command: Command) -> Command: +        # NOTE: this could be changed if a subclass of Command were to be used. I didn't see the need for it +        # so I just made it raise an error when the decorator is applied before the actual command object exists. +        # +        # If the `before_invoke` detail is ever a problem then I can quickly just swap over. +        if not isinstance(command, Command): +            raise TypeError('Decorator `cooldown_with_role_bypass` must be applied after the command decorator. ' +                            'This means it has to be above the command decorator in the code.') + +        command._before_invoke = predicate + +        return command + +    return wrapper diff --git a/bot/utils/converters.py b/bot/utils/converters.py new file mode 100644 index 00000000..228714c9 --- /dev/null +++ b/bot/utils/converters.py @@ -0,0 +1,16 @@ +import discord +from discord.ext.commands.converter import MessageConverter + + +class WrappedMessageConverter(MessageConverter): +    """A converter that handles embed-suppressed links like <http://example.com>.""" + +    async def convert(self, ctx: discord.ext.commands.Context, argument: str) -> discord.Message: +        """Wrap the commands.MessageConverter to handle <> delimited message links.""" +        # It's possible to wrap a message in [<>] as well, and it's supported because its easy +        if argument.startswith("[") and argument.endswith("]"): +            argument = argument[1:-1] +        if argument.startswith("<") and argument.endswith(">"): +            argument = argument[1:-1] + +        return await super().convert(ctx, argument) diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py new file mode 100644 index 00000000..50350ea8 --- /dev/null +++ b/bot/utils/extensions.py @@ -0,0 +1,34 @@ +import importlib +import inspect +import pkgutil +from typing import Iterator, NoReturn + +from bot import exts + + +def unqualify(name: str) -> str: +    """Return an unqualified name given a qualified module/package `name`.""" +    return name.rsplit(".", maxsplit=1)[-1] + + +def walk_extensions() -> Iterator[str]: +    """Yield extension names from the bot.exts subpackage.""" + +    def on_error(name: str) -> NoReturn: +        raise ImportError(name=name)  # pragma: no cover + +    for module in pkgutil.walk_packages(exts.__path__, f"{exts.__name__}.", onerror=on_error): +        if unqualify(module.name).startswith("_"): +            # Ignore module/package names starting with an underscore. +            continue + +        if module.ispkg: +            imported = importlib.import_module(module.name) +            if not inspect.isfunction(getattr(imported, "setup", None)): +                # If it lacks a setup function, it's not an extension. +                continue + +        yield module.name + + +EXTENSIONS = frozenset(walk_extensions()) | 
