aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile2
-rw-r--r--Pipfile.lock176
-rw-r--r--bot/__init__.py1
-rw-r--r--bot/constants.py15
-rw-r--r--bot/resources/easter/april_fools_vids.json125
-rw-r--r--bot/resources/easter/chocolate_bunny.pngbin0 -> 7789 bytes
-rw-r--r--bot/resources/persist/egg_hunt.sqlitebin0 -> 16384 bytes
-rw-r--r--bot/seasons/christmas/adventofcode.py6
-rw-r--r--bot/seasons/christmas/hanukkah_embed.py112
-rw-r--r--bot/seasons/easter/april_fools_vids.py38
-rw-r--r--bot/seasons/easter/avatar_easterifier.py132
-rw-r--r--bot/seasons/easter/egg_decorating.py1
-rw-r--r--bot/seasons/easter/egg_hunt/__init__.py12
-rw-r--r--bot/seasons/easter/egg_hunt/cog.py638
-rw-r--r--bot/seasons/easter/egg_hunt/constants.py39
-rw-r--r--bot/seasons/evergreen/snakes/snakes_cog.py9
-rw-r--r--bot/seasons/evergreen/snakes/utils.py11
-rw-r--r--bot/seasons/halloween/candy_collection.py10
-rw-r--r--bot/seasons/halloween/halloween_facts.py4
-rw-r--r--bot/seasons/halloween/spookyavatar.py5
-rw-r--r--bot/seasons/season.py2
-rw-r--r--bot/seasons/valentines/be_my_valentine.py6
22 files changed, 1216 insertions, 128 deletions
diff --git a/Pipfile b/Pipfile
index 15f9cab0..85f8dc87 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,13 +4,13 @@ verify_ssl = true
name = "pypi"
[packages]
-discord-py = {ref = "43b4475",git = "https://github.com/Rapptz/discord.py",editable = true}
arrow = "*"
beautifulsoup4 = "*"
aiodns = "*"
pillow = "*"
pytz = "*"
fuzzywuzzy = "*"
+discord-py = "~=1.1"
[dev-packages]
"flake8" = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index d4a2183c..1cf9e44a 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "d3f50052000b1e8bda6997dbdace86d5218aa19d240983f2586d990b6d18f36c"
+ "sha256": "0e7e34beb8c746a91b7e2209586e5c93663bae113b2989af8a0df849cf0d7dc7"
},
"pipfile-spec": 6,
"requires": {
@@ -84,36 +84,36 @@
},
"cffi": {
"hashes": [
- "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"
+ "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
+ "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
+ "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
+ "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
+ "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
+ "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
+ "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
+ "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
+ "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
+ "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
+ "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
+ "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
+ "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
+ "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
+ "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
+ "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
+ "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
+ "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
+ "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
+ "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
+ "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
+ "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
+ "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
+ "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
+ "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
+ "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
+ "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
+ "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
+ ],
+ "version": "==1.12.3"
},
"chardet": {
"hashes": [
@@ -123,9 +123,11 @@
"version": "==3.0.4"
},
"discord-py": {
- "editable": true,
- "git": "https://github.com/Rapptz/discord.py",
- "ref": "43b44751af647ecfcfb17868962972d543eb69a9"
+ "hashes": [
+ "sha256:d0ab22f1fee1fcc02ac50a67ff49a5d1f6d7bc7eba77e34e35bd160b3ad3d7e8"
+ ],
+ "index": "pypi",
+ "version": "==1.1.1"
},
"fuzzywuzzy": {
"hashes": [
@@ -178,39 +180,35 @@
},
"pillow": {
"hashes": [
- "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e",
- "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7",
- "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a",
- "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3",
- "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1",
- "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1",
- "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7",
- "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1",
- "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3",
- "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055",
- "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf",
- "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f",
- "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f",
- "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239",
- "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe",
- "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c",
- "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697",
- "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494",
- "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356",
- "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6",
- "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000",
- "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f",
- "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c",
- "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca",
- "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8",
- "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3",
- "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad",
- "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9",
- "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc",
- "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"
+ "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55",
+ "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479",
+ "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a",
+ "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d",
+ "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb",
+ "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb",
+ "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8",
+ "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72",
+ "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754",
+ "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f",
+ "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce",
+ "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601",
+ "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5",
+ "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734",
+ "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b",
+ "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b",
+ "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1",
+ "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91",
+ "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8",
+ "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239",
+ "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af",
+ "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8",
+ "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232",
+ "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a",
+ "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3",
+ "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062"
],
"index": "pypi",
- "version": "==5.4.1"
+ "version": "==6.0.0"
},
"pycares": {
"hashes": [
@@ -245,11 +243,11 @@
},
"pytz": {
"hashes": [
- "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
- "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
+ "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
+ "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
],
"index": "pypi",
- "version": "==2018.9"
+ "version": "==2019.1"
},
"six": {
"hashes": [
@@ -260,10 +258,10 @@
},
"soupsieve": {
"hashes": [
- "sha256:3aef141566afd07201b525c17bfaadd07580a8066f82b57f7c9417f26adbd0a3",
- "sha256:e41a65e99bd125972d84221022beb1e4b5cfc68fa12c170c39834ce32d1b294c"
+ "sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece",
+ "sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca"
],
- "version": "==1.9"
+ "version": "==1.9.1"
},
"websockets": {
"hashes": [
@@ -325,10 +323,10 @@
},
"cfgv": {
"hashes": [
- "sha256:39f8475d8eca48639f900daffa3f8bd2f60a31d989df41a9f81c5ad1779a66eb",
- "sha256:a6a4366d32799a6bfb6f577ebe113b27ba8d1bae43cb57133b1472c1c3dae227"
+ "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef",
+ "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172"
],
- "version": "==1.5.0"
+ "version": "==1.6.0"
},
"entrypoints": {
"hashes": [
@@ -401,17 +399,17 @@
},
"identify": {
"hashes": [
- "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd",
- "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171"
+ "sha256:432c548d6138cb57a3d8f62f079a025a29b8ae34a50dd3b496bbf661818f2bc0",
+ "sha256:d4401d60bf1938aa3074a352a5cc9044107edf11a6fedd3a1db172c141619b81"
],
- "version": "==1.4.1"
+ "version": "==1.4.3"
},
"importlib-metadata": {
"hashes": [
- "sha256:a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095",
- "sha256:b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"
+ "sha256:6a0080fdc87c8225e004b00b55bd1eab153a32ef5a11e17c14de81edbb8ed1a7",
+ "sha256:c0bdce522d5b215c710f237cfc1f58ace432affd3052176bbb719f53e2465256"
],
- "version": "==0.8"
+ "version": "==0.11"
},
"mccabe": {
"hashes": [
@@ -428,11 +426,11 @@
},
"pre-commit": {
"hashes": [
- "sha256:d3d69c63ae7b7584c4b51446b0b583d454548f9df92575b2fe93a68ec800c4d3",
- "sha256:fc512f129b9526e35e80d656a16a31c198f584c4fce3a5c739045b5140584917"
+ "sha256:6ca409d1f22d444af427fb023a33ca8b69625d508a50e1b7eaabd59247c93043",
+ "sha256:94dd519597f5bff06a4b0df194a79c524b78f4b1534c1ce63241a9d4fb23b926"
],
"index": "pypi",
- "version": "==1.14.4"
+ "version": "==1.16.1"
},
"pycodestyle": {
"hashes": [
@@ -495,17 +493,17 @@
},
"virtualenv": {
"hashes": [
- "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417",
- "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39"
+ "sha256:15ee248d13e4001a691d9583948ad3947bcb8a289775102e4c4aa98a8b7a6d73",
+ "sha256:bfc98bb9b42a3029ee41b96dc00a34c2f254cbf7716bec824477b2c82741a5c4"
],
- "version": "==16.4.3"
+ "version": "==16.5.0"
},
"zipp": {
"hashes": [
- "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478",
- "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4"
+ "sha256:46dfd547d9ccbf8bdc26ecea52818046bb28509f12bb6a0de1cd66ab06e9a9be",
+ "sha256:d7ac25f895fb65bff937b381353c14eb1fa23d35f40abd72a5342cd57eb57fd1"
],
- "version": "==0.3.3"
+ "version": "==0.5.0"
}
}
}
diff --git a/bot/__init__.py b/bot/__init__.py
index 21ff8c97..7c564178 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -54,6 +54,7 @@ if root.handlers:
# Silence irrelevant loggers
logging.getLogger("discord").setLevel(logging.ERROR)
logging.getLogger("websockets").setLevel(logging.ERROR)
+logging.getLogger("PIL").setLevel(logging.ERROR)
# Setup new logging configuration
logging.basicConfig(
diff --git a/bot/constants.py b/bot/constants.py
index b19d494b..bf542daf 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -18,7 +18,6 @@ class AdventOfCode:
leaderboard_join_code = str(environ.get("AOC_JOIN_CODE", None))
leaderboard_max_displayed_members = 10
year = 2018
- channel_id = int(environ.get("AOC_CHANNEL_ID", 517745814039166986))
role_id = int(environ.get("AOC_ROLE_ID", 518565788744024082))
@@ -29,7 +28,7 @@ class Channels(NamedTuple):
bot = 267659945086812160
checkpoint_test = 422077681434099723
devalerts = 460181980097675264
- devlog = int(environ.get('CHANNEL_DEVLOG', 409308876241108992))
+ devlog = int(environ.get('CHANNEL_DEVLOG', 548438471685963776))
devtest = 414574275865870337
help_0 = 303906576991780866
help_1 = 303906556754395136
@@ -46,25 +45,29 @@ class Channels(NamedTuple):
off_topic_2 = 463035268514185226
python = 267624335836053506
reddit = 458224812528238616
+ seasonalbot_chat = int(environ.get('CHANNEL_SEASONALBOT_CHAT', 542272993192050698))
staff_lounge = 464905259261755392
verification = 352442727016693763
+ python_discussion = 267624335836053506
class Client(NamedTuple):
guild = int(environ.get('SEASONALBOT_GUILD', 267624335836053506))
- prefix = "."
+ prefix = environ.get("PREFIX", ".")
token = environ.get('SEASONALBOT_TOKEN')
debug = environ.get('SEASONALBOT_DEBUG', '').lower() == 'true'
season_override = environ.get('SEASON_OVERRIDE')
class Colours:
- soft_red = 0xcd6d6d
- soft_green = 0x68c290
+ blue = 0x0279fd
bright_green = 0x01d277
dark_green = 0x1f8b4c
orange = 0xe67e22
pink = 0xcf84e0
+ soft_green = 0x68c290
+ soft_red = 0xcd6d6d
+ yellow = 0xf9f586
class Emojis:
@@ -81,12 +84,10 @@ class Emojis:
class Lovefest:
- channel_id = int(environ.get("LOVEFEST_CHANNEL_ID", 542272993192050698))
role_id = int(environ.get("LOVEFEST_ROLE_ID", 542431903886606399))
class Hacktoberfest(NamedTuple):
- channel_id = 498804484324196362
voice_id = 514420006474219521
diff --git a/bot/resources/easter/april_fools_vids.json b/bot/resources/easter/april_fools_vids.json
new file mode 100644
index 00000000..dfc01b7b
--- /dev/null
+++ b/bot/resources/easter/april_fools_vids.json
@@ -0,0 +1,125 @@
+{
+ "google": [
+ {
+ "title": "Introducing Bad Joke Detector",
+ "link": "https://youtu.be/OYcv406J_J4"
+ },
+ {
+ "title": "Introducing Google Cloud Hummus API - Find your Hummus!",
+ "link": "https://youtu.be/0_5X6N6DHyk"
+ },
+ {
+ "title": "Introducing Google Play for Pets",
+ "link": "https://youtu.be/UmJ2NBHXTqo"
+ },
+ {
+ "title": "Haptic Helpers: bringing you to your senses",
+ "link": "https://youtu.be/3MA6_21nka8"
+ },
+ {
+ "title": "Introducing Google Gnome",
+ "link": "https://youtu.be/vNOllWX-2aE"
+ },
+ {
+ "title": "Introducing Google Wind",
+ "link": "https://youtu.be/QAwL0O5nXe0"
+ },
+ {
+ "title": "Experience YouTube in #SnoopaVision",
+ "link": "https://youtu.be/DPEJB-FCItk"
+ },
+ {
+ "title": "Introducing the self-driving bicycle in the Netherlands",
+ "link": "https://youtu.be/LSZPNwZex9s"
+ },
+ {
+ "title": "Android Developer Story: The Guardian goes galactic with Android and Google Play",
+ "link": "https://youtu.be/dFrgNiweQDk"
+ },
+ {
+ "title": "Introducing new delivery technology from Google Express",
+ "link": "https://youtu.be/F0F6SnbqUcE"
+ },
+ {
+ "title": "Google Cardboard Plastic",
+ "link": "https://youtu.be/VkOuShXpoKc"
+ },
+ {
+ "title": "Google Photos: Search your photos by emoji",
+ "link": "https://youtu.be/HQtGFBbwKEk"
+ },
+ {
+ "title": "Introducing Google Actual Cloud Platform",
+ "link": "https://youtu.be/Cp10_PygJ4o"
+ },
+ {
+ "title": "Introducing Dial-Up mode",
+ "link": "https://youtu.be/XTTtkisylQw"
+ },
+ {
+ "title": "Smartbox by Inbox: the mailbox of tomorrow, today",
+ "link": "https://youtu.be/hydLZJXG3Tk"
+ },
+ {
+ "title": "Introducing Coffee to the Home",
+ "link": "https://youtu.be/U2JBFlW--UU"
+ },
+ {
+ "title": "Chrome for Android and iOS: Emojify the Web",
+ "link": "https://youtu.be/G3NXNnoGr3Y"
+ },
+ {
+ "title": "Google Maps: Pokémon Challenge",
+ "link": "https://youtu.be/4YMD6xELI_k"
+ },
+ {
+ "title": "Introducing Google Fiber to the Pole",
+ "link": "https://youtu.be/qcgWRpQP6ds"
+ },
+ {
+ "title": "Introducing Gmail Blue",
+ "link": "https://youtu.be/Zr4JwPb99qU"
+ },
+ {
+ "title": "Introducing Google Nose",
+ "link": "https://youtu.be/VFbYadm_mrw"
+ },
+ {
+ "title": "Explore Treasure Mode with Google Maps",
+ "link": "https://youtu.be/_qFFHC0eIUc"
+ },
+ {
+ "title": "YouTube's ready to select a winner",
+ "link": "https://youtu.be/H542nLTTbu0"
+ },
+ {
+ "title": "A word about Gmail Tap",
+ "link": "https://youtu.be/Je7Xq9tdCJc"
+ },
+ {
+ "title": "Introducing the Google Fiber Bar",
+ "link": "https://youtu.be/re0VRK6ouwI"
+ },
+ {
+ "title": "Introducing Gmail Tap",
+ "link": "https://youtu.be/1KhZKNZO8mQ"
+ },
+ {
+ "title": "Chrome Multitask Mode",
+ "link": "https://youtu.be/UiLSiqyDf4Y"
+ },
+ {
+ "title": "Google Maps 8-bit for NES",
+ "link": "https://youtu.be/rznYifPHxDg"
+ },
+ {
+ "title": "Being a Google Autocompleter",
+ "link": "https://youtu.be/blB_X38YSxQ"
+ },
+ {
+ "title": "Introducing Gmail Motion",
+ "link": "https://youtu.be/Bu927_ul_X0"
+ }
+ ]
+
+} \ No newline at end of file
diff --git a/bot/resources/easter/chocolate_bunny.png b/bot/resources/easter/chocolate_bunny.png
new file mode 100644
index 00000000..6b25aa5a
--- /dev/null
+++ b/bot/resources/easter/chocolate_bunny.png
Binary files differ
diff --git a/bot/resources/persist/egg_hunt.sqlite b/bot/resources/persist/egg_hunt.sqlite
new file mode 100644
index 00000000..6a7ae32d
--- /dev/null
+++ b/bot/resources/persist/egg_hunt.sqlite
Binary files differ
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 5d05dce6..440484b4 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -13,7 +13,7 @@ from bs4 import BeautifulSoup
from discord.ext import commands
from pytz import timezone
-from bot.constants import AdventOfCode as AocConfig, Colours, Emojis, Tokens
+from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens
log = logging.getLogger(__name__)
@@ -88,7 +88,7 @@ async def day_countdown(bot: commands.Bot):
await asyncio.sleep(time_left.seconds)
- channel = bot.get_channel(AocConfig.channel_id)
+ channel = bot.get_channel(Channels.seasonalbot_chat)
if not channel:
log.error("Could not find the AoC channel to send notification in")
@@ -132,7 +132,7 @@ class AdventOfCode(commands.Cog):
async def adventofcode_group(self, ctx: commands.Context):
"""All of the Advent of Code commands."""
- await ctx.invoke(self.bot.get_command("help"), "adventofcode")
+ await ctx.send_help(ctx.command)
@adventofcode_group.command(
name="subscribe",
diff --git a/bot/seasons/christmas/hanukkah_embed.py b/bot/seasons/christmas/hanukkah_embed.py
new file mode 100644
index 00000000..652a1f35
--- /dev/null
+++ b/bot/seasons/christmas/hanukkah_embed.py
@@ -0,0 +1,112 @@
+import datetime
+import logging
+
+from discord import Embed
+from discord.ext import commands
+
+from bot.constants import Colours
+
+
+log = logging.getLogger(__name__)
+
+
+class HanukkahEmbed(commands.Cog):
+ """A cog that returns information about Hanukkah festival."""
+
+ def __init__(self, bot):
+ self.bot = bot
+ self.url = ("https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
+ "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on")
+ self.hanukkah_days = []
+ self.hanukkah_months = []
+ self.hanukkah_years = []
+
+ async def get_hanukkah_dates(self):
+ """Gets the dates for hanukkah festival."""
+ hanukkah_dates = []
+ async with self.bot.http_session.get(self.url) as response:
+ json_data = await response.json()
+ festivals = json_data['items']
+ for festival in festivals:
+ if festival['title'].startswith('Chanukah'):
+ date = festival['date']
+ hanukkah_dates.append(date)
+ return hanukkah_dates
+
+ @commands.command(name='hanukkah', aliases=['chanukah'])
+ async def hanukkah_festival(self, ctx):
+ """Tells you about the Hanukkah Festivaltime of festival, festival day, etc)."""
+ hanukkah_dates = await self.get_hanukkah_dates()
+ self.hanukkah_dates_split(hanukkah_dates)
+ hanukkah_start_day = int(self.hanukkah_days[0])
+ hanukkah_start_month = int(self.hanukkah_months[0])
+ hanukkah_start_year = int(self.hanukkah_years[0])
+ hanukkah_end_day = int(self.hanukkah_days[8])
+ hanukkah_end_month = int(self.hanukkah_months[8])
+ hanukkah_end_year = int(self.hanukkah_years[8])
+
+ hanukkah_start = datetime.date(hanukkah_start_year, hanukkah_start_month, hanukkah_start_day)
+ hanukkah_end = datetime.date(hanukkah_end_year, hanukkah_end_month, hanukkah_end_day)
+ today = datetime.date.today()
+ # today = datetime.date(2019, 12, 24) (for testing)
+ day = str(today.day)
+ month = str(today.month)
+ year = str(today.year)
+ embed = Embed()
+ embed.title = 'Hanukkah'
+ embed.colour = Colours.blue
+ if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years:
+ if int(day) == hanukkah_start_day:
+ now = datetime.datetime.utcnow()
+ now = str(now)
+ hours = int(now[11:13]) + 4 # using only hours
+ hanukkah_start_hour = 18
+ if hours < hanukkah_start_hour:
+ embed.description = (f"Hanukkah hasnt started yet, "
+ f"it will start in about {hanukkah_start_hour-hours} hour/s.")
+ return await ctx.send(embed=embed)
+ elif hours > hanukkah_start_hour:
+ embed.description = (f'It is the starting day of Hanukkah ! '
+ f'Its been {hours-hanukkah_start_hour} hours hanukkah started !')
+ return await ctx.send(embed=embed)
+ festival_day = self.hanukkah_days.index(day)
+ number_suffixes = ['st', 'nd', 'rd', 'th']
+ suffix = ''
+ if int(festival_day) == 1:
+ suffix = number_suffixes[0]
+ if int(festival_day) == 2:
+ suffix = number_suffixes[1]
+ if int(festival_day) == 3:
+ suffix = number_suffixes[2]
+ if int(festival_day) > 3:
+ suffix = number_suffixes[3]
+ message = ''
+ for _ in range(1, festival_day + 1):
+ message += ':menorah:'
+ embed.description = f'It is the {festival_day}{suffix} day of Hanukkah ! \n {message}'
+ await ctx.send(embed=embed)
+ else:
+ if today < hanukkah_start:
+ festival_starting_month = hanukkah_start.strftime('%B')
+ embed.description = (f"Hanukkah has not started yet. "
+ f"Hanukkah will start at sundown on {hanukkah_start_day}th "
+ f"of {festival_starting_month}.")
+ else:
+ festival_end_month = hanukkah_end.strftime('%B')
+ embed.description = (f"Looks like you missed Hanukkah !"
+ f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}.")
+
+ await ctx.send(embed=embed)
+
+ def hanukkah_dates_split(self, hanukkah_dates):
+ """We are splitting the dates for hanukkah into days, months and years."""
+ for date in hanukkah_dates:
+ self.hanukkah_days.append(date[8:10])
+ self.hanukkah_months.append(date[5:7])
+ self.hanukkah_years.append(date[0:4])
+
+
+def setup(bot):
+ """Cog load."""
+ bot.add_cog(HanukkahEmbed(bot))
+ log.info("Hanukkah embed cog loaded")
diff --git a/bot/seasons/easter/april_fools_vids.py b/bot/seasons/easter/april_fools_vids.py
new file mode 100644
index 00000000..5dae8485
--- /dev/null
+++ b/bot/seasons/easter/april_fools_vids.py
@@ -0,0 +1,38 @@
+import logging
+import random
+from json import load
+from pathlib import Path
+
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+
+
+class AprilFoolVideos(commands.Cog):
+ """A cog for april fools that gets a random april fools video from youtube."""
+ def __init__(self, bot):
+ self.bot = bot
+ self.yt_vids = self.load_json()
+ self.youtubers = ['google'] # will add more in future
+
+ @staticmethod
+ def load_json():
+ """A function to load json data."""
+ p = Path('bot', 'resources', 'easter', 'april_fools_vids.json')
+ with p.open() as json_file:
+ all_vids = load(json_file)
+ return all_vids
+
+ @commands.command(name='fool')
+ async def aprial_fools(self, ctx):
+ """Gets a random april fools video from youtube."""
+ random_youtuber = random.choice(self.youtubers)
+ category = self.yt_vids[random_youtuber]
+ random_vid = random.choice(category)
+ await ctx.send(f"Check out this April Fools' video by {random_youtuber}.\n\n{random_vid['link']}")
+
+
+def setup(bot):
+ """A function to add the cog."""
+ bot.add_cog(AprilFoolVideos(bot))
+ log.info('April Fools videos cog loaded!')
diff --git a/bot/seasons/easter/avatar_easterifier.py b/bot/seasons/easter/avatar_easterifier.py
new file mode 100644
index 00000000..a84e5eb4
--- /dev/null
+++ b/bot/seasons/easter/avatar_easterifier.py
@@ -0,0 +1,132 @@
+import asyncio
+import logging
+from io import BytesIO
+from pathlib import Path
+from typing import Union
+
+import discord
+from PIL import Image
+from PIL.ImageOps import posterize
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+
+COLOURS = [
+ (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203),
+ (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238),
+ (135, 206, 235), (0, 204, 204), (64, 224, 208)
+] # Pastel colours - Easter-like
+
+
+class AvatarEasterifier(commands.Cog):
+ """Put an Easter spin on your avatar or image!"""
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @staticmethod
+ def closest(x):
+ """
+ Finds the closest easter colour to a given pixel.
+
+ Returns a merge between the original colour and the closest colour
+ """
+
+ r1, g1, b1 = x
+
+ def distance(point):
+ """Finds the difference between a pastel colour and the original pixel colour"""
+
+ r2, g2, b2 = point
+ return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2)
+
+ closest_colours = sorted(COLOURS, key=lambda point: distance(point))
+ r2, g2, b2 = closest_colours[0]
+ r = (r1 + r2) // 2
+ g = (g1 + g2) // 2
+ b = (b1 + b2) // 2
+ return (r, g, b)
+
+ @commands.command(pass_context=True, aliases=["easterify"])
+ async def avatareasterify(self, ctx, *colours: Union[discord.Colour, str]):
+ """
+ This "Easterifies" the user's avatar.
+
+ Given colours will produce a personalised egg in the corner, similar to the egg_decorate command.
+ If colours are not given, a nice little chocolate bunny will sit in the corner.
+ Colours are split by spaces, unless you wrap the colour name in double quotes.
+ Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
+ """
+
+ async def send(*args, **kwargs):
+ """
+ This replaces the original ctx.send.
+
+ When invoking the egg decorating command, the egg itself doesn't print to to the channel.
+ Returns the message content so that if any errors occur, the error message can be output.
+ """
+ if args:
+ return args[0]
+
+ async with ctx.typing():
+
+ # Grabs image of avatar
+ image_bytes = await ctx.author.avatar_url_as(size=256).read()
+
+ old = Image.open(BytesIO(image_bytes))
+ old = old.convert("RGBA")
+
+ # Grabs alpha channel since posterize can't be used with an RGBA image.
+ alpha = old.getchannel("A").getdata()
+ old = old.convert("RGB")
+ old = posterize(old, 6)
+
+ data = old.getdata()
+ setted_data = set(data)
+ new_d = {}
+
+ for x in setted_data:
+ new_d[x] = self.closest(x)
+ await asyncio.sleep(0) # Ensures discord doesn't break in the background.
+ new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)]
+
+ im = Image.new("RGBA", old.size)
+ im.putdata(new_data)
+
+ if colours:
+ send_message = ctx.send
+ ctx.send = send # Assigns ctx.send to a fake send
+ egg = await ctx.invoke(self.bot.get_command("eggdecorate"), *colours)
+ if isinstance(egg, str): # When an error message occurs in eggdecorate.
+ return await send_message(egg)
+
+ ratio = 64 / egg.height
+ egg = egg.resize((round(egg.width * ratio), round(egg.height * ratio)))
+ egg = egg.convert("RGBA")
+ im.alpha_composite(egg, (im.width - egg.width, (im.height - egg.height)//2)) # Right centre.
+ ctx.send = send_message # Reassigns ctx.send
+ else:
+ bunny = Image.open(Path("bot", "resources", "easter", "chocolate_bunny.png"))
+ im.alpha_composite(bunny, (im.width - bunny.width, (im.height - bunny.height)//2)) # Right centre.
+
+ bufferedio = BytesIO()
+ im.save(bufferedio, format="PNG")
+
+ bufferedio.seek(0)
+
+ file = discord.File(bufferedio, filename="easterified_avatar.png") # Creates file to be used in embed
+ embed = discord.Embed(
+ name="Your Lovely Easterified Avatar",
+ description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D"
+ )
+ embed.set_image(url="attachment://easterified_avatar.png")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
+
+ await ctx.send(file=file, embed=embed)
+
+
+def setup(bot):
+ """Cog load."""
+
+ bot.add_cog(AvatarEasterifier(bot))
+ log.info("AvatarEasterifier cog loaded")
diff --git a/bot/seasons/easter/egg_decorating.py b/bot/seasons/easter/egg_decorating.py
index b5f3e428..d283e42a 100644
--- a/bot/seasons/easter/egg_decorating.py
+++ b/bot/seasons/easter/egg_decorating.py
@@ -109,6 +109,7 @@ class EggDecorating(commands.Cog):
embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
await ctx.send(file=file, embed=embed)
+ return new_im
def setup(bot):
diff --git a/bot/seasons/easter/egg_hunt/__init__.py b/bot/seasons/easter/egg_hunt/__init__.py
new file mode 100644
index 00000000..43bda223
--- /dev/null
+++ b/bot/seasons/easter/egg_hunt/__init__.py
@@ -0,0 +1,12 @@
+import logging
+
+from .cog import EggHunt
+
+log = logging.getLogger(__name__)
+
+
+def setup(bot):
+ """Easter Egg Hunt Cog load."""
+
+ bot.add_cog(EggHunt())
+ log.info("EggHunt cog loaded")
diff --git a/bot/seasons/easter/egg_hunt/cog.py b/bot/seasons/easter/egg_hunt/cog.py
new file mode 100644
index 00000000..c9e2dc18
--- /dev/null
+++ b/bot/seasons/easter/egg_hunt/cog.py
@@ -0,0 +1,638 @@
+import asyncio
+import contextlib
+import logging
+import random
+import sqlite3
+from datetime import datetime, timezone
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Channels, Client, Roles as MainRoles, bot
+from bot.decorators import with_role
+from .constants import Colours, EggHuntSettings, Emoji, Roles
+
+log = logging.getLogger(__name__)
+
+DB_PATH = Path("bot", "resources", "persist", "egg_hunt.sqlite")
+
+TEAM_MAP = {
+ Roles.white: Emoji.egg_white,
+ Roles.blurple: Emoji.egg_blurple,
+ Emoji.egg_white: Roles.white,
+ Emoji.egg_blurple: Roles.blurple
+}
+
+GUILD = bot.get_guild(Client.guild)
+
+MUTED = GUILD.get_role(MainRoles.muted)
+
+
+def get_team_role(user: discord.Member) -> discord.Role:
+ """Helper function to get the team role for a member."""
+
+ if Roles.white in user.roles:
+ return Roles.white
+ if Roles.blurple in user.roles:
+ return Roles.blurple
+
+
+async def assign_team(user: discord.Member) -> discord.Member:
+ """Helper function to assign a new team role for a member."""
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+ c.execute(f"SELECT team FROM user_scores WHERE user_id = {user.id}")
+ result = c.fetchone()
+ if not result:
+ c.execute(
+ "SELECT team, COUNT(*) AS count FROM user_scores "
+ "GROUP BY team ORDER BY count ASC LIMIT 1;"
+ )
+ result = c.fetchone()
+ result = result[0] if result else "WHITE"
+
+ if result[0] == "WHITE":
+ new_team = Roles.white
+ else:
+ new_team = Roles.blurple
+
+ db.close()
+
+ log.debug(f"Assigned role {new_team} to {user}.")
+
+ await user.add_roles(new_team)
+ return GUILD.get_member(user.id)
+
+
+class EggMessage:
+ """Handles a single egg reaction drop session."""
+
+ def __init__(self, message: discord.Message, egg: discord.Emoji):
+ self.message = message
+ self.egg = egg
+ self.first = None
+ self.users = set()
+ self.teams = {Roles.white: "WHITE", Roles.blurple: "BLURPLE"}
+ self.new_team_assignments = {}
+ self.timeout_task = None
+
+ @staticmethod
+ def add_user_score_sql(user_id: int, team: str, score: int) -> str:
+ """Builds the SQL for adding a score to a user in the database."""
+
+ return (
+ "INSERT INTO user_scores(user_id, team, score)"
+ f"VALUES({user_id}, '{team}', {score})"
+ f"ON CONFLICT (user_id) DO UPDATE SET score=score+{score}"
+ )
+
+ @staticmethod
+ def add_team_score_sql(team_name: str, score: int) -> str:
+ """Builds the SQL for adding a score to a team in the database."""
+
+ return f"UPDATE team_scores SET team_score=team_score+{score} WHERE team_id='{team_name}'"
+
+ def finalise_score(self):
+ """Sums and actions scoring for this egg drop session."""
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+
+ team_scores = {"WHITE": 0, "BLURPLE": 0}
+
+ first_team = get_team_role(self.first)
+ if not first_team:
+ log.debug("User without team role!")
+ db.close()
+ return
+
+ score = 3 if first_team == TEAM_MAP[first_team] else 2
+
+ c.execute(self.add_user_score_sql(self.first.id, self.teams[first_team], score))
+ team_scores[self.teams[first_team]] += score
+
+ for user in self.users:
+ team = get_team_role(user)
+ if not team:
+ log.debug("User without team role!")
+ continue
+
+ team_name = self.teams[team]
+ team_scores[team_name] += 1
+ score = 2 if team == first_team else 1
+ c.execute(self.add_user_score_sql(user.id, team_name, score))
+
+ for team_name, score in team_scores.items():
+ if not score:
+ continue
+ c.execute(self.add_team_score_sql(team_name, score))
+
+ db.commit()
+ db.close()
+
+ log.debug(
+ f"EggHunt session finalising: ID({self.message.id}) "
+ f"FIRST({self.first}) REST({self.users})."
+ )
+
+ async def start_timeout(self, seconds: int = 5):
+ """Begins a task that will sleep until the given seconds before finalizing the session."""
+
+ if self.timeout_task:
+ self.timeout_task.cancel()
+ self.timeout_task = None
+
+ await asyncio.sleep(seconds)
+
+ bot.remove_listener(self.collect_reacts, name="on_reaction_add")
+
+ with contextlib.suppress(discord.Forbidden):
+ await self.message.clear_reactions()
+
+ if self.first:
+ self.finalise_score()
+
+ def is_valid_react(self, reaction: discord.Reaction, user: discord.Member) -> bool:
+ """Validates a reaction event was meant for this session."""
+
+ if user.bot:
+ return False
+ if reaction.message.id != self.message.id:
+ return False
+ if reaction.emoji != self.egg:
+ return False
+
+ # ignore the pushished
+ if MUTED in user.roles:
+ return False
+
+ return True
+
+ async def collect_reacts(self, reaction: discord.Reaction, user: discord.Member):
+ """Handles emitted reaction_add events via listener."""
+
+ if not self.is_valid_react(reaction, user):
+ return
+
+ team = get_team_role(user)
+ if not team:
+ log.debug(f"Assigning a team for {user}.")
+ user = await assign_team(user)
+
+ if not self.first:
+ log.debug(f"{user} was first to react to egg on {self.message.id}.")
+ self.first = user
+ await self.start_timeout()
+ else:
+ if user != self.first:
+ self.users.add(user)
+
+ async def start(self):
+ """Starts the egg drop session."""
+
+ log.debug(f"EggHunt session started for message {self.message.id}.")
+ bot.add_listener(self.collect_reacts, name="on_reaction_add")
+ with contextlib.suppress(discord.Forbidden):
+ await self.message.add_reaction(self.egg)
+ self.timeout_task = asyncio.create_task(self.start_timeout(300))
+ while True:
+ if not self.timeout_task:
+ break
+ if not self.timeout_task.done():
+ await self.timeout_task
+ else:
+ # make sure any exceptions raise if necessary
+ self.timeout_task.result()
+ break
+
+
+class SuperEggMessage(EggMessage):
+ """Handles a super egg session."""
+
+ def __init__(self, message: discord.Message, egg: discord.Emoji, window: int):
+ super().__init__(message, egg)
+ self.window = window
+
+ async def finalise_score(self):
+ """Sums and actions scoring for this super egg session."""
+ try:
+ message = await self.message.channel.fetch_message(self.message.id)
+ except discord.NotFound:
+ return
+
+ count = 0
+ white = 0
+ blurple = 0
+ react_users = []
+ for reaction in message.reactions:
+ if reaction.emoji == self.egg:
+ react_users = await reaction.users().flatten()
+ for user in react_users:
+ team = get_team_role(user)
+ if team == Roles.white:
+ white += 1
+ elif team == Roles.blurple:
+ blurple += 1
+ count = reaction.count - 1
+ break
+
+ score = 50 if self.egg == Emoji.egg_gold else 100
+ if white == blurple:
+ log.debug("Tied SuperEgg Result.")
+ team = None
+ score /= 2
+ elif white > blurple:
+ team = Roles.white
+ else:
+ team = Roles.blurple
+
+ embed = self.message.embeds[0]
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+
+ user_bonus = 5 if self.egg == Emoji.egg_gold else 10
+ for user in react_users:
+ if user.bot:
+ continue
+ role = get_team_role(user)
+ if not role:
+ print("issue")
+ user_score = 1 if user != self.first else user_bonus
+ c.execute(self.add_user_score_sql(user.id, self.teams[role], user_score))
+
+ if not team:
+ embed.description = f"{embed.description}\n\nA Tie!\nBoth got {score} points!"
+ c.execute(self.add_team_score_sql(self.teams[Roles.white], score))
+ c.execute(self.add_team_score_sql(self.teams[Roles.blurple], score))
+ team_name = "TIE"
+ else:
+ team_name = self.teams[team]
+ embed.description = (
+ f"{embed.description}\n\nTeam {team_name.capitalize()} won the points!"
+ )
+ c.execute(self.add_team_score_sql(team_name, score))
+
+ c.execute(
+ "INSERT INTO super_eggs (message_id, egg_type, team, window) "
+ f"VALUES ({self.message.id}, '{self.egg.name}', '{team_name}', {self.window});"
+ )
+
+ log.debug("Committing Super Egg scores.")
+ db.commit()
+ db.close()
+
+ embed.set_footer(text=f"Finished with {count} total reacts.")
+ with contextlib.suppress(discord.HTTPException):
+ await self.message.edit(embed=embed)
+
+ async def start_timeout(self, seconds=None):
+ """Starts the super egg session."""
+
+ if not seconds:
+ return
+ count = 4
+ for _ in range(count):
+ await asyncio.sleep(60)
+ embed = self.message.embeds[0]
+ embed.set_footer(text=f"Finishing in {count} minutes.")
+ try:
+ await self.message.edit(embed=embed)
+ except discord.HTTPException:
+ break
+ count -= 1
+ bot.remove_listener(self.collect_reacts, name="on_reaction_add")
+ await self.finalise_score()
+
+
+class EggHunt(commands.Cog):
+ """Easter Egg Hunt Event."""
+
+ def __init__(self):
+ self.event_channel = GUILD.get_channel(Channels.seasonalbot_chat)
+ self.super_egg_buffer = 60*60
+ self.tables = {
+ "super_eggs": (
+ "CREATE TABLE super_eggs ("
+ "message_id INTEGER NOT NULL "
+ " CONSTRAINT super_eggs_pk PRIMARY KEY, "
+ "egg_type TEXT NOT NULL, "
+ "team TEXT NOT NULL, "
+ "window INTEGER);"
+ ),
+ "team_scores": (
+ "CREATE TABLE team_scores ("
+ "team_id TEXT, "
+ "team_score INTEGER DEFAULT 0);"
+ ),
+ "user_scores": (
+ "CREATE TABLE user_scores("
+ "user_id INTEGER NOT NULL "
+ " CONSTRAINT user_scores_pk PRIMARY KEY, "
+ "team TEXT NOT NULL, "
+ "score INTEGER DEFAULT 0 NOT NULL);"
+ ),
+ "react_logs": (
+ "CREATE TABLE react_logs("
+ "member_id INTEGER NOT NULL, "
+ "message_id INTEGER NOT NULL, "
+ "reaction_id TEXT NOT NULL, "
+ "react_timestamp REAL NOT NULL);"
+ )
+ }
+ self.prepare_db()
+ self.task = asyncio.create_task(self.super_egg())
+ self.task.add_done_callback(self.task_cleanup)
+
+ def prepare_db(self):
+ """Ensures database tables all exist and if not, creates them."""
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+
+ exists_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}';"
+
+ missing_tables = []
+ for table in self.tables:
+ c.execute(exists_sql.format(table_name=table))
+ result = c.fetchone()
+ if not result:
+ missing_tables.append(table)
+
+ for table in missing_tables:
+ log.info(f"Table {table} is missing, building new one.")
+ c.execute(self.tables[table])
+
+ db.commit()
+ db.close()
+
+ def task_cleanup(self, task):
+ """Returns task result and restarts. Used as a done callback to show raised exceptions."""
+
+ task.result()
+ self.task = asyncio.create_task(self.super_egg())
+
+ @staticmethod
+ def current_timestamp() -> float:
+ """Returns a timestamp of the current UTC time."""
+
+ return datetime.utcnow().replace(tzinfo=timezone.utc).timestamp()
+
+ async def super_egg(self):
+ """Manages the timing of super egg drops."""
+
+ while True:
+ now = int(self.current_timestamp())
+
+ if now > EggHuntSettings.end_time:
+ log.debug("Hunt ended. Ending task.")
+ break
+
+ if now < EggHuntSettings.start_time:
+ remaining = EggHuntSettings.start_time - now
+ log.debug(f"Hunt not started yet. Sleeping for {remaining}.")
+ await asyncio.sleep(remaining)
+
+ log.debug(f"Hunt started.")
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+
+ current_window = None
+ next_window = None
+ windows = EggHuntSettings.windows.copy()
+ windows.insert(0, EggHuntSettings.start_time)
+ for i, window in enumerate(windows):
+ c.execute(f"SELECT COUNT(*) FROM super_eggs WHERE window={window}")
+ already_dropped = c.fetchone()[0]
+
+ if already_dropped:
+ log.debug(f"Window {window} already dropped, checking next one.")
+ continue
+
+ if now < window:
+ log.debug("Drop windows up to date, sleeping until next one.")
+ await asyncio.sleep(window-now)
+ now = int(self.current_timestamp())
+
+ current_window = window
+ next_window = windows[i+1]
+ break
+
+ count = c.fetchone()
+ db.close()
+
+ if not current_window:
+ log.debug("No drop windows left, ending task.")
+ break
+
+ log.debug(f"Current Window: {current_window}. Next Window {next_window}")
+
+ if not count:
+ if next_window < now:
+ log.debug("An Egg Drop Window was missed, dropping one now.")
+ next_drop = 0
+ else:
+ next_drop = random.randrange(now, next_window)
+
+ if next_drop:
+ log.debug(f"Sleeping until next super egg drop: {next_drop}.")
+ await asyncio.sleep(next_drop)
+
+ if random.randrange(10) <= 2:
+ egg = Emoji.egg_diamond
+ egg_type = "Diamond"
+ score = "100"
+ colour = Colours.diamond
+ else:
+ egg = Emoji.egg_gold
+ egg_type = "Gold"
+ score = "50"
+ colour = Colours.gold
+
+ embed = discord.Embed(
+ title=f"A {egg_type} Egg Has Appeared!",
+ description=f"**Worth {score} team points!**\n\n"
+ "The team with the most reactions after 5 minutes wins!",
+ colour=colour
+ )
+ embed.set_thumbnail(url=egg.url)
+ embed.set_footer(text="Finishing in 5 minutes.")
+ msg = await self.event_channel.send(embed=embed)
+ await SuperEggMessage(msg, egg, current_window).start()
+
+ log.debug("Sleeping until next window.")
+ next_loop = max(next_window - int(self.current_timestamp()), self.super_egg_buffer)
+ await asyncio.sleep(next_loop)
+
+ @commands.Cog.listener()
+ async def on_raw_reaction_add(self, payload):
+ """Reaction event listener for reaction logging for later anti-cheat analysis."""
+
+ if payload.channel_id not in EggHuntSettings.allowed_channels:
+ return
+
+ now = self.current_timestamp()
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+ c.execute(
+ "INSERT INTO react_logs(member_id, message_id, reaction_id, react_timestamp) "
+ f"VALUES({payload.user_id}, {payload.message_id}, '{payload.emoji}', {now})"
+ )
+ db.commit()
+ db.close()
+
+ @commands.Cog.listener()
+ async def on_message(self, message):
+ """Message event listener for random egg drops."""
+
+ if self.current_timestamp() < EggHuntSettings.start_time:
+ return
+
+ if message.channel.id not in EggHuntSettings.allowed_channels:
+ log.debug("Message not in Egg Hunt channel; ignored.")
+ return
+
+ if message.author.bot:
+ return
+
+ if random.randrange(100) <= 5:
+ await EggMessage(message, random.choice([Emoji.egg_white, Emoji.egg_blurple])).start()
+
+ @commands.group(invoke_without_command=True)
+ async def hunt(self, ctx):
+ """
+ For 48 hours, hunt down as many eggs randomly appearing as possible.
+
+ Standard Eggs
+ --------------
+ Egg React: +1pt
+ Team Bonus for Claimed Egg: +1pt
+ First React on Other Team Egg: +1pt
+ First React on Your Team Egg: +2pt
+
+ If you get first react, you will claim that egg for your team, allowing
+ your team to get the Team Bonus point, but be quick, as the egg will
+ disappear after 5 seconds of the first react.
+
+ Super Eggs
+ -----------
+ Gold Egg: 50 team pts, 5pts to first react
+ Diamond Egg: 100 team pts, 10pts to first react
+
+ Super Eggs only appear in #seasonalbot-chat so be sure to keep an eye
+ out. They stay around for 5 minutes and the team with the most reacts
+ wins the points.
+ """
+
+ await ctx.invoke(bot.get_command("help"), command="hunt")
+
+ @hunt.command()
+ async def countdown(self, ctx):
+ """Show the time status of the Egg Hunt event."""
+
+ now = self.current_timestamp()
+ if now > EggHuntSettings.end_time:
+ return await ctx.send("The Hunt has ended.")
+
+ difference = EggHuntSettings.start_time - now
+ if difference < 0:
+ difference = EggHuntSettings.end_time - now
+ msg = "The Egg Hunt will end in"
+ else:
+ msg = "The Egg Hunt will start in"
+
+ hours, r = divmod(difference, 3600)
+ minutes, r = divmod(r, 60)
+ await ctx.send(f"{msg} {hours:.0f}hrs, {minutes:.0f}mins & {r:.0f}secs")
+
+ @hunt.command()
+ async def leaderboard(self, ctx):
+ """Show the Egg Hunt Leaderboards."""
+
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+ c.execute(f"SELECT *, RANK() OVER(ORDER BY score DESC) AS rank FROM user_scores LIMIT 10")
+ user_result = c.fetchall()
+ c.execute(f"SELECT * FROM team_scores ORDER BY team_score DESC")
+ team_result = c.fetchall()
+ db.close()
+ output = []
+ if user_result:
+ # Get the alignment needed for the score
+ score_lengths = []
+ for result in user_result:
+ length = len(str(result[2]))
+ score_lengths.append(length)
+
+ score_length = max(score_lengths)
+ for user_id, team, score, rank in user_result:
+ user = GUILD.get_member(user_id) or user_id
+ team = team.capitalize()
+ score = f"{score}pts"
+ output.append(f"{rank:>2}. {score:>{score_length+3}} - {user} ({team})")
+ user_board = "\n".join(output)
+ else:
+ user_board = "No entries."
+ if team_result:
+ output = []
+ for team, score in team_result:
+ output.append(f"{team:<7}: {score}")
+ team_board = "\n".join(output)
+ else:
+ team_board = "No entries."
+ embed = discord.Embed(
+ title="Egg Hunt Leaderboards",
+ description=f"**Team Scores**\n```\n{team_board}\n```\n"
+ f"**Top 10 Members**\n```\n{user_board}\n```"
+ )
+ await ctx.send(embed=embed)
+
+ @hunt.command()
+ async def rank(self, ctx, *, member: discord.Member = None):
+ """Get your ranking in the Egg Hunt Leaderboard."""
+
+ member = member or ctx.author
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+ c.execute(
+ "SELECT rank FROM "
+ "(SELECT RANK() OVER(ORDER BY score DESC) AS rank, user_id FROM user_scores)"
+ f"WHERE user_id = {member.id};"
+ )
+ result = c.fetchone()
+ db.close()
+ if not result:
+ embed = discord.Embed().set_author(name=f"Egg Hunt - No Ranking")
+ else:
+ embed = discord.Embed().set_author(name=f"Egg Hunt - Rank #{result[0]}")
+ await ctx.send(embed=embed)
+
+ @with_role(MainRoles.admin)
+ @hunt.command()
+ async def clear_db(self, ctx):
+ """Resets the database to it's initial state."""
+
+ def check(msg):
+ if msg.author != ctx.author:
+ return False
+ if msg.channel != ctx.channel:
+ return False
+ return True
+ await ctx.send(
+ "WARNING: This will delete all current event data.\n"
+ "Please verify this action by replying with 'Yes, I want to delete all data.'"
+ )
+ reply_msg = await bot.wait_for('message', check=check)
+ if reply_msg.content != "Yes, I want to delete all data.":
+ return await ctx.send("Reply did not match. Aborting database deletion.")
+ db = sqlite3.connect(DB_PATH)
+ c = db.cursor()
+ c.execute("DELETE FROM super_eggs;")
+ c.execute("DELETE FROM user_scores;")
+ c.execute("UPDATE team_scores SET team_score=0")
+ db.commit()
+ db.close()
+ await ctx.send("Database successfully cleared.")
diff --git a/bot/seasons/easter/egg_hunt/constants.py b/bot/seasons/easter/egg_hunt/constants.py
new file mode 100644
index 00000000..c7d9818b
--- /dev/null
+++ b/bot/seasons/easter/egg_hunt/constants.py
@@ -0,0 +1,39 @@
+import os
+
+from discord import Colour
+
+from bot.constants import Channels, Client, bot
+
+
+GUILD = bot.get_guild(Client.guild)
+
+
+class EggHuntSettings:
+ start_time = int(os.environ["HUNT_START"])
+ end_time = start_time + 172800 # 48 hrs later
+ windows = [int(w) for w in os.environ.get("HUNT_WINDOWS").split(',')] or []
+ allowed_channels = [
+ Channels.seasonalbot_chat,
+ Channels.off_topic_0,
+ Channels.off_topic_1,
+ Channels.off_topic_2,
+ ]
+
+
+class Roles:
+ white = GUILD.get_role(569304397054607363)
+ blurple = GUILD.get_role(569304472820514816)
+
+
+class Emoji:
+ egg_white = bot.get_emoji(569266762428841989)
+ egg_blurple = bot.get_emoji(569266666094067819)
+ egg_gold = bot.get_emoji(569266900106739712)
+ egg_diamond = bot.get_emoji(569266839738384384)
+
+
+class Colours:
+ white = Colour(0xFFFFFF)
+ blurple = Colour(0x7289DA)
+ gold = Colour(0xE4E415)
+ diamond = Colour(0xECF5FF)
diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py
index 3ffdf1bf..b5fb2881 100644
--- a/bot/seasons/evergreen/snakes/snakes_cog.py
+++ b/bot/seasons/evergreen/snakes/snakes_cog.py
@@ -458,7 +458,7 @@ class Snakes(Cog):
async def snakes_group(self, ctx: Context):
"""Commands from our first code jam."""
- await ctx.invoke(self.bot.get_command("help"), "snake")
+ await ctx.send_help(ctx.command)
@bot_has_permissions(manage_messages=True)
@snakes_group.command(name='antidote')
@@ -1055,13 +1055,6 @@ class Snakes(Cog):
)
await ctx.channel.send(embed=embed)
- @snakes_group.command(name='help')
- async def help_command(self, ctx: Context):
- """Invokes the help command for the Snakes 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):
"""
diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py
index e2ed60bd..a7cb70a7 100644
--- a/bot/seasons/evergreen/snakes/utils.py
+++ b/bot/seasons/evergreen/snakes/utils.py
@@ -8,7 +8,6 @@ from itertools import product
from pathlib import Path
from typing import List, Tuple
-import aiohttp
from PIL import Image
from PIL.ImageDraw import ImageDraw
from discord import File, Member, Reaction
@@ -480,12 +479,10 @@ class SnakeAndLaddersGame:
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
+
+ avatar_bytes = await user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE).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):
"""
diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py
index 70648e64..f8ab4c60 100644
--- a/bot/seasons/halloween/candy_collection.py
+++ b/bot/seasons/halloween/candy_collection.py
@@ -7,7 +7,7 @@ import random
import discord
from discord.ext import commands
-from bot.constants import Hacktoberfest
+from bot.constants import Channels
log = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ class CandyCollection(commands.Cog):
if message.author.bot:
return
# ensure it's hacktober channel
- if message.channel.id != Hacktoberfest.channel_id:
+ if message.channel.id != Channels.seasonalbot_chat:
return
# do random check for skull first as it has the lower chance
@@ -65,7 +65,7 @@ class CandyCollection(commands.Cog):
return
# check to ensure it is in correct channel
- if message.channel.id != Hacktoberfest.channel_id:
+ if message.channel.id != Channels.seasonalbot_chat:
return
# if its not a candy or skull, and it is one of 10 most recent messages,
@@ -127,7 +127,7 @@ class CandyCollection(commands.Cog):
ten_recent = []
recent_msg = max(message.id for message
in self.bot._connection._messages
- if message.channel.id == Hacktoberfest.channel_id)
+ if message.channel.id == Channels.seasonalbot_chat)
channel = await self.hacktober_channel()
ten_recent.append(recent_msg.id)
@@ -159,7 +159,7 @@ class CandyCollection(commands.Cog):
async def hacktober_channel(self):
"""Get #hacktoberbot channel from its ID."""
- return self.bot.get_channel(id=Hacktoberfest.channel_id)
+ return self.bot.get_channel(id=Channels.seasonalbot_chat)
async def remove_reactions(self, reaction):
"""Remove all candy/skull reactions."""
diff --git a/bot/seasons/halloween/halloween_facts.py b/bot/seasons/halloween/halloween_facts.py
index ee90dbd3..ad9aa716 100644
--- a/bot/seasons/halloween/halloween_facts.py
+++ b/bot/seasons/halloween/halloween_facts.py
@@ -7,7 +7,7 @@ from pathlib import Path
import discord
from discord.ext import commands
-from bot.constants import Hacktoberfest
+from bot.constants import Channels
log = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ class HalloweenFacts(commands.Cog):
async def on_ready(self):
"""Get event Channel object and initialize fact task loop."""
- self.channel = self.bot.get_channel(Hacktoberfest.channel_id)
+ self.channel = self.bot.get_channel(Channels.seasonalbot_chat)
self.bot.loop.create_task(self._fact_publisher_task())
def random_fact(self):
diff --git a/bot/seasons/halloween/spookyavatar.py b/bot/seasons/halloween/spookyavatar.py
index 15c7c431..2cc81da8 100644
--- a/bot/seasons/halloween/spookyavatar.py
+++ b/bot/seasons/halloween/spookyavatar.py
@@ -37,8 +37,9 @@ class SpookyAvatar(commands.Cog):
embed = discord.Embed(colour=0xFF0000)
embed.title = "Is this you or am I just really paranoid?"
embed.set_author(name=str(user.name), icon_url=user.avatar_url)
- resp = await self.get(user.avatar_url)
- im = Image.open(BytesIO(resp))
+
+ image_bytes = await ctx.author.avatar_url.read()
+ im = Image.open(BytesIO(image_bytes))
modified_im = spookifications.get_random_effect(im)
modified_im.save(str(ctx.message.id)+'.png')
f = discord.File(str(ctx.message.id)+'.png')
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index 6d992276..6d99b77f 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -442,7 +442,7 @@ class SeasonManager(commands.Cog):
async def refresh(self, ctx):
"""Refreshes certain seasonal elements without reloading seasons."""
if not ctx.invoked_subcommand:
- await ctx.invoke(bot.get_command("help"), "refresh")
+ await ctx.send_help(ctx.command)
@refresh.command(name="avatar")
async def refresh_avatar(self, ctx):
diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py
index 55c4adb1..8340d7fa 100644
--- a/bot/seasons/valentines/be_my_valentine.py
+++ b/bot/seasons/valentines/be_my_valentine.py
@@ -8,7 +8,7 @@ import discord
from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType
-from bot.constants import Client, Colours, Lovefest
+from bot.constants import Channels, Client, Colours, Lovefest
log = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ class BeMyValentine(commands.Cog):
2) use the command \".lovefest unsub\" to get rid of the lovefest role.
"""
- await ctx.invoke(self.bot.get_command("help"), "lovefest")
+ await ctx.send_help(ctx.command)
@lovefest_role.command(name="sub")
async def add_role(self, ctx):
@@ -99,7 +99,7 @@ class BeMyValentine(commands.Cog):
emoji_1, emoji_2 = self.random_emoji()
lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- channel = self.bot.get_channel(Lovefest.channel_id)
+ channel = self.bot.get_channel(Channels.seasonalbot_chat)
valentine, title = self.valentine_check(valentine_type)
if user is None: